From 2c99a8bee4804433605191372cb8136e4afb46d6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 6 Apr 2017 16:26:48 -0400 Subject: [PATCH] Closes #1052: Added rack reservation list and bulk delete views --- netbox/dcim/filters.py | 37 +++++++++++++++++++ netbox/dcim/forms.py | 13 +++++++ netbox/dcim/models.py | 10 +++++ netbox/dcim/tables.py | 25 ++++++++++++- netbox/dcim/urls.py | 2 + netbox/dcim/views.py | 14 +++++++ netbox/templates/_base.html | 2 + netbox/templates/dcim/rack.html | 2 +- .../templates/dcim/rackreservation_list.html | 14 +++++++ 9 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 netbox/templates/dcim/rackreservation_list.html diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index bf390e17b..d08b1800a 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -147,6 +147,33 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): class RackReservationFilter(django_filters.FilterSet): + id__in = NumericInFilter(name='id', lookup_expr='in') + q = django_filters.CharFilter( + method='search', + label='Search', + ) + site_id = django_filters.ModelMultipleChoiceFilter( + name='rack__site', + queryset=Site.objects.all(), + label='Site (ID)', + ) + site = django_filters.ModelMultipleChoiceFilter( + name='rack__site__slug', + queryset=Site.objects.all(), + to_field_name='slug', + label='Site (slug)', + ) + group_id = NullableModelMultipleChoiceFilter( + name='rack__group', + queryset=RackGroup.objects.all(), + label='Group (ID)', + ) + group = NullableModelMultipleChoiceFilter( + name='rack__group', + queryset=RackGroup.objects.all(), + to_field_name='slug', + label='Group', + ) rack_id = django_filters.ModelMultipleChoiceFilter( name='rack', queryset=Rack.objects.all(), @@ -157,6 +184,16 @@ class RackReservationFilter(django_filters.FilterSet): model = RackReservation fields = ['rack', 'user'] + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(rack__name__icontains=value) | + Q(rack__facility_id__icontains=value) | + Q(user__username__icontains=value) | + Q(description__icontains=value) + ) + class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet): id__in = NumericInFilter(name='id', lookup_expr='in') diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index e2cd829b8..32b8850f6 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -330,6 +330,19 @@ class RackReservationForm(BootstrapMixin, forms.ModelForm): return unit_choices +class RackReservationFilterForm(BootstrapMixin, forms.Form): + q = forms.CharField(required=False, label='Search') + site = FilterChoiceField( + queryset=Site.objects.annotate(filter_count=Count('racks__reservations')), + to_field_name='slug' + ) + group_id = FilterChoiceField( + queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__reservations')), + label='Rack group', + null_option=(0, 'None') + ) + + # # Manufacturers # diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index dd9a37a57..901dc37ed 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1,4 +1,5 @@ from collections import OrderedDict +from itertools import count, groupby from mptt.models import MPTTModel, TreeForeignKey @@ -571,6 +572,15 @@ class RackReservation(models.Model): ) }) + @property + def unit_list(self): + """ + Express the assigned units as a string of summarized ranges. For example: + [0, 1, 2, 10, 14, 15, 16] => "0-2, 10, 14-16" + """ + group = (list(x) for _, x in groupby(sorted(self.units), lambda x, c=count(): next(c) - x)) + return ', '.join('-'.join(map(str, (g[0], g[-1])[:len(g)])) for g in group) + # # Device Types diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 6af772fde..c56a20744 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -6,7 +6,7 @@ from utilities.tables import BaseTable, ToggleColumn from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, - RackGroup, Region, Site, + RackGroup, RackReservation, Region, Site, ) @@ -64,6 +64,12 @@ RACK_ROLE = """ {% endif %} """ +RACKRESERVATION_ACTIONS = """ +{% if perms.dcim.change_rackreservation %} + +{% endif %} +""" + DEVICEROLE_ACTIONS = """ {% if perms.dcim.change_devicerole %} @@ -226,6 +232,23 @@ class RackImportTable(BaseTable): fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height') +# +# Rack reservations +# + +class RackReservationTable(BaseTable): + pk = ToggleColumn() + rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) + unit_list = tables.Column(orderable=False, verbose_name='Units') + actions = tables.TemplateColumn( + template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='' + ) + + class Meta(BaseTable.Meta): + model = RackReservation + fields = ('pk', 'rack', 'unit_list', 'user', 'created', 'description', 'actions') + + # # Manufacturers # diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 7fde6e9b3..c52807aa3 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -36,6 +36,8 @@ urlpatterns = [ url(r'^rack-roles/(?P\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'), # Rack reservations + url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'), + url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'), url(r'^rack-reservations/(?P\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'), url(r'^rack-reservations/(?P\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index f2b042599..8962d0219 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -360,6 +360,14 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Rack reservations # +class RackReservationListView(ObjectListView): + queryset = RackReservation.objects.all() + filter = filters.RackReservationFilter + filter_form = forms.RackReservationFilterForm + table = tables.RackReservationTable + template_name = 'dcim/rackreservation_list.html' + + class RackReservationEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'dcim.change_rackreservation' model = RackReservation @@ -383,6 +391,12 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView): return obj.rack.get_absolute_url() +class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'dcim.delete_rackreservation' + cls = RackReservation + default_return_url = 'dcim:rackreservation_list' + + # # Manufacturers # diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 4e5846e14..800739b74 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -72,6 +72,8 @@ {% if perms.dcim.add_rackrole %}
  • Add a Rack Role
  • {% endif %} +
  • +
  • Rack Reservations