mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
Closes #951: Provide a side-by-side view of rack elevations
This commit is contained in:
parent
864fa17b75
commit
13cc29cd8c
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -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'),
|
||||
|
@ -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)
|
||||
|
@ -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
netbox/templates/dcim/rack_elevation_list.html
Normal file
50
netbox/templates/dcim/rack_elevation_list.html
Normal 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 %}
|
@ -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>
|
||||
|
@ -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 %}">«</a></li>
|
||||
{% if page.has_previous %}
|
||||
<li><a href="{% querystring request page=page.previous_page_number %}">«</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>…</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if table.page.has_next %}
|
||||
<li><a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}">»</a></li>
|
||||
{% if page.has_next %}
|
||||
<li><a href="{% querystring request page=page.next_page_number %}">»</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>
|
||||
|
@ -21,6 +21,6 @@
|
||||
|
||||
{% block pagination %}
|
||||
{% if not hide_paginator %}
|
||||
{% include 'paginator.html' %}
|
||||
{% include 'table_paginator.html' %}
|
||||
{% endif %}
|
||||
{% endblock pagination %}
|
||||
|
@ -5,6 +5,6 @@
|
||||
|
||||
{% block pagination %}
|
||||
{% if not hide_paginator %}
|
||||
{% include 'paginator.html' %}
|
||||
{% include 'table_paginator.html' %}
|
||||
{% endif %}
|
||||
{% endblock pagination %}
|
||||
|
34
netbox/templates/table_paginator.html
Normal file
34
netbox/templates/table_paginator.html
Normal 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 %}">«</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>…</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if table.page.has_next %}
|
||||
<li><a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}">»</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>
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user