Browse Source

Closes #951: Provide a side-by-side view of rack elevations

Jeremy Stretch 8 years ago
parent
commit
13cc29cd8c

+ 4 - 5
netbox/dcim/forms.py

@@ -1,6 +1,5 @@
-import re
-
 from mptt.forms import TreeNodeChoiceField
+import re
 
 from django import forms
 from django.contrib.postgres.forms.array import SimpleArrayField
@@ -21,9 +20,9 @@ from .models import (
     DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
     Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
-    Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
-    RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
-    SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES
+    Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate,
+    RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES,
+    SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES,
 )
 
 

+ 1 - 0
netbox/dcim/urls.py

@@ -47,6 +47,7 @@ urlpatterns = [
 
     # Racks
     url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
+    url(r'^rack-elevations/$', views.RackElevationListView.as_view(), name='rack_elevation_list'),
     url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'),
     url(r'^racks/import/$', views.RackBulkImportView.as_view(), name='rack_import'),
     url(r'^racks/edit/$', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),

+ 42 - 0
netbox/dcim/views.py

@@ -6,6 +6,7 @@ from operator import attrgetter
 from django.contrib import messages
 from django.contrib.auth.decorators import permission_required
 from django.contrib.auth.mixins import PermissionRequiredMixin
+from django.core.paginator import EmptyPage, PageNotAnInteger
 from django.db.models import Count
 from django.http import HttpResponseRedirect
 from django.shortcuts import get_object_or_404, redirect, render
@@ -17,6 +18,7 @@ from ipam.models import Prefix, Service, VLAN
 from circuits.models import Circuit
 from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
 from utilities.forms import ConfirmationForm
+from utilities.paginator import EnhancedPaginator
 from utilities.views import (
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
@@ -291,6 +293,46 @@ class RackListView(ObjectListView):
     template_name = 'dcim/rack_list.html'
 
 
+class RackElevationListView(View):
+    """
+    Display a set of rack elevations side-by-side.
+    """
+
+    def get(self, request):
+
+        racks = Rack.objects.select_related(
+            'site', 'group', 'tenant', 'role'
+        ).prefetch_related(
+            'devices__device_type'
+        )
+        racks = filters.RackFilter(request.GET, racks).qs
+        total_count = racks.count()
+
+        # Pagination
+        paginator = EnhancedPaginator(racks, 25)
+        page_number = request.GET.get('page', 1)
+        try:
+            page = paginator.page(page_number)
+        except PageNotAnInteger:
+            page = paginator.page(1)
+        except EmptyPage:
+            page = paginator.page(paginator.num_pages)
+
+        # Determine rack face
+        if request.GET.get('face') == '1':
+            face_id = 1
+        else:
+            face_id = 0
+
+        return render(request, 'dcim/rack_elevation_list.html', {
+            'paginator': paginator,
+            'page': page,
+            'total_count': total_count,
+            'face_id': face_id,
+            'filter_form': forms.RackFilterForm(request.GET),
+        })
+
+
 def rack(request, pk):
 
     rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)

+ 1 - 0
netbox/templates/_base.html

@@ -58,6 +58,7 @@
                         <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
                         <ul class="dropdown-menu">
                             <li><a href="{% url 'dcim:rack_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Racks</a></li>
+                            <li><a href="{% url 'dcim:rack_elevation_list' %}"><i class="fa fa-bars" aria-hidden="true"></i> Rack Elevations</a></li>
                             {% if perms.dcim.add_rack %}
                                 <li><a href="{% url 'dcim:rack_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack</a></li>
                                 <li><a href="{% url 'dcim:rack_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Racks</a></li>

+ 50 - 0
netbox/templates/dcim/rack_elevation_list.html

@@ -0,0 +1,50 @@
+{% extends '_base.html' %}
+{% load helpers %}
+
+{% block content %}
+<div class="btn-group pull-right" role="group">
+    <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face=0 %}" class="btn btn-default{% if request.GET.face != '1' %} active{% endif %}">Front</a>
+    <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face=1 %}" class="btn btn-default{% if request.GET.face == '1' %} active{% endif %}">Rear</a>
+</div>
+<h1>{% block title %}Rack Elevations{% endblock %}</h1>
+<div class="row">
+    {% if page %}
+        <div class="col-md-9">
+            <div style="white-space: nowrap; overflow-x: scroll;">
+            {% for rack in page %}
+                <div style="display: inline-block; width: 266px">
+                    <div class="rack_header">
+                        <h4>{{ rack.name }}</h4>
+                    </div>
+                    {% if face_id %}
+                        {% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 %}
+                    {% else %}
+                        {% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 %}
+                    {% endif %}
+                    <div class="clearfix"></div>
+                    <div class="rack_header">
+                        <h4>{{ rack.name }}</h4>
+                    </div>
+                </div>
+            {% endfor %}
+            </div>
+            {% include 'paginator.html' %}
+        </div>
+    {% else %}
+        <div class="col-md-9">
+            <p>No racks found</p>
+        </div>
+    {% endif %}
+    <div class="col-md-3">
+		{% include 'inc/search_panel.html' %}
+    </div>
+</div>
+{% endblock %}
+
+{% block javascript %}
+<script type="text/javascript">
+$(function() {
+  $('[data-toggle="popover"]').popover()
+})
+</script>
+{% endblock %}

+ 4 - 9
netbox/templates/ipam/prefix_list.html

@@ -6,15 +6,10 @@
 
 {% block content %}
 <div class="pull-right">
-    <a href="{% url 'ipam:prefix_list' %}{% querystring_toggle request expand='on' %}" class="btn btn-default">
-        {% if 'expand' in request.GET %}
-            <span class="fa fa-chevron-right" aria-hidden="true"></span>
-            Collapse all
-        {% else %}
-            <span class="fa fa-chevron-down" aria-hidden="true"></span>
-            Expand all
-        {% endif %}
-    </a>
+    <div class="btn-group" role="group">
+        <a href="{% url 'ipam:prefix_list' %}{% querystring request expand=None page=1 %}" class="btn btn-default{% if not request.GET.expand %} active{% endif %}">Collapse</a>
+        <a href="{% url 'ipam:prefix_list' %}{% querystring request expand='on' page=1 %}" class="btn btn-default{% if request.GET.expand %} active{% endif %}">Expand</a>
+    </div>
     {% if perms.ipam.add_prefix %}
 		<a href="{% url 'ipam:prefix_add' %}" class="btn btn-primary">
 			<span class="fa fa-plus" aria-hidden="true"></span>

+ 10 - 17
netbox/templates/paginator.html

@@ -1,34 +1,27 @@
-{% load django_tables2 %}
+{% load helpers %}
 
-{# Custom pagination controls to render nicely with Bootstrap CSS. smart_pages requires EnhancedPaginator. #}
-
-<div class="paginator pull-right">
-    {% if table.paginator.num_pages > 1 %}
+<div class="paginator pull-right" style="margin-top: 20px">
+    {% if paginator.num_pages > 1 %}
         <nav>
             <ul class="pagination pull-right">
-                {% if table.page.has_previous %}
-                    <li><a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}">&laquo;</a></li>
+                {% if page.has_previous %}
+                    <li><a href="{% querystring request page=page.previous_page_number %}">&laquo;</a></li>
                 {% endif %}
-                {% for p in table.page.smart_pages %}
+                {% for p in page.smart_pages %}
                     {% if p %}
-                        <li{% ifequal table.page.number p %} class="active"{% endifequal %}><a href="{% querystring table.prefixed_page_field=p %}">{{ p }}</a></li>
+                        <li{% ifequal page.number p %} class="active"{% endifequal %}><a href="{% querystring request page=p %}">{{ p }}</a></li>
                     {% else %}
                         <li class="disabled"><span>&hellip;</span></li>
                     {% endif %}
                 {% endfor %}
-                {% if table.page.has_next %}
-                    <li><a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}">&raquo;</a></li>
+                {% if page.has_next %}
+                    <li><a href="{% querystring request page=page.next_page_number %}">&raquo;</a></li>
                 {% endif %}
             </ul>
         </nav>
     {% endif %}
     <div class="clearfix"></div>
     <div class="text-right text-muted">
-        Showing {{ table.page.start_index }}-{{ table.page.end_index }} of {{ total }}
-        {% if total == 1 %}
-            {{ table.data.verbose_name }}
-        {% else %}
-            {{ table.data.verbose_name_plural }}
-        {% endif %}
+        Showing {{ page.start_index }}-{{ page.end_index }} of {{ total_count }}
     </div>
 </div>

+ 1 - 1
netbox/templates/panel_table.html

@@ -21,6 +21,6 @@
 
 {% block pagination %}
     {% if not hide_paginator %}
-        {% include 'paginator.html' %}
+        {% include 'table_paginator.html' %}
     {% endif %}
 {% endblock pagination %}

+ 1 - 1
netbox/templates/table.html

@@ -5,6 +5,6 @@
 
 {% block pagination %}
     {% if not hide_paginator %}
-        {% include 'paginator.html' %}
+        {% include 'table_paginator.html' %}
     {% endif %}
 {% endblock pagination %}

+ 34 - 0
netbox/templates/table_paginator.html

@@ -0,0 +1,34 @@
+{% load django_tables2 %}
+
+{# Custom pagination controls to render nicely with Bootstrap CSS. smart_pages requires EnhancedPaginator. #}
+
+<div class="paginator pull-right">
+    {% if table.paginator.num_pages > 1 %}
+        <nav>
+            <ul class="pagination pull-right">
+                {% if table.page.has_previous %}
+                    <li><a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}">&laquo;</a></li>
+                {% endif %}
+                {% for p in table.page.smart_pages %}
+                    {% if p %}
+                        <li{% ifequal table.page.number p %} class="active"{% endifequal %}><a href="{% querystring table.prefixed_page_field=p %}">{{ p }}</a></li>
+                    {% else %}
+                        <li class="disabled"><span>&hellip;</span></li>
+                    {% endif %}
+                {% endfor %}
+                {% if table.page.has_next %}
+                    <li><a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}">&raquo;</a></li>
+                {% endif %}
+            </ul>
+        </nav>
+    {% endif %}
+    <div class="clearfix"></div>
+    <div class="text-right text-muted">
+        Showing {{ table.page.start_index }}-{{ table.page.end_index }} of {{ total }}
+        {% if total == 1 %}
+            {{ table.data.verbose_name }}
+        {% else %}
+            {{ table.data.verbose_name_plural }}
+        {% endif %}
+    </div>
+</div>

+ 8 - 21
netbox/utilities/templatetags/helpers.py

@@ -65,30 +65,17 @@ def bettertitle(value):
 #
 
 @register.simple_tag()
-def querystring_toggle(request, multi=True, page_key='page', **kwargs):
+def querystring(request, **kwargs):
     """
-    Add or remove a parameter in the HTTP GET query string
+    Append or update the page number in a querystring.
     """
-    new_querydict = request.GET.copy()
-
-    # Remove page number from querystring
-    try:
-        new_querydict.pop(page_key)
-    except KeyError:
-        pass
-
-    # Add/toggle parameters
+    querydict = request.GET.copy()
     for k, v in kwargs.items():
-        values = new_querydict.getlist(k)
-        if k in new_querydict and v in values:
-            values.remove(v)
-            new_querydict.setlist(k, values)
-        elif not multi:
-            new_querydict[k] = v
-        else:
-            new_querydict.update({k: v})
-
-    querystring = new_querydict.urlencode()
+        if v is not None:
+            querydict[k] = v
+        elif k in querydict:
+            querydict.pop(k)
+    querystring = querydict.urlencode()
     if querystring:
         return '?' + querystring
     else: