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

This commit is contained in:
Jeremy Stretch 2017-04-20 13:07:22 -04:00
parent 864fa17b75
commit 13cc29cd8c
11 changed files with 156 additions and 54 deletions

View File

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

View File

@ -47,6 +47,7 @@ urlpatterns = [
# Racks # Racks
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'), 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/add/$', views.RackEditView.as_view(), name='rack_add'),
url(r'^racks/import/$', views.RackBulkImportView.as_view(), name='rack_import'), url(r'^racks/import/$', views.RackBulkImportView.as_view(), name='rack_import'),
url(r'^racks/edit/$', views.RackBulkEditView.as_view(), name='rack_bulk_edit'), url(r'^racks/edit/$', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),

View File

@ -6,6 +6,7 @@ from operator import attrgetter
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.paginator import EmptyPage, PageNotAnInteger
from django.db.models import Count from django.db.models import Count
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render 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 circuits.models import Circuit
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator
from utilities.views import ( from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
@ -291,6 +293,46 @@ class RackListView(ObjectListView):
template_name = 'dcim/rack_list.html' 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): def rack(request, pk):
rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk) rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)

View File

@ -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> <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"> <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_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 %} {% 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_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> <li><a href="{% url 'dcim:rack_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Racks</a></li>

View File

@ -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 %}

View File

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

View File

@ -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" style="margin-top: 20px">
{% if paginator.num_pages > 1 %}
<div class="paginator pull-right">
{% if table.paginator.num_pages > 1 %}
<nav> <nav>
<ul class="pagination pull-right"> <ul class="pagination pull-right">
{% if table.page.has_previous %} {% if page.has_previous %}
<li><a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}">&laquo;</a></li> <li><a href="{% querystring request page=page.previous_page_number %}">&laquo;</a></li>
{% endif %} {% endif %}
{% for p in table.page.smart_pages %} {% for p in page.smart_pages %}
{% if p %} {% 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 %} {% else %}
<li class="disabled"><span>&hellip;</span></li> <li class="disabled"><span>&hellip;</span></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if table.page.has_next %} {% if page.has_next %}
<li><a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}">&raquo;</a></li> <li><a href="{% querystring request page=page.next_page_number %}">&raquo;</a></li>
{% endif %} {% endif %}
</ul> </ul>
</nav> </nav>
{% endif %} {% endif %}
<div class="clearfix"></div> <div class="clearfix"></div>
<div class="text-right text-muted"> <div class="text-right text-muted">
Showing {{ table.page.start_index }}-{{ table.page.end_index }} of {{ total }} Showing {{ page.start_index }}-{{ page.end_index }} of {{ total_count }}
{% if total == 1 %}
{{ table.data.verbose_name }}
{% else %}
{{ table.data.verbose_name_plural }}
{% endif %}
</div> </div>
</div> </div>

View File

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

View File

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

View File

@ -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>

View File

@ -65,30 +65,17 @@ def bettertitle(value):
# #
@register.simple_tag() @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() querydict = request.GET.copy()
# Remove page number from querystring
try:
new_querydict.pop(page_key)
except KeyError:
pass
# Add/toggle parameters
for k, v in kwargs.items(): for k, v in kwargs.items():
values = new_querydict.getlist(k) if v is not None:
if k in new_querydict and v in values: querydict[k] = v
values.remove(v) elif k in querydict:
new_querydict.setlist(k, values) querydict.pop(k)
elif not multi: querystring = querydict.urlencode()
new_querydict[k] = v
else:
new_querydict.update({k: v})
querystring = new_querydict.urlencode()
if querystring: if querystring:
return '?' + querystring return '?' + querystring
else: else: