diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index afb36f464..193d7e74a 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -27,6 +27,12 @@ If you followed the original installation guide to set up gunicorn, be sure to c # cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py ``` +Copy the LDAP configuration if using LDAP: + +```no-highlight +# cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ldap_config.py +``` + ## Option B: Clone the Git Repository (latest master release) This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch: diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index c85fad1a1..de5ef1a22 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -224,9 +224,9 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView): fields_initial = ['term_side'] template_name = 'circuits/circuittermination_edit.html' - def alter_obj(self, obj, args, kwargs): - if 'circuit' in kwargs: - obj.circuit = get_object_or_404(Circuit, pk=kwargs['circuit']) + def alter_obj(self, obj, request, url_args, url_kwargs): + if 'circuit' in url_kwargs: + obj.circuit = get_object_or_404(Circuit, pk=url_kwargs['circuit']) return obj def get_return_url(self, obj): diff --git a/netbox/dcim/admin.py b/netbox/dcim/admin.py index 8828b52c4..fb4c281ac 100644 --- a/netbox/dcim/admin.py +++ b/netbox/dcim/admin.py @@ -4,7 +4,7 @@ from django.db.models import Count from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform, - PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, Site, + PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Site, ) @@ -37,6 +37,11 @@ class RackAdmin(admin.ModelAdmin): list_display = ['name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height'] +@admin.register(RackReservation) +class RackRackReservationAdmin(admin.ModelAdmin): + list_display = ['rack', 'units', 'description', 'user', 'created'] + + # # Device types # diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index b4f079281..5390b6078 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -5,8 +5,8 @@ from dcim.models import ( CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, - PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Site, - STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES, + PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES, + RACK_WIDTH_CHOICES, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES, ) from extras.api.serializers import CustomFieldModelSerializer from tenancy.api.serializers import NestedTenantSerializer @@ -97,14 +97,13 @@ class NestedRackRoleSerializer(serializers.ModelSerializer): # Racks # - class RackSerializer(CustomFieldModelSerializer): site = NestedSiteSerializer() group = NestedRackGroupSerializer() tenant = NestedTenantSerializer() role = NestedRackRoleSerializer() type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES) - width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES) + # width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES) class Meta: model = Rack @@ -132,6 +131,18 @@ class WritableRackSerializer(serializers.ModelSerializer): ] +# +# Rack reservations +# + +class RackReservationSerializer(serializers.ModelSerializer): + rack = NestedRackSerializer() + + class Meta: + model = RackReservation + fields = ['id', 'rack', 'units', 'created', 'user', 'description'] + + # # Manufacturers # diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index 1d8eca592..d4afdaadc 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -16,6 +16,7 @@ router.register(r'sites', views.SiteViewSet) router.register(r'rack-groups', views.RackGroupViewSet) router.register(r'rack-roles', views.RackRoleViewSet) router.register(r'racks', views.RackViewSet) +router.register(r'rack-reservations', views.RackReservationViewSet) # Device types router.register(r'manufacturers', views.ManufacturerViewSet) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index e44c29571..e87f11255 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -12,7 +12,8 @@ from django.shortcuts import get_object_or_404 from dcim.models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, - Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, Site, + Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, + RackRole, Site, ) from dcim import filters from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer @@ -97,6 +98,16 @@ class RackViewSet(WritableSerializerMixin, CustomFieldModelViewSet): return Response(elevation) +# +# Rack reservations +# + +class RackReservationViewSet(ModelViewSet): + queryset = RackReservation.objects.all() + serializer_class = serializers.RackReservationSerializer + filter_class = filters.RackReservationFilter + + # # Manufacturers # diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 1bfdcec80..01bce82e4 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -9,7 +9,8 @@ from utilities.filters import NullableModelMultipleChoiceFilter from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, - Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, Site, + Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, + RackRole, Site, ) @@ -123,6 +124,18 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): ) +class RackReservationFilter(django_filters.FilterSet): + rack_id = django_filters.ModelMultipleChoiceFilter( + name='rack', + queryset=Rack.objects.all(), + label='Rack (ID)', + ) + + class Meta: + model = RackReservation + fields = ['rack', 'user'] + + class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet): q = django_filters.MethodFilter( action='search', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 9f6c7bde6..64e8b57fa 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1,6 +1,7 @@ import re from django import forms +from django.contrib.postgres.forms.array import SimpleArrayField from django.core.exceptions import ValidationError from django.db.models import Count, Q @@ -8,9 +9,9 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi from ipam.models import IPAddress from tenancy.models import Tenant from utilities.forms import ( - APISelect, add_blank_choice, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, CSVDataField, - ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, - SlugField, + APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, + CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, + SmallTextarea, SlugField, ) from .formfields import MACAddressFormField @@ -19,7 +20,7 @@ from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, - RACK_WIDTH_CHOICES, Rack, RackGroup, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD + RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD ) @@ -243,6 +244,34 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=(0, 'None')) +# +# Rack reservations +# + +class RackReservationForm(BootstrapMixin, forms.ModelForm): + units = SimpleArrayField(forms.IntegerField(), widget=ArrayFieldSelectMultiple(attrs={'size': 10})) + + class Meta: + model = RackReservation + fields = ['units', 'description'] + + def __init__(self, *args, **kwargs): + + super(RackReservationForm, self).__init__(*args, **kwargs) + + # Populate rack unit choices + self.fields['units'].widget.choices = self._get_unit_choices() + + def _get_unit_choices(self): + rack = self.instance.rack + reserved_units = [] + for resv in rack.reservations.exclude(pk=self.instance.pk): + for u in resv.units: + reserved_units.append(u) + unit_choices = [(u, {'label': str(u), 'disabled': u in reserved_units}) for u in rack.units] + return unit_choices + + # # Manufacturers # diff --git a/netbox/dcim/migrations/0026_add_rack_reservations.py b/netbox/dcim/migrations/0026_add_rack_reservations.py new file mode 100644 index 000000000..b9d4f8214 --- /dev/null +++ b/netbox/dcim/migrations/0026_add_rack_reservations.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2017-02-16 18:43 +from __future__ import unicode_literals + +from django.conf import settings +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('dcim', '0025_devicetype_add_interface_ordering'), + ] + + operations = [ + migrations.CreateModel( + name='RackReservation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('units', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(), size=None)), + ('created', models.DateTimeField(auto_now_add=True)), + ('description', models.CharField(max_length=100)), + ('rack', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='dcim.Rack')), + ('user', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['created'], + }, + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index d29ca745d..1225988ca 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1,8 +1,10 @@ from collections import OrderedDict from django.conf import settings +from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericRelation +from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.core.validators import MaxValueValidator, MinValueValidator @@ -478,6 +480,50 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): return int(float(self.u_height - u_available) / self.u_height * 100) +@python_2_unicode_compatible +class RackReservation(models.Model): + """ + One or more reserved units within a Rack. + """ + rack = models.ForeignKey('Rack', related_name='reservations', editable=False, on_delete=models.CASCADE) + units = ArrayField(models.PositiveSmallIntegerField()) + created = models.DateTimeField(auto_now_add=True) + user = models.ForeignKey(User, editable=False, on_delete=models.PROTECT) + description = models.CharField(max_length=100) + + class Meta: + ordering = ['created'] + + def __str__(self): + return u"Reservation for rack {}".format(self.rack) + + def clean(self): + + if self.units: + + # Validate that all specified units exist in the Rack. + invalid_units = [u for u in self.units if u not in self.rack.units] + if invalid_units: + raise ValidationError({ + 'units': u"Invalid unit(s) for {}U rack: {}".format( + self.rack.u_height, + ', '.join([str(u) for u in invalid_units]), + ), + }) + + # Check that none of the units has already been reserved for this Rack. + reserved_units = [] + for resv in self.rack.reservations.exclude(pk=self.pk): + reserved_units += resv.units + conflicting_units = [u for u in self.units if u in reserved_units] + if conflicting_units: + raise ValidationError({ + 'units': 'The following units have already been reserved: {}'.format( + ', '.join([str(u) for u in conflicting_units]), + ) + }) + + # # Device Types # diff --git a/netbox/dcim/tests/test_apis.py b/netbox/dcim/tests/test_apis.py index 7545a80ba..265c5ddef 100644 --- a/netbox/dcim/tests/test_apis.py +++ b/netbox/dcim/tests/test_apis.py @@ -136,6 +136,7 @@ class RackTest(APITestCase): 'width', 'u_height', 'desc_units', + 'reservations', 'comments', 'custom_fields', 'front_units', diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 58bc91802..1b337ad6e 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -29,6 +29,10 @@ urlpatterns = [ url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'), url(r'^rack-roles/(?P\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'), + # Rack reservations + 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'), + # Racks url(r'^racks/$', views.RackListView.as_view(), name='rack_list'), url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'), @@ -38,6 +42,7 @@ urlpatterns = [ url(r'^racks/(?P\d+)/$', views.rack, name='rack'), url(r'^racks/(?P\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'), url(r'^racks/(?P\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'), + url(r'^racks/(?P\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'), # Manufacturers url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 17f74eae3..4bec35be9 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -26,7 +26,7 @@ from .models import ( CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, - RackRole, Site, + RackReservation, RackRole, Site, ) @@ -269,8 +269,16 @@ def rack(request, pk): next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first() prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first() + reservations = RackReservation.objects.filter(rack=rack) + reserved_units = {} + for r in reservations: + for u in r.units: + reserved_units[u] = r + return render(request, 'dcim/rack.html', { 'rack': rack, + 'reservations': reservations, + 'reserved_units': reserved_units, 'nonracked_devices': nonracked_devices, 'next_rack': next_rack, 'prev_rack': prev_rack, @@ -317,6 +325,33 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): default_return_url = 'dcim:rack_list' +# +# Rack reservations +# + +class RackReservationEditView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.change_rackreservation' + model = RackReservation + form_class = forms.RackReservationForm + + def alter_obj(self, obj, request, args, kwargs): + if not obj.pk: + obj.rack = get_object_or_404(Rack, pk=kwargs['rack']) + obj.user = request.user + return obj + + def get_return_url(self, obj): + return obj.rack.get_absolute_url() + + +class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView): + permission_required = 'dcim.delete_rackreservation' + model = RackReservation + + def get_return_url(self, obj): + return obj.rack.get_absolute_url() + + # # Manufacturers # @@ -1517,9 +1552,9 @@ class ModuleEditView(PermissionRequiredMixin, ComponentEditView): model = Module form_class = forms.ModuleForm - def alter_obj(self, obj, args, kwargs): - if 'device' in kwargs: - obj.device = get_object_or_404(Device, pk=kwargs['device']) + def alter_obj(self, obj, request, url_args, url_kwargs): + if 'device' in url_kwargs: + obj.device = get_object_or_404(Device, pk=url_kwargs['device']) return obj diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index f93f297e0..b53bb82ab 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -764,9 +764,9 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView): form_class = forms.ServiceForm template_name = 'ipam/service_edit.html' - def alter_obj(self, obj, args, kwargs): - if 'device' in kwargs: - obj.device = get_object_or_404(Device, pk=kwargs['device']) + def alter_obj(self, obj, request, url_args, url_kwargs): + if 'device' in url_kwargs: + obj.device = get_object_or_404(Device, pk=url_kwargs['device']) return obj def get_return_url(self, obj): diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index 4f569edea..11ea04b72 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -264,6 +264,15 @@ ul.rack_far_face li.blocked { #ffc7c7 14px ); } +ul.rack_near_face li.reserved { + background: repeating-linear-gradient( + 45deg, + #f7f7f7, + #f7f7f7 7px, + #c7c7ff 7px, + #c7c7ff 14px + ); +} ul.rack_near_face { z-index: 200; } diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 3a5ad2b83..5a736627e 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -73,6 +73,7 @@ $(document).ready(function() { // Resolve child field by ID specified in parent var child_name = $(this).attr('filter-for'); var child_field = $('#id_' + child_name); + var child_selected = child_field.val(); // Wipe out any existing options within the child field child_field.empty(); @@ -106,7 +107,9 @@ $(document).ready(function() { $.each(response, function (index, choice) { var option = $("").attr("value", choice.id).text(choice[display_field]); if (disabled_indicator && choice[disabled_indicator] && choice.id != initial_value) { - option.attr("disabled", "disabled") + option.attr("disabled", "disabled"); + } else if (choice.id == child_selected) { + option.attr("selected", "selected"); } child_field.append(option); }); diff --git a/netbox/templates/dcim/inc/rack_elevation.html b/netbox/templates/dcim/inc/rack_elevation.html index 0ffc6b7ad..049dcbc61 100644 --- a/netbox/templates/dcim/inc/rack_elevation.html +++ b/netbox/templates/dcim/inc/rack_elevation.html @@ -1,3 +1,5 @@ +{% load helpers %} +
    {% for u in rack.units %}
  • {{ u }}
  • @@ -35,9 +37,14 @@ {% endifequal %} {% else %} -
  • +
  • {% if perms.dcim.add_device %} - add device + add device {% endif %}
  • {% endif %} diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index d73a0b560..37cddf213 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -189,6 +189,51 @@ {% endif %} +
    +
    + Reservations +
    + {% if reservations %} + + + + + + + {% for resv in reservations %} + + + + + + {% endfor %} +
    UnitsDescription
    {{ resv.units|join:', ' }} + {{ resv.description }}
    + {{ resv.user }} · {{ resv.created }} +
    + {% if perms.change_rackreservation %} + + + + {% endif %} + {% if perms.delete_rackreservation %} + + + + {% endif %} +
    + {% else %} +
    None
    + {% endif %} + {% if perms.dcim.add_rackreservation %} + + {% endif %} +
    diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index f6e0b36b1..6eb11c208 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -169,6 +169,27 @@ class SelectWithDisabled(forms.Select): force_text(option_label)) +class ArrayFieldSelectMultiple(SelectWithDisabled, forms.SelectMultiple): + """ + MultiSelect widgets for a SimpleArrayField. Choices must be populated on the widget. + """ + + def __init__(self, *args, **kwargs): + self.delimiter = kwargs.pop('delimiter', ',') + super(ArrayFieldSelectMultiple, self).__init__(*args, **kwargs) + + def render_options(self, selected_choices): + # Split the delimited string of values into a list + if selected_choices: + selected_choices = selected_choices.split(self.delimiter) + return super(ArrayFieldSelectMultiple, self).render_options(selected_choices) + + def value_from_datadict(self, data, files, name): + # Condense the list of selected choices into a delimited string + data = super(ArrayFieldSelectMultiple, self).value_from_datadict(data, files, name) + return self.delimiter.join(data) + + class APISelect(SelectWithDisabled): """ A select widget populated via an API call diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 74d490390..164aa24b2 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -27,6 +27,14 @@ def getlist(value, arg): return value.getlist(arg) +@register.filter +def getkey(value, key): + """ + Return a dictionary item specified by key + """ + return value[key] + + @register.filter(is_safe=True) def gfm(value): """ diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 315298383..9d1561a48 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -145,9 +145,9 @@ class ObjectEditView(View): return get_object_or_404(self.model, pk=kwargs['pk']) return self.model() - def alter_obj(self, obj, args, kwargs): + def alter_obj(self, obj, request, url_args, url_kwargs): # Allow views to add extra info to an object before it is processed. For example, a parent object can be defined - # given some parameter from the request URI. + # given some parameter from the request URL. return obj def get_return_url(self, obj): @@ -159,7 +159,7 @@ class ObjectEditView(View): def get(self, request, *args, **kwargs): obj = self.get_object(kwargs) - obj = self.alter_obj(obj, args, kwargs) + obj = self.alter_obj(obj, request, args, kwargs) initial_data = {k: request.GET[k] for k in self.fields_initial if k in request.GET} form = self.form_class(instance=obj, initial=initial_data) @@ -173,7 +173,7 @@ class ObjectEditView(View): def post(self, request, *args, **kwargs): obj = self.get_object(kwargs) - obj = self.alter_obj(obj, args, kwargs) + obj = self.alter_obj(obj, request, args, kwargs) form = self.form_class(request.POST, instance=obj) if form.is_valid():