diff --git a/.gitignore b/.gitignore index 4fc377333..2f957c678 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ *.pyc /netbox/netbox/configuration.py +/netbox/netbox/ldap_config.py /netbox/static .idea /*.sh !upgrade.sh fabfile.py *.swp +gunicorn_config.py diff --git a/.travis.yml b/.travis.yml index e8822e5e3..b23c9d8fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,9 @@ env: language: python python: - "2.7" + - "3.4" + - "3.5" + - "3.6" install: - pip install -r requirements.txt - pip install pep8 diff --git a/docs/installation/netbox.md b/docs/installation/netbox.md index fa476b865..e47e92133 100644 --- a/docs/installation/netbox.md +++ b/docs/installation/netbox.md @@ -2,12 +2,29 @@ **Debian/Ubuntu** +Python 3: + +```no-highlight +# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev +``` + +Python 2: + ```no-highlight # apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev ``` **CentOS/RHEL** +Python 3: + +```no-highlight +# yum install -y epel-release +# yum install -y gcc python3 python3-devel python3-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel +``` + +Python 2: + ```no-highlight # yum install -y epel-release # yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel diff --git a/netbox/circuits/__init__.py b/netbox/circuits/__init__.py index e69de29bb..e5400337f 100644 --- a/netbox/circuits/__init__.py +++ b/netbox/circuits/__init__.py @@ -0,0 +1 @@ +default_app_config = 'circuits.apps.CircuitsConfig' diff --git a/netbox/circuits/apps.py b/netbox/circuits/apps.py new file mode 100644 index 000000000..bc0b7d87d --- /dev/null +++ b/netbox/circuits/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class CircuitsConfig(AppConfig): + name = "circuits" + verbose_name = "Circuits" + + def ready(self): + import circuits.signals diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index a1777bb16..4b9e949f8 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -62,6 +62,7 @@ class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Provider + q = forms.CharField(required=False, label='Search') site = FilterChoiceField(queryset=Site.objects.all(), to_field_name='slug') @@ -126,6 +127,7 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Circuit + q = forms.CharField(required=False, label='Search') type = FilterChoiceField(queryset=CircuitType.objects.annotate(filter_count=Count('circuits')), to_field_name='slug') provider = FilterChoiceField(queryset=Provider.objects.annotate(filter_count=Count('circuits')), diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 7f6cc4f21..6a0380dd5 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -1,6 +1,7 @@ from django.contrib.contenttypes.fields import GenericRelation from django.core.urlresolvers import reverse from django.db import models +from django.utils.encoding import python_2_unicode_compatible from dcim.fields import ASNField from extras.models import CustomFieldModel, CustomFieldValue @@ -33,6 +34,7 @@ def humanize_speed(speed): return '{} Kbps'.format(speed) +@python_2_unicode_compatible class Provider(CreatedUpdatedModel, CustomFieldModel): """ Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model @@ -51,7 +53,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): @@ -67,6 +69,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel): ]) +@python_2_unicode_compatible class CircuitType(models.Model): """ Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named @@ -78,13 +81,14 @@ class CircuitType(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug) +@python_2_unicode_compatible class Circuit(CreatedUpdatedModel, CustomFieldModel): """ A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple @@ -105,7 +109,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): ordering = ['provider', 'cid'] unique_together = ['provider', 'cid'] - def __unicode__(self): + def __str__(self): return u'{} {}'.format(self.provider, self.cid) def get_absolute_url(self): @@ -141,6 +145,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): commit_rate_human.admin_order_field = 'commit_rate' +@python_2_unicode_compatible class CircuitTermination(models.Model): circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE) term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination') @@ -156,7 +161,7 @@ class CircuitTermination(models.Model): ordering = ['circuit', 'term_side'] unique_together = ['circuit', 'term_side'] - def __unicode__(self): + def __str__(self): return u'{} (Side {})'.format(self.circuit, self.get_term_side_display()) def get_peer_termination(self): diff --git a/netbox/circuits/signals.py b/netbox/circuits/signals.py new file mode 100644 index 000000000..bdfe8c0b6 --- /dev/null +++ b/netbox/circuits/signals.py @@ -0,0 +1,13 @@ +from django.db.models.signals import post_delete, post_save +from django.dispatch import receiver +from django.utils import timezone + +from .models import Circuit, CircuitTermination + + +@receiver((post_save, post_delete), sender=CircuitTermination) +def update_circuit(instance, **kwargs): + """ + When a CircuitTermination has been modified, update the last_updated time of its parent Circuit. + """ + Circuit.objects.filter(pk=instance.circuit_id).update(last_updated=timezone.now()) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 3eb269327..c85fad1a1 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -25,7 +25,6 @@ class ProviderListView(ObjectListView): filter = filters.ProviderFilter filter_form = forms.ProviderFilterForm table = tables.ProviderTable - edit_permissions = ['circuits.change_provider', 'circuits.delete_provider'] template_name = 'circuits/provider_list.html' @@ -47,7 +46,7 @@ class ProviderEditView(PermissionRequiredMixin, ObjectEditView): model = Provider form_class = forms.ProviderForm template_name = 'circuits/provider_edit.html' - obj_list_url = 'circuits:provider_list' + default_return_url = 'circuits:provider_list' class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView): @@ -61,21 +60,23 @@ class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView): form = forms.ProviderImportForm table = tables.ProviderTable template_name = 'circuits/provider_import.html' - obj_list_url = 'circuits:provider_list' + default_return_url = 'circuits:provider_list' class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'circuits.change_provider' cls = Provider + filter = filters.ProviderFilter form = forms.ProviderBulkEditForm template_name = 'circuits/provider_bulk_edit.html' - default_redirect_url = 'circuits:provider_list' + default_return_url = 'circuits:provider_list' class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'circuits.delete_provider' cls = Provider - default_redirect_url = 'circuits:provider_list' + filter = filters.ProviderFilter + default_return_url = 'circuits:provider_list' # @@ -85,7 +86,6 @@ class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class CircuitTypeListView(ObjectListView): queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')) table = tables.CircuitTypeTable - edit_permissions = ['circuits.change_circuittype', 'circuits.delete_circuittype'] template_name = 'circuits/circuittype_list.html' @@ -101,7 +101,7 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView): class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'circuits.delete_circuittype' cls = CircuitType - default_redirect_url = 'circuits:circuittype_list' + default_return_url = 'circuits:circuittype_list' # @@ -113,7 +113,6 @@ class CircuitListView(ObjectListView): filter = filters.CircuitFilter filter_form = forms.CircuitFilterForm table = tables.CircuitTable - edit_permissions = ['circuits.change_circuit', 'circuits.delete_circuit'] template_name = 'circuits/circuit_list.html' @@ -136,7 +135,7 @@ class CircuitEditView(PermissionRequiredMixin, ObjectEditView): form_class = forms.CircuitForm fields_initial = ['provider'] template_name = 'circuits/circuit_edit.html' - obj_list_url = 'circuits:circuit_list' + default_return_url = 'circuits:circuit_list' class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView): @@ -150,21 +149,23 @@ class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView): form = forms.CircuitImportForm table = tables.CircuitTable template_name = 'circuits/circuit_import.html' - obj_list_url = 'circuits:circuit_list' + default_return_url = 'circuits:circuit_list' class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'circuits.change_circuit' cls = Circuit + filter = filters.CircuitFilter form = forms.CircuitBulkEditForm template_name = 'circuits/circuit_bulk_edit.html' - default_redirect_url = 'circuits:circuit_list' + default_return_url = 'circuits:circuit_list' class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'circuits.delete_circuit' cls = Circuit - default_redirect_url = 'circuits:circuit_list' + filter = filters.CircuitFilter + default_return_url = 'circuits:circuit_list' @permission_required('circuits.change_circuittermination') @@ -208,7 +209,7 @@ def circuit_terminations_swap(request, pk): 'form': form, 'panel_class': 'default', 'button_class': 'primary', - 'cancel_url': circuit.get_absolute_url(), + 'return_url': circuit.get_absolute_url(), }) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 698e7d2d6..f81f299af 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -134,12 +134,13 @@ class ManufacturerNestedSerializer(ManufacturerSerializer): class DeviceTypeSerializer(CustomFieldSerializer, serializers.ModelSerializer): manufacturer = ManufacturerNestedSerializer() subdevice_role = serializers.SerializerMethodField() + instance_count = serializers.IntegerField(source='instances.count', read_only=True) class Meta: model = DeviceType fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', - 'comments', 'custom_fields'] + 'comments', 'custom_fields', 'instance_count'] def get_subdevice_role(self, obj): return { diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 13e69392f..e76ec82ad 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -331,7 +331,8 @@ class InterfaceListView(generics.ListAPIView): def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) - queryset = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b') + queryset = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\ + .select_related('connected_as_a', 'connected_as_b', 'circuit_termination') # Filter by type (physical or virtual) iface_type = self.request.query_params.get('type') @@ -489,8 +490,8 @@ class RelatedConnectionsView(APIView): response['power-ports'].append(data) # Interface connections - interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b', - 'circuit_termination') + interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\ + .select_related('connected_as_a', 'connected_as_b', 'circuit_termination') for iface in interfaces: data = serializers.InterfaceDetailSerializer(instance=iface).data del(data['device']) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index cb35ff881..9f6c7bde6 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -13,7 +13,7 @@ from utilities.forms import ( SlugField, ) -from formfields import MACAddressFormField +from .formfields import MACAddressFormField from .models import ( DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, @@ -101,6 +101,7 @@ class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Site + q = forms.CharField(required=False, label='Search') tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('sites')), to_field_name='slug', null_option=(0, 'None')) @@ -232,6 +233,7 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Rack + q = forms.CharField(required=False, label='Search') site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks')), to_field_name='slug') group_id = FilterChoiceField(queryset=RackGroup.objects.select_related('site') .annotate(filter_count=Count('racks')), label='Rack group', null_option=(0, 'None')) @@ -281,6 +283,7 @@ class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm): model = DeviceType + q = forms.CharField(required=False, label='Search') manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')), to_field_name='slug') @@ -639,18 +642,46 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Device - site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')), to_field_name='slug') - rack_group_id = FilterChoiceField(queryset=RackGroup.objects.annotate(filter_count=Count('racks__devices')), - label='Rack Group') - role = FilterChoiceField(queryset=DeviceRole.objects.annotate(filter_count=Count('devices')), to_field_name='slug') - tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug', - null_option=(0, 'None')) - device_type_id = FilterChoiceField(queryset=DeviceType.objects.select_related('manufacturer') - .annotate(filter_count=Count('instances')), label='Type') - platform = FilterChoiceField(queryset=Platform.objects.annotate(filter_count=Count('devices')), - to_field_name='slug', null_option=(0, 'None')) - status = forms.NullBooleanField(required=False, widget=forms.Select(choices=FORM_STATUS_CHOICES)) - mac_address = forms.CharField(required=False, label='MAC address') + q = forms.CharField(required=False, label='Search') + site = FilterChoiceField( + queryset=Site.objects.annotate(filter_count=Count('racks__devices')), + to_field_name='slug', + ) + rack_group_id = FilterChoiceField( + queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__devices')), + label='Rack Group', + ) + role = FilterChoiceField( + queryset=DeviceRole.objects.annotate(filter_count=Count('devices')), + to_field_name='slug', + ) + tenant = FilterChoiceField( + queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug', + null_option=(0, 'None'), + ) + manufacturer_id = FilterChoiceField( + queryset=Manufacturer.objects.all(), + label='Manufacturer', + ) + device_type_id = FilterChoiceField( + queryset=DeviceType.objects.select_related('manufacturer').order_by('model').annotate( + filter_count=Count('instances'), + ), + label='Model', + ) + platform = FilterChoiceField( + queryset=Platform.objects.annotate(filter_count=Count('devices')), + to_field_name='slug', + null_option=(0, 'None'), + ) + status = forms.NullBooleanField( + required=False, + widget=forms.Select(choices=FORM_STATUS_CHOICES), + ) + mac_address = forms.CharField( + required=False, + label='MAC address', + ) # diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 6274f3a53..d29ca745d 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -8,6 +8,7 @@ from django.core.urlresolvers import reverse from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import Count, Q, ObjectDoesNotExist +from django.utils.encoding import python_2_unicode_compatible from circuits.models import Circuit from extras.models import CustomFieldModel, CustomField, CustomFieldValue @@ -199,6 +200,7 @@ class SiteManager(NaturalOrderByManager): return self.natural_order_by('name') +@python_2_unicode_compatible class Site(CreatedUpdatedModel, CustomFieldModel): """ A Site represents a geographic location within a network; typically a building or campus. The optional facility @@ -222,7 +224,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): @@ -265,6 +267,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel): # Racks # +@python_2_unicode_compatible class RackGroup(models.Model): """ Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For @@ -282,13 +285,14 @@ class RackGroup(models.Model): ['site', 'slug'], ] - def __unicode__(self): + def __str__(self): return u'{} - {}'.format(self.site.name, self.name) def get_absolute_url(self): return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk) +@python_2_unicode_compatible class RackRole(models.Model): """ Racks can be organized by functional role, similar to Devices. @@ -300,7 +304,7 @@ class RackRole(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): @@ -313,6 +317,7 @@ class RackManager(NaturalOrderByManager): return self.natural_order_by('site__name', 'name') +@python_2_unicode_compatible class Rack(CreatedUpdatedModel, CustomFieldModel): """ Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face. @@ -343,7 +348,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): ['site', 'facility_id'], ] - def __unicode__(self): + def __str__(self): return self.display_name def get_absolute_url(self): @@ -442,7 +447,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): devices = self.devices.select_related('device_type').filter(position__gte=1).exclude(pk__in=exclude) # Initialize the rack unit skeleton - units = range(1, self.u_height + 1) + units = list(range(1, self.u_height + 1)) # Remove units consumed by installed devices for d in devices: @@ -477,6 +482,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): # Device Types # +@python_2_unicode_compatible class Manufacturer(models.Model): """ A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell. @@ -487,13 +493,14 @@ class Manufacturer(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug) +@python_2_unicode_compatible class DeviceType(models.Model, CustomFieldModel): """ A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as @@ -538,7 +545,7 @@ class DeviceType(models.Model, CustomFieldModel): ['manufacturer', 'slug'], ] - def __unicode__(self): + def __str__(self): return self.model def __init__(self, *args, **kwargs): @@ -608,6 +615,7 @@ class DeviceType(models.Model, CustomFieldModel): return bool(self.subdevice_role is False) +@python_2_unicode_compatible class ConsolePortTemplate(models.Model): """ A template for a ConsolePort to be created for a new Device. @@ -619,10 +627,11 @@ class ConsolePortTemplate(models.Model): ordering = ['device_type', 'name'] unique_together = ['device_type', 'name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class ConsoleServerPortTemplate(models.Model): """ A template for a ConsoleServerPort to be created for a new Device. @@ -634,10 +643,11 @@ class ConsoleServerPortTemplate(models.Model): ordering = ['device_type', 'name'] unique_together = ['device_type', 'name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class PowerPortTemplate(models.Model): """ A template for a PowerPort to be created for a new Device. @@ -649,10 +659,11 @@ class PowerPortTemplate(models.Model): ordering = ['device_type', 'name'] unique_together = ['device_type', 'name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class PowerOutletTemplate(models.Model): """ A template for a PowerOutlet to be created for a new Device. @@ -664,7 +675,7 @@ class PowerOutletTemplate(models.Model): ordering = ['device_type', 'name'] unique_together = ['device_type', 'name'] - def __unicode__(self): + def __str__(self): return self.name @@ -706,6 +717,7 @@ class InterfaceManager(models.Manager): }).order_by(*ordering) +@python_2_unicode_compatible class InterfaceTemplate(models.Model): """ A template for a physical data interface on a new Device. @@ -721,10 +733,11 @@ class InterfaceTemplate(models.Model): ordering = ['device_type', 'name'] unique_together = ['device_type', 'name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class DeviceBayTemplate(models.Model): """ A template for a DeviceBay to be created for a new parent Device. @@ -736,7 +749,7 @@ class DeviceBayTemplate(models.Model): ordering = ['device_type', 'name'] unique_together = ['device_type', 'name'] - def __unicode__(self): + def __str__(self): return self.name @@ -744,6 +757,7 @@ class DeviceBayTemplate(models.Model): # Devices # +@python_2_unicode_compatible class DeviceRole(models.Model): """ Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a @@ -756,13 +770,14 @@ class DeviceRole(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): return "{}?role={}".format(reverse('dcim:device_list'), self.slug) +@python_2_unicode_compatible class Platform(models.Model): """ Platform refers to the software or firmware running on a Device; for example, "Cisco IOS-XR" or "Juniper Junos". @@ -776,7 +791,7 @@ class Platform(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): @@ -789,6 +804,7 @@ class DeviceManager(NaturalOrderByManager): return self.natural_order_by('name') +@python_2_unicode_compatible class Device(CreatedUpdatedModel, CustomFieldModel): """ A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType, @@ -828,7 +844,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel): ordering = ['name'] unique_together = ['rack', 'position', 'face'] - def __unicode__(self): + def __str__(self): return self.display_name def get_absolute_url(self): @@ -968,6 +984,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel): return RPC_CLIENTS.get(self.platform.rpc_client) +@python_2_unicode_compatible class ConsolePort(models.Model): """ A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. @@ -982,7 +999,7 @@ class ConsolePort(models.Model): ordering = ['device', 'name'] unique_together = ['device', 'name'] - def __unicode__(self): + def __str__(self): return self.name # Used for connections export @@ -1011,6 +1028,7 @@ class ConsoleServerPortManager(models.Manager): }).order_by('device', 'name_as_integer') +@python_2_unicode_compatible class ConsoleServerPort(models.Model): """ A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. @@ -1023,10 +1041,11 @@ class ConsoleServerPort(models.Model): class Meta: unique_together = ['device', 'name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class PowerPort(models.Model): """ A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. @@ -1041,7 +1060,7 @@ class PowerPort(models.Model): ordering = ['device', 'name'] unique_together = ['device', 'name'] - def __unicode__(self): + def __str__(self): return self.name # Used for connections export @@ -1064,6 +1083,7 @@ class PowerOutletManager(models.Manager): }).order_by('device', 'name_padded') +@python_2_unicode_compatible class PowerOutlet(models.Model): """ A physical power outlet (output) within a Device which provides power to a PowerPort. @@ -1076,10 +1096,11 @@ class PowerOutlet(models.Model): class Meta: unique_together = ['device', 'name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Interface(models.Model): """ A physical data interface within a Device. An Interface can connect to exactly one other Interface via the creation @@ -1099,7 +1120,7 @@ class Interface(models.Model): ordering = ['device', 'name'] unique_together = ['device', 'name'] - def __unicode__(self): + def __str__(self): return self.name def clean(self): @@ -1176,6 +1197,7 @@ class InterfaceConnection(models.Model): ]) +@python_2_unicode_compatible class DeviceBay(models.Model): """ An empty space within a Device which can house a child device @@ -1189,7 +1211,7 @@ class DeviceBay(models.Model): ordering = ['device', 'name'] unique_together = ['device', 'name'] - def __unicode__(self): + def __str__(self): return u'{} - {}'.format(self.device.name, self.name) def clean(self): @@ -1205,6 +1227,7 @@ class DeviceBay(models.Model): raise ValidationError("Cannot install a device into itself.") +@python_2_unicode_compatible class Module(models.Model): """ A Module represents a piece of hardware within a Device, such as a line card or power supply. Modules are used only @@ -1223,5 +1246,5 @@ class Module(models.Model): ordering = ['device__id', 'parent__id', 'name'] unique_together = ['device', 'parent', 'name'] - def __unicode__(self): + def __str__(self): return self.name diff --git a/netbox/dcim/tests/test_apis.py b/netbox/dcim/tests/test_apis.py index 3cef01701..0f7d1bbe3 100644 --- a/netbox/dcim/tests/test_apis.py +++ b/netbox/dcim/tests/test_apis.py @@ -65,7 +65,7 @@ class SiteTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/sites/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -75,7 +75,7 @@ class SiteTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/sites/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -84,9 +84,9 @@ class SiteTest(APITestCase): def test_get_site_list_rack(self, endpoint='/{}api/dcim/sites/1/racks/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) - for i in json.loads(response.content): + for i in json.loads(response.content.decode('utf-8')): self.assertEqual( sorted(i.keys()), sorted(self.rack_fields), @@ -99,9 +99,9 @@ class SiteTest(APITestCase): def test_get_site_list_graphs(self, endpoint='/{}api/dcim/sites/1/graphs/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) - for i in json.loads(response.content): + for i in json.loads(response.content.decode('utf-8')): self.assertEqual( sorted(i.keys()), sorted(self.graph_fields), @@ -159,7 +159,7 @@ class RackTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/racks/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -173,7 +173,7 @@ class RackTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/racks/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -202,7 +202,7 @@ class ManufacturersTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/manufacturers/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -212,7 +212,7 @@ class ManufacturersTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/manufacturers/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -239,6 +239,7 @@ class DeviceTypeTest(APITestCase): 'subdevice_role', 'comments', 'custom_fields', + 'instance_count', ] nested_fields = [ @@ -250,7 +251,7 @@ class DeviceTypeTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/device-types/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -261,7 +262,7 @@ class DeviceTypeTest(APITestCase): def test_detail_list(self, endpoint='/{}api/dcim/device-types/1/'.format(settings.BASE_PATH)): # TODO: details returns list view. # response = self.client.get(endpoint) - # content = json.loads(response.content) + # content = json.loads(response.content.decode('utf-8')) # self.assertEqual(response.status_code, status.HTTP_200_OK) # self.assertEqual( # sorted(content.keys()), @@ -284,7 +285,7 @@ class DeviceRolesTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/device-roles/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -294,7 +295,7 @@ class DeviceRolesTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/device-roles/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -312,7 +313,7 @@ class PlatformsTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/platforms/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -322,7 +323,7 @@ class PlatformsTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/platforms/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -360,7 +361,7 @@ class DeviceTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/devices/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for device in content: self.assertEqual( @@ -425,7 +426,7 @@ class DeviceTest(APITestCase): ] response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) device = content[0] self.assertEqual( @@ -435,7 +436,7 @@ class DeviceTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/devices/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -453,7 +454,7 @@ class ConsoleServerPortsTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/devices/9/console-server-ports/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for console_port in content: self.assertEqual( @@ -475,7 +476,7 @@ class ConsolePortsTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/devices/1/console-ports/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for console_port in content: self.assertEqual( @@ -493,7 +494,7 @@ class ConsolePortsTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/console-ports/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -514,7 +515,7 @@ class PowerPortsTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/devices/1/power-ports/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -528,7 +529,7 @@ class PowerPortsTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/power-ports/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -549,7 +550,7 @@ class PowerOutletsTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/devices/11/power-outlets/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -599,7 +600,7 @@ class InterfaceTest(APITestCase): def test_get_list(self, endpoint='/{}api/dcim/devices/1/interfaces/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) for i in content: self.assertEqual( @@ -613,7 +614,7 @@ class InterfaceTest(APITestCase): def test_get_detail(self, endpoint='/{}api/dcim/interfaces/1/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -625,19 +626,19 @@ class InterfaceTest(APITestCase): ) def test_get_graph_list(self, endpoint='/{}api/dcim/interfaces/1/graphs/'.format(settings.BASE_PATH)): - response = self.client.get(endpoint) - content = json.loads(response.content) - self.assertEqual(response.status_code, status.HTTP_200_OK) - for i in content: - self.assertEqual( - sorted(i.keys()), - sorted(SiteTest.graph_fields), - ) + response = self.client.get(endpoint) + content = json.loads(response.content.decode('utf-8')) + self.assertEqual(response.status_code, status.HTTP_200_OK) + for i in content: + self.assertEqual( + sorted(i.keys()), + sorted(SiteTest.graph_fields), + ) def test_get_interface_connections(self, endpoint='/{}api/dcim/interface-connections/4/' .format(settings.BASE_PATH)): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), @@ -659,7 +660,7 @@ class RelatedConnectionsTest(APITestCase): def test_get_list(self, endpoint=('/{}api/dcim/related-connections/?peer-device=test1-edge1&peer-interface=xe-0/0/3' .format(settings.BASE_PATH))): response = self.client.get(endpoint) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( sorted(content.keys()), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 6b01c7bb8..17f74eae3 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -71,7 +71,7 @@ class ComponentCreateView(View): 'parent': parent, 'component_type': self.model._meta.verbose_name, 'form': self.form(initial=request.GET), - 'cancel_url': parent.get_absolute_url(), + 'return_url': parent.get_absolute_url(), }) def post(self, request, pk): @@ -112,10 +112,22 @@ class ComponentCreateView(View): 'parent': parent, 'component_type': self.model._meta.verbose_name, 'form': form, - 'cancel_url': parent.get_absolute_url(), + 'return_url': parent.get_absolute_url(), }) +class ComponentEditView(ObjectEditView): + + def get_return_url(self, obj): + return obj.device.get_absolute_url() + + +class ComponentDeleteView(ObjectDeleteView): + + def get_return_url(self, obj): + return obj.device.get_absolute_url() + + # # Sites # @@ -125,7 +137,6 @@ class SiteListView(ObjectListView): filter = filters.SiteFilter filter_form = forms.SiteFilterForm table = tables.SiteTable - edit_permissions = ['dcim.change_rack', 'dcim.delete_rack'] template_name = 'dcim/site_list.html' @@ -157,7 +168,7 @@ class SiteEditView(PermissionRequiredMixin, ObjectEditView): model = Site form_class = forms.SiteForm template_name = 'dcim/site_edit.html' - obj_list_url = 'dcim:site_list' + default_return_url = 'dcim:site_list' class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView): @@ -171,15 +182,16 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView): form = forms.SiteImportForm table = tables.SiteTable template_name = 'dcim/site_import.html' - obj_list_url = 'dcim:site_list' + default_return_url = 'dcim:site_list' class SiteBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_site' cls = Site + filter = filters.SiteFilter form = forms.SiteBulkEditForm template_name = 'dcim/site_bulk_edit.html' - default_redirect_url = 'dcim:site_list' + default_return_url = 'dcim:site_list' # @@ -191,7 +203,6 @@ class RackGroupListView(ObjectListView): filter = filters.RackGroupFilter filter_form = forms.RackGroupFilterForm table = tables.RackGroupTable - edit_permissions = ['dcim.change_rackgroup', 'dcim.delete_rackgroup'] template_name = 'dcim/rackgroup_list.html' @@ -207,7 +218,8 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView): class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rackgroup' cls = RackGroup - default_redirect_url = 'dcim:rackgroup_list' + filter = filters.RackGroupFilter + default_return_url = 'dcim:rackgroup_list' # @@ -217,7 +229,6 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RackRoleListView(ObjectListView): queryset = RackRole.objects.annotate(rack_count=Count('racks')) table = tables.RackRoleTable - edit_permissions = ['dcim.change_rackrole', 'dcim.delete_rackrole'] template_name = 'dcim/rackrole_list.html' @@ -233,7 +244,7 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView): class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rackrole' cls = RackRole - default_redirect_url = 'dcim:rackrole_list' + default_return_url = 'dcim:rackrole_list' # @@ -246,7 +257,6 @@ class RackListView(ObjectListView): filter = filters.RackFilter filter_form = forms.RackFilterForm table = tables.RackTable - edit_permissions = ['dcim.change_rack', 'dcim.delete_rack'] template_name = 'dcim/rack_list.html' @@ -274,7 +284,7 @@ class RackEditView(PermissionRequiredMixin, ObjectEditView): model = Rack form_class = forms.RackForm template_name = 'dcim/rack_edit.html' - obj_list_url = 'dcim:rack_list' + default_return_url = 'dcim:rack_list' class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView): @@ -288,21 +298,23 @@ class RackBulkImportView(PermissionRequiredMixin, BulkImportView): form = forms.RackImportForm table = tables.RackImportTable template_name = 'dcim/rack_import.html' - obj_list_url = 'dcim:rack_list' + default_return_url = 'dcim:rack_list' class RackBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_rack' cls = Rack + filter = filters.RackFilter form = forms.RackBulkEditForm template_name = 'dcim/rack_bulk_edit.html' - default_redirect_url = 'dcim:rack_list' + default_return_url = 'dcim:rack_list' class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rack' cls = Rack - default_redirect_url = 'dcim:rack_list' + filter = filters.RackFilter + default_return_url = 'dcim:rack_list' # @@ -312,7 +324,6 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ManufacturerListView(ObjectListView): queryset = Manufacturer.objects.annotate(devicetype_count=Count('device_types')) table = tables.ManufacturerTable - edit_permissions = ['dcim.change_manufacturer', 'dcim.delete_manufacturer'] template_name = 'dcim/manufacturer_list.html' @@ -328,7 +339,7 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView): class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_manufacturer' cls = Manufacturer - default_redirect_url = 'dcim:manufacturer_list' + default_return_url = 'dcim:manufacturer_list' # @@ -340,7 +351,6 @@ class DeviceTypeListView(ObjectListView): filter = filters.DeviceTypeFilter filter_form = forms.DeviceTypeFilterForm table = tables.DeviceTypeTable - edit_permissions = ['dcim.change_devicetype', 'dcim.delete_devicetype'] template_name = 'dcim/devicetype_list.html' @@ -398,7 +408,7 @@ class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView): model = DeviceType form_class = forms.DeviceTypeForm template_name = 'dcim/devicetype_edit.html' - obj_list_url = 'dcim:devicetype_list' + default_return_url = 'dcim:devicetype_list' class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView): @@ -410,15 +420,17 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView): class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_devicetype' cls = DeviceType + filter = filters.DeviceTypeFilter form = forms.DeviceTypeBulkEditForm template_name = 'dcim/devicetype_bulk_edit.html' - default_redirect_url = 'dcim:devicetype_list' + default_return_url = 'dcim:devicetype_list' class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_devicetype' cls = DeviceType - default_redirect_url = 'dcim:devicetype_list' + filter = filters.DeviceTypeFilter + default_return_url = 'dcim:devicetype_list' # @@ -532,7 +544,6 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class DeviceRoleListView(ObjectListView): queryset = DeviceRole.objects.annotate(device_count=Count('devices')) table = tables.DeviceRoleTable - edit_permissions = ['dcim.change_devicerole', 'dcim.delete_devicerole'] template_name = 'dcim/devicerole_list.html' @@ -548,7 +559,7 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView): class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_devicerole' cls = DeviceRole - default_redirect_url = 'dcim:devicerole_list' + default_return_url = 'dcim:devicerole_list' # @@ -558,7 +569,6 @@ class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class PlatformListView(ObjectListView): queryset = Platform.objects.annotate(device_count=Count('devices')) table = tables.PlatformTable - edit_permissions = ['dcim.change_platform', 'dcim.delete_platform'] template_name = 'dcim/platform_list.html' @@ -574,7 +584,7 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView): class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_platform' cls = Platform - default_redirect_url = 'dcim:platform_list' + default_return_url = 'dcim:platform_list' # @@ -587,7 +597,6 @@ class DeviceListView(ObjectListView): filter = filters.DeviceFilter filter_form = forms.DeviceFilterForm table = tables.DeviceTable - edit_permissions = ['dcim.change_device', 'dcim.delete_device'] template_name = 'dcim/device_list.html' @@ -666,7 +675,7 @@ class DeviceEditView(PermissionRequiredMixin, ObjectEditView): form_class = forms.DeviceForm fields_initial = ['site', 'rack', 'position', 'face', 'device_bay'] template_name = 'dcim/device_edit.html' - obj_list_url = 'dcim:device_list' + default_return_url = 'dcim:device_list' class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView): @@ -680,7 +689,7 @@ class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView): form = forms.DeviceImportForm table = tables.DeviceImportTable template_name = 'dcim/device_import.html' - obj_list_url = 'dcim:device_list' + default_return_url = 'dcim:device_list' class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView): @@ -688,7 +697,7 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView): form = forms.ChildDeviceImportForm table = tables.DeviceImportTable template_name = 'dcim/device_import_child.html' - obj_list_url = 'dcim:device_list' + default_return_url = 'dcim:device_list' def save_obj(self, obj): # Inherent rack from parent device @@ -703,15 +712,17 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView): class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_device' cls = Device + filter = filters.DeviceFilter form = forms.DeviceBulkEditForm template_name = 'dcim/device_bulk_edit.html' - default_redirect_url = 'dcim:device_list' + default_return_url = 'dcim:device_list' class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_device' cls = Device - default_redirect_url = 'dcim:device_list' + filter = filters.DeviceFilter + default_return_url = 'dcim:device_list' def device_inventory(request, pk): @@ -729,7 +740,8 @@ def device_inventory(request, pk): def device_lldp_neighbors(request, pk): device = get_object_or_404(Device, pk=pk) - interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b') + interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\ + .select_related('connected_as_a', 'connected_as_b') return render(request, 'dcim/device_lldp_neighbors.html', { 'device': device, @@ -776,7 +788,7 @@ def consoleport_connect(request, pk): return render(request, 'dcim/consoleport_connect.html', { 'consoleport': consoleport, 'form': form, - 'cancel_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}), + 'return_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}), }) @@ -805,17 +817,17 @@ def consoleport_disconnect(request, pk): return render(request, 'dcim/consoleport_disconnect.html', { 'consoleport': consoleport, 'form': form, - 'cancel_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}), + 'return_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}), }) -class ConsolePortEditView(PermissionRequiredMixin, ObjectEditView): +class ConsolePortEditView(PermissionRequiredMixin, ComponentEditView): permission_required = 'dcim.change_consoleport' model = ConsolePort form_class = forms.ConsolePortForm -class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView): +class ConsolePortDeleteView(PermissionRequiredMixin, ComponentDeleteView): permission_required = 'dcim.delete_consoleport' model = ConsolePort @@ -872,7 +884,7 @@ def consoleserverport_connect(request, pk): return render(request, 'dcim/consoleserverport_connect.html', { 'consoleserverport': consoleserverport, 'form': form, - 'cancel_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}), + 'return_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}), }) @@ -902,17 +914,17 @@ def consoleserverport_disconnect(request, pk): return render(request, 'dcim/consoleserverport_disconnect.html', { 'consoleserverport': consoleserverport, 'form': form, - 'cancel_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}), + 'return_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}), }) -class ConsoleServerPortEditView(PermissionRequiredMixin, ObjectEditView): +class ConsoleServerPortEditView(PermissionRequiredMixin, ComponentEditView): permission_required = 'dcim.change_consoleserverport' model = ConsoleServerPort form_class = forms.ConsoleServerPortForm -class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): +class ConsoleServerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView): permission_required = 'dcim.delete_consoleserverport' model = ConsoleServerPort @@ -962,7 +974,7 @@ def powerport_connect(request, pk): return render(request, 'dcim/powerport_connect.html', { 'powerport': powerport, 'form': form, - 'cancel_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}), + 'return_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}), }) @@ -991,17 +1003,17 @@ def powerport_disconnect(request, pk): return render(request, 'dcim/powerport_disconnect.html', { 'powerport': powerport, 'form': form, - 'cancel_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}), + 'return_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}), }) -class PowerPortEditView(PermissionRequiredMixin, ObjectEditView): +class PowerPortEditView(PermissionRequiredMixin, ComponentEditView): permission_required = 'dcim.change_powerport' model = PowerPort form_class = forms.PowerPortForm -class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): +class PowerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView): permission_required = 'dcim.delete_powerport' model = PowerPort @@ -1058,7 +1070,7 @@ def poweroutlet_connect(request, pk): return render(request, 'dcim/poweroutlet_connect.html', { 'poweroutlet': poweroutlet, 'form': form, - 'cancel_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}), + 'return_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}), }) @@ -1087,17 +1099,17 @@ def poweroutlet_disconnect(request, pk): return render(request, 'dcim/poweroutlet_disconnect.html', { 'poweroutlet': poweroutlet, 'form': form, - 'cancel_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}), + 'return_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}), }) -class PowerOutletEditView(PermissionRequiredMixin, ObjectEditView): +class PowerOutletEditView(PermissionRequiredMixin, ComponentEditView): permission_required = 'dcim.change_poweroutlet' model = PowerOutlet form_class = forms.PowerOutletForm -class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView): +class PowerOutletDeleteView(PermissionRequiredMixin, ComponentDeleteView): permission_required = 'dcim.delete_poweroutlet' model = PowerOutlet @@ -1121,13 +1133,13 @@ class InterfaceAddView(PermissionRequiredMixin, ComponentCreateView): model_form = forms.InterfaceForm -class InterfaceEditView(PermissionRequiredMixin, ObjectEditView): +class InterfaceEditView(PermissionRequiredMixin, ComponentEditView): permission_required = 'dcim.change_interface' model = Interface form_class = forms.InterfaceForm -class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView): +class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView): permission_required = 'dcim.delete_interface' model = Interface @@ -1159,13 +1171,13 @@ class DeviceBayAddView(PermissionRequiredMixin, ComponentCreateView): model_form = forms.DeviceBayForm -class DeviceBayEditView(PermissionRequiredMixin, ObjectEditView): +class DeviceBayEditView(PermissionRequiredMixin, ComponentEditView): permission_required = 'dcim.change_devicebay' model = DeviceBay form_class = forms.DeviceBayForm -class DeviceBayDeleteView(PermissionRequiredMixin, ObjectDeleteView): +class DeviceBayDeleteView(PermissionRequiredMixin, ComponentDeleteView): permission_required = 'dcim.delete_devicebay' model = DeviceBay @@ -1192,7 +1204,7 @@ def devicebay_populate(request, pk): return render(request, 'dcim/devicebay_populate.html', { 'device_bay': device_bay, 'form': form, - 'cancel_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}), + 'return_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}), }) @@ -1216,7 +1228,7 @@ def devicebay_depopulate(request, pk): return render(request, 'dcim/devicebay_depopulate.html', { 'device_bay': device_bay, 'form': form, - 'cancel_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}), + 'return_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}), }) @@ -1245,7 +1257,7 @@ class DeviceBulkAddComponentView(View): # Are we editing *all* objects in the queryset or just a selected subset? if request.POST.get('_all'): - pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk] + pk_list = [obj.pk for obj in filters.DeviceFilter(request.GET, Device.objects.all())] else: pk_list = [int(pk) for pk in request.POST.getlist('pk')] @@ -1291,7 +1303,7 @@ class DeviceBulkAddComponentView(View): 'form': form, 'component_name': self.model._meta.verbose_name_plural, 'selected_devices': selected_devices, - 'cancel_url': reverse('dcim:device_list'), + 'return_url': reverse('dcim:device_list'), }) @@ -1373,7 +1385,7 @@ def interfaceconnection_add(request, pk): return render(request, 'dcim/interfaceconnection_edit.html', { 'device': device, 'form': form, - 'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}), + 'return_url': reverse('dcim:device', kwargs={'pk': device.pk}), }) @@ -1405,15 +1417,15 @@ def interfaceconnection_delete(request, pk): # Determine where to direct user upon cancellation if device_id: - cancel_url = reverse('dcim:device', kwargs={'pk': device_id}) + return_url = reverse('dcim:device', kwargs={'pk': device_id}) else: - cancel_url = reverse('dcim:device_list') + return_url = reverse('dcim:device_list') return render(request, 'dcim/interfaceconnection_delete.html', { 'interfaceconnection': interfaceconnection, 'device_id': device_id, 'form': form, - 'cancel_url': cancel_url, + 'return_url': return_url, }) @@ -1492,7 +1504,7 @@ def ipaddress_assign(request, pk): return render(request, 'dcim/ipaddress_assign.html', { 'device': device, 'form': form, - 'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}), + 'return_url': reverse('dcim:device', kwargs={'pk': device.pk}), }) @@ -1500,7 +1512,7 @@ def ipaddress_assign(request, pk): # Modules # -class ModuleEditView(PermissionRequiredMixin, ObjectEditView): +class ModuleEditView(PermissionRequiredMixin, ComponentEditView): permission_required = 'dcim.change_module' model = Module form_class = forms.ModuleForm @@ -1510,10 +1522,7 @@ class ModuleEditView(PermissionRequiredMixin, ObjectEditView): obj.device = get_object_or_404(Device, pk=kwargs['device']) return obj - def get_return_url(self, obj): - return obj.device.get_absolute_url() - -class ModuleDeleteView(PermissionRequiredMixin, ObjectDeleteView): +class ModuleDeleteView(PermissionRequiredMixin, ComponentDeleteView): permission_required = 'dcim.delete_module' model = Module diff --git a/netbox/extras/api/renderers.py b/netbox/extras/api/renderers.py index 1b1f4ac27..0fd35c762 100644 --- a/netbox/extras/api/renderers.py +++ b/netbox/extras/api/renderers.py @@ -50,7 +50,7 @@ class FlatJSONRenderer(renderers.BaseRenderer): def render(self, data, media_type=None, renderer_context=None): def flatten(entry): - for key, val in entry.iteritems(): + for key, val in entry.items(): if isinstance(val, dict): for child_key, child_val in flatten(val): yield "{}_{}".format(key, child_key), child_val diff --git a/netbox/extras/models.py b/netbox/extras/models.py index d45e4846f..f06d0aa29 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -8,6 +8,7 @@ from django.core.validators import ValidationError from django.db import models from django.http import HttpResponse from django.template import Template, Context +from django.utils.encoding import python_2_unicode_compatible from django.utils.safestring import mark_safe @@ -93,6 +94,7 @@ class CustomFieldModel(object): return OrderedDict([(field, None) for field in fields]) +@python_2_unicode_compatible class CustomField(models.Model): obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)', limit_choices_to={'model__in': CUSTOMFIELD_MODELS}, @@ -114,7 +116,7 @@ class CustomField(models.Model): class Meta: ordering = ['weight', 'name'] - def __unicode__(self): + def __str__(self): return self.label or self.name.replace('_', ' ').capitalize() def serialize_value(self, value): @@ -153,6 +155,7 @@ class CustomField(models.Model): return serialized_value +@python_2_unicode_compatible class CustomFieldValue(models.Model): field = models.ForeignKey('CustomField', related_name='values') obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT) @@ -164,7 +167,7 @@ class CustomFieldValue(models.Model): ordering = ['obj_type', 'obj_id'] unique_together = ['field', 'obj_type', 'obj_id'] - def __unicode__(self): + def __str__(self): return u'{} {}'.format(self.obj, self.field) @property @@ -183,6 +186,7 @@ class CustomFieldValue(models.Model): super(CustomFieldValue, self).save(*args, **kwargs) +@python_2_unicode_compatible class CustomFieldChoice(models.Model): field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT}, on_delete=models.CASCADE) @@ -193,7 +197,7 @@ class CustomFieldChoice(models.Model): ordering = ['field', 'weight', 'value'] unique_together = ['field', 'value'] - def __unicode__(self): + def __str__(self): return self.value def clean(self): @@ -207,6 +211,7 @@ class CustomFieldChoice(models.Model): CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete() +@python_2_unicode_compatible class Graph(models.Model): type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES) weight = models.PositiveSmallIntegerField(default=1000) @@ -217,7 +222,7 @@ class Graph(models.Model): class Meta: ordering = ['type', 'weight', 'name'] - def __unicode__(self): + def __str__(self): return self.name def embed_url(self, obj): @@ -231,6 +236,7 @@ class Graph(models.Model): return template.render(Context({'obj': obj})) +@python_2_unicode_compatible class ExportTemplate(models.Model): content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}) name = models.CharField(max_length=100) @@ -245,7 +251,7 @@ class ExportTemplate(models.Model): ['content_type', 'name'] ] - def __unicode__(self): + def __str__(self): return u'{}: {}'.format(self.content_type, self.name) def to_response(self, context_dict, filename): @@ -264,6 +270,7 @@ class ExportTemplate(models.Model): return response +@python_2_unicode_compatible class TopologyMap(models.Model): name = models.CharField(max_length=50, unique=True) slug = models.SlugField(unique=True) @@ -278,7 +285,7 @@ class TopologyMap(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name @property @@ -328,6 +335,7 @@ class UserActionManager(models.Manager): self.log_bulk_action(user, content_type, ACTION_BULK_DELETE, message) +@python_2_unicode_compatible class UserAction(models.Model): """ A record of an action (add, edit, or delete) performed on an object by a User. @@ -344,7 +352,7 @@ class UserAction(models.Model): class Meta: ordering = ['-time'] - def __unicode__(self): + def __str__(self): if self.message: return u'{} {}'.format(self.user, self.message) return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type) diff --git a/netbox/generate_secret_key.py b/netbox/generate_secret_key.py index d935910f6..0e0214dc4 100755 --- a/netbox/generate_secret_key.py +++ b/netbox/generate_secret_key.py @@ -1,8 +1,8 @@ -#!/usr/bin/python +#!/usr/bin/env python # This script will generate a random 50-character string suitable for use as a SECRET_KEY. import os import random charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)' random.seed = (os.urandom(2048)) -print ''.join(random.choice(charset) for c in range(50)) +print(''.join(random.choice(charset) for c in range(50))) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 2f6f0af84..4b9d8ddf5 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -63,6 +63,7 @@ class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm): model = VRF + q = forms.CharField(required=False, label='Search') tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), to_field_name='slug', null_option=(0, None)) @@ -128,6 +129,7 @@ class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Aggregate + q = forms.CharField(required=False, label='Search') family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family') rir = FilterChoiceField(queryset=RIR.objects.annotate(filter_count=Count('aggregates')), to_field_name='slug', label='RIR') @@ -256,8 +258,9 @@ def prefix_status_choices(): class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Prefix - parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={ - 'placeholder': 'Network', + q = forms.CharField(required=False, label='Search') + parent = forms.CharField(required=False, label='Parent Prefix', widget=forms.TextInput(attrs={ + 'placeholder': 'Prefix', })) family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family') vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('prefixes')), to_field_name='rd', @@ -446,7 +449,8 @@ def ipaddress_status_choices(): class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): model = IPAddress - parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={ + q = forms.CharField(required=False, label='Search') + parent = forms.CharField(required=False, label='Parent Prefix', widget=forms.TextInput(attrs={ 'placeholder': 'Prefix', })) family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family') @@ -560,6 +564,7 @@ def vlan_status_choices(): class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): model = VLAN + q = forms.CharField(required=False, label='Search') site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug') group_id = FilterChoiceField(queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group', null_option=(0, 'None')) diff --git a/netbox/ipam/migrations/0014_ipaddress_status_add_deprecated.py b/netbox/ipam/migrations/0014_ipaddress_status_add_deprecated.py new file mode 100644 index 000000000..adc8e606c --- /dev/null +++ b/netbox/ipam/migrations/0014_ipaddress_status_add_deprecated.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2017-01-23 19:10 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0013_prefix_add_is_pool'), + ] + + operations = [ + migrations.AlterField( + model_name='ipaddress', + name='status', + field=models.PositiveSmallIntegerField(choices=[(1, b'Active'), (2, b'Reserved'), (3, b'Deprecated'), (5, b'DHCP')], default=1, verbose_name=b'Status'), + ), + ] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index c8afc6402..d37fdec25 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -7,6 +7,7 @@ from django.core.urlresolvers import reverse from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models.expressions import RawSQL +from django.utils.encoding import python_2_unicode_compatible from dcim.models import Interface from extras.models import CustomFieldModel, CustomFieldValue @@ -36,10 +37,12 @@ PREFIX_STATUS_CHOICES = ( IPADDRESS_STATUS_ACTIVE = 1 IPADDRESS_STATUS_RESERVED = 2 +IPADDRESS_STATUS_DEPRECATED = 3 IPADDRESS_STATUS_DHCP = 5 IPADDRESS_STATUS_CHOICES = ( (IPADDRESS_STATUS_ACTIVE, 'Active'), (IPADDRESS_STATUS_RESERVED, 'Reserved'), + (IPADDRESS_STATUS_DEPRECATED, 'Deprecated'), (IPADDRESS_STATUS_DHCP, 'DHCP') ) @@ -70,6 +73,7 @@ IP_PROTOCOL_CHOICES = ( ) +@python_2_unicode_compatible class VRF(CreatedUpdatedModel, CustomFieldModel): """ A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing @@ -89,7 +93,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel): verbose_name = 'VRF' verbose_name_plural = 'VRFs' - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): @@ -105,6 +109,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel): ]) +@python_2_unicode_compatible class RIR(models.Model): """ A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address @@ -120,13 +125,14 @@ class RIR(models.Model): verbose_name = 'RIR' verbose_name_plural = 'RIRs' - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug) +@python_2_unicode_compatible class Aggregate(CreatedUpdatedModel, CustomFieldModel): """ An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize @@ -142,7 +148,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel): class Meta: ordering = ['family', 'prefix'] - def __unicode__(self): + def __str__(self): return str(self.prefix) def get_absolute_url(self): @@ -204,6 +210,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel): return int(children_size / self.prefix.size * 100) +@python_2_unicode_compatible class Role(models.Model): """ A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or @@ -216,7 +223,7 @@ class Role(models.Model): class Meta: ordering = ['weight', 'name'] - def __unicode__(self): + def __str__(self): return self.name @property @@ -263,6 +270,7 @@ class PrefixQuerySet(NullsFirstQuerySet): return filter(lambda p: p.depth <= limit, queryset) +@python_2_unicode_compatible class Prefix(CreatedUpdatedModel, CustomFieldModel): """ A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and @@ -292,7 +300,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): ordering = ['vrf', 'family', 'prefix'] verbose_name_plural = 'prefixes' - def __unicode__(self): + def __str__(self): return str(self.prefix) def get_absolute_url(self): @@ -377,6 +385,7 @@ class IPAddressManager(models.Manager): return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host') +@python_2_unicode_compatible class IPAddress(CreatedUpdatedModel, CustomFieldModel): """ An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is @@ -409,7 +418,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): verbose_name = 'IP address' verbose_name_plural = 'IP addresses' - def __unicode__(self): + def __str__(self): return str(self.address) def get_absolute_url(self): @@ -469,6 +478,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): return STATUS_CHOICE_CLASSES[self.status] +@python_2_unicode_compatible class VLANGroup(models.Model): """ A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique. @@ -486,13 +496,14 @@ class VLANGroup(models.Model): verbose_name = 'VLAN group' verbose_name_plural = 'VLAN groups' - def __unicode__(self): + def __str__(self): return u'{} - {}'.format(self.site.name, self.name) def get_absolute_url(self): return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk) +@python_2_unicode_compatible class VLAN(CreatedUpdatedModel, CustomFieldModel): """ A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned @@ -524,7 +535,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): verbose_name = 'VLAN' verbose_name_plural = 'VLANs' - def __unicode__(self): + def __str__(self): return self.display_name def get_absolute_url(self): @@ -558,6 +569,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): return STATUS_CHOICE_CLASSES[self.status] +@python_2_unicode_compatible class Service(CreatedUpdatedModel): """ A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device. A Service may optionally be tied @@ -576,5 +588,5 @@ class Service(CreatedUpdatedModel): ordering = ['device', 'protocol', 'port'] unique_together = ['device', 'protocol', 'port'] - def __unicode__(self): + def __str__(self): return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display()) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index f4ceffd60..6c99f7d9e 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -234,11 +234,12 @@ class PrefixBriefTable(BaseTable): vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') + vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN') role = tables.Column(verbose_name='Role') class Meta(BaseTable.Meta): model = Prefix - fields = ('prefix', 'vrf', 'status', 'site', 'role') + fields = ('prefix', 'vrf', 'status', 'site', 'vlan', 'role') orderable = False diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 4182532ab..38c641e05 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -95,7 +95,6 @@ class VRFListView(ObjectListView): filter = filters.VRFFilter filter_form = forms.VRFFilterForm table = tables.VRFTable - edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf'] template_name = 'ipam/vrf_list.html' @@ -118,7 +117,7 @@ class VRFEditView(PermissionRequiredMixin, ObjectEditView): model = VRF form_class = forms.VRFForm template_name = 'ipam/vrf_edit.html' - obj_list_url = 'ipam:vrf_list' + default_return_url = 'ipam:vrf_list' class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView): @@ -132,21 +131,23 @@ class VRFBulkImportView(PermissionRequiredMixin, BulkImportView): form = forms.VRFImportForm table = tables.VRFTable template_name = 'ipam/vrf_import.html' - obj_list_url = 'ipam:vrf_list' + default_return_url = 'ipam:vrf_list' class VRFBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'ipam.change_vrf' cls = VRF + filter = filters.VRFFilter form = forms.VRFBulkEditForm template_name = 'ipam/vrf_bulk_edit.html' - default_redirect_url = 'ipam:vrf_list' + default_return_url = 'ipam:vrf_list' class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'ipam.delete_vrf' cls = VRF - default_redirect_url = 'ipam:vrf_list' + filter = filters.VRFFilter + default_return_url = 'ipam:vrf_list' # @@ -158,7 +159,6 @@ class RIRListView(ObjectListView): filter = filters.RIRFilter filter_form = forms.RIRFilterForm table = tables.RIRTable - edit_permissions = ['ipam.change_rir', 'ipam.delete_rir'] template_name = 'ipam/rir_list.html' def alter_queryset(self, request): @@ -250,7 +250,8 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView): class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'ipam.delete_rir' cls = RIR - default_redirect_url = 'ipam:rir_list' + filter = filters.RIRFilter + default_return_url = 'ipam:rir_list' # @@ -264,7 +265,6 @@ class AggregateListView(ObjectListView): filter = filters.AggregateFilter filter_form = forms.AggregateFilterForm table = tables.AggregateTable - edit_permissions = ['ipam.change_aggregate', 'ipam.delete_aggregate'] template_name = 'ipam/aggregate_list.html' def extra_context(self): @@ -308,7 +308,7 @@ class AggregateEditView(PermissionRequiredMixin, ObjectEditView): model = Aggregate form_class = forms.AggregateForm template_name = 'ipam/aggregate_edit.html' - obj_list_url = 'ipam:aggregate_list' + default_return_url = 'ipam:aggregate_list' class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView): @@ -322,21 +322,23 @@ class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView): form = forms.AggregateImportForm table = tables.AggregateTable template_name = 'ipam/aggregate_import.html' - obj_list_url = 'ipam:aggregate_list' + default_return_url = 'ipam:aggregate_list' class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'ipam.change_aggregate' cls = Aggregate + filter = filters.AggregateFilter form = forms.AggregateBulkEditForm template_name = 'ipam/aggregate_bulk_edit.html' - default_redirect_url = 'ipam:aggregate_list' + default_return_url = 'ipam:aggregate_list' class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'ipam.delete_aggregate' cls = Aggregate - default_redirect_url = 'ipam:aggregate_list' + filter = filters.AggregateFilter + default_return_url = 'ipam:aggregate_list' # @@ -346,7 +348,6 @@ class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RoleListView(ObjectListView): queryset = Role.objects.all() table = tables.RoleTable - edit_permissions = ['ipam.change_role', 'ipam.delete_role'] template_name = 'ipam/role_list.html' @@ -362,7 +363,7 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView): class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'ipam.delete_role' cls = Role - default_redirect_url = 'ipam:role_list' + default_return_url = 'ipam:role_list' # @@ -374,7 +375,6 @@ class PrefixListView(ObjectListView): filter = filters.PrefixFilter filter_form = forms.PrefixFilterForm table = tables.PrefixTable - edit_permissions = ['ipam.change_prefix', 'ipam.delete_prefix'] template_name = 'ipam/prefix_list.html' def alter_queryset(self, request): @@ -401,11 +401,13 @@ def prefix(request, pk): .filter(prefix__net_contains=str(prefix.prefix))\ .select_related('site', 'role').annotate_depth() parent_prefix_table = tables.PrefixBriefTable(parent_prefixes) + parent_prefix_table.exclude = ('vrf',) # Duplicate prefixes table duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\ .select_related('site', 'role') duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes)) + duplicate_prefix_table.exclude = ('vrf',) # Child prefixes table if prefix.vrf: @@ -430,6 +432,7 @@ def prefix(request, pk): 'parent_prefix_table': parent_prefix_table, 'child_prefix_table': child_prefix_table, 'duplicate_prefix_table': duplicate_prefix_table, + 'return_url': prefix.get_absolute_url(), }) @@ -439,14 +442,14 @@ class PrefixEditView(PermissionRequiredMixin, ObjectEditView): form_class = forms.PrefixForm template_name = 'ipam/prefix_edit.html' fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan'] - obj_list_url = 'ipam:prefix_list' + default_return_url = 'ipam:prefix_list' class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_prefix' model = Prefix - default_return_url = 'ipam:prefix_list' template_name = 'ipam/prefix_delete.html' + default_return_url = 'ipam:prefix_list' class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView): @@ -454,21 +457,23 @@ class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView): form = forms.PrefixImportForm table = tables.PrefixTable template_name = 'ipam/prefix_import.html' - obj_list_url = 'ipam:prefix_list' + default_return_url = 'ipam:prefix_list' class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'ipam.change_prefix' cls = Prefix + filter = filters.PrefixFilter form = forms.PrefixBulkEditForm template_name = 'ipam/prefix_bulk_edit.html' - default_redirect_url = 'ipam:prefix_list' + default_return_url = 'ipam:prefix_list' class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'ipam.delete_prefix' cls = Prefix - default_redirect_url = 'ipam:prefix_list' + filter = filters.PrefixFilter + default_return_url = 'ipam:prefix_list' def prefix_ipaddresses(request, pk): @@ -500,7 +505,6 @@ class IPAddressListView(ObjectListView): filter = filters.IPAddressFilter filter_form = forms.IPAddressFilterForm table = tables.IPAddressTable - edit_permissions = ['ipam.change_ipaddress', 'ipam.delete_ipaddress'] template_name = 'ipam/ipaddress_list.html' @@ -562,7 +566,7 @@ def ipaddress_assign(request, pk): return render(request, 'ipam/ipaddress_assign.html', { 'ipaddress': ipaddress, 'form': form, - 'cancel_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}), + 'return_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}), }) @@ -595,7 +599,7 @@ def ipaddress_remove(request, pk): return render(request, 'ipam/ipaddress_unassign.html', { 'ipaddress': ipaddress, 'form': form, - 'cancel_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}), + 'return_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}), }) @@ -605,7 +609,7 @@ class IPAddressEditView(PermissionRequiredMixin, ObjectEditView): form_class = forms.IPAddressForm fields_initial = ['address', 'vrf'] template_name = 'ipam/ipaddress_edit.html' - obj_list_url = 'ipam:ipaddress_list' + default_return_url = 'ipam:ipaddress_list' class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView): @@ -619,7 +623,7 @@ class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView): form = forms.IPAddressBulkAddForm model = IPAddress template_name = 'ipam/ipaddress_bulk_add.html' - redirect_url = 'ipam:ipaddress_list' + default_return_url = 'ipam:ipaddress_list' class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView): @@ -627,7 +631,7 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView): form = forms.IPAddressImportForm table = tables.IPAddressTable template_name = 'ipam/ipaddress_import.html' - obj_list_url = 'ipam:ipaddress_list' + default_return_url = 'ipam:ipaddress_list' def save_obj(self, obj): obj.save() @@ -648,15 +652,17 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView): class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'ipam.change_ipaddress' cls = IPAddress + filter = filters.IPAddressFilter form = forms.IPAddressBulkEditForm template_name = 'ipam/ipaddress_bulk_edit.html' - default_redirect_url = 'ipam:ipaddress_list' + default_return_url = 'ipam:ipaddress_list' class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'ipam.delete_ipaddress' cls = IPAddress - default_redirect_url = 'ipam:ipaddress_list' + filter = filters.IPAddressFilter + default_return_url = 'ipam:ipaddress_list' # @@ -668,7 +674,6 @@ class VLANGroupListView(ObjectListView): filter = filters.VLANGroupFilter filter_form = forms.VLANGroupFilterForm table = tables.VLANGroupTable - edit_permissions = ['ipam.change_vlangroup', 'ipam.delete_vlangroup'] template_name = 'ipam/vlangroup_list.html' @@ -684,7 +689,8 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView): class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'ipam.delete_vlangroup' cls = VLANGroup - default_redirect_url = 'ipam:vlangroup_list' + filter = filters.VLANGroupFilter + default_return_url = 'ipam:vlangroup_list' # @@ -696,7 +702,6 @@ class VLANListView(ObjectListView): filter = filters.VLANFilter filter_form = forms.VLANFilterForm table = tables.VLANTable - edit_permissions = ['ipam.change_vlan', 'ipam.delete_vlan'] template_name = 'ipam/vlan_list.html' @@ -705,6 +710,7 @@ def vlan(request, pk): vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk) prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role') prefix_table = tables.PrefixBriefTable(list(prefixes)) + prefix_table.exclude = ('vlan',) return render(request, 'ipam/vlan.html', { 'vlan': vlan, @@ -717,7 +723,7 @@ class VLANEditView(PermissionRequiredMixin, ObjectEditView): model = VLAN form_class = forms.VLANForm template_name = 'ipam/vlan_edit.html' - obj_list_url = 'ipam:vlan_list' + default_return_url = 'ipam:vlan_list' class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView): @@ -731,21 +737,23 @@ class VLANBulkImportView(PermissionRequiredMixin, BulkImportView): form = forms.VLANImportForm table = tables.VLANTable template_name = 'ipam/vlan_import.html' - obj_list_url = 'ipam:vlan_list' + default_return_url = 'ipam:vlan_list' class VLANBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'ipam.change_vlan' cls = VLAN + filter = filters.VLANFilter form = forms.VLANBulkEditForm template_name = 'ipam/vlan_bulk_edit.html' - default_redirect_url = 'ipam:vlan_list' + default_return_url = 'ipam:vlan_list' class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'ipam.delete_vlan' cls = VLAN - default_redirect_url = 'ipam:vlan_list' + filter = filters.VLANFilter + default_return_url = 'ipam:vlan_list' # diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index ad21789ec..d5909a960 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -6,13 +6,13 @@ from django.contrib.messages import constants as messages from django.core.exceptions import ImproperlyConfigured try: - import configuration + from netbox import configuration except ImportError: raise ImproperlyConfigured("Configuration file is not present. Please define netbox/netbox/configuration.py per " "the documentation.") -VERSION = '1.8.2' +VERSION = '1.8.3' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: @@ -50,7 +50,7 @@ CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS # Attempt to import LDAP configuration if it has been defined LDAP_IGNORE_CERT_ERRORS = False try: - from ldap_config import * + from netbox.ldap_config import * LDAP_CONFIGURED = True except ImportError: LDAP_CONFIGURED = False diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 9d01f1773..bbfdee58d 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -2,7 +2,7 @@ from django.conf import settings from django.conf.urls import include, url from django.contrib import admin -from views import home, handle_500, trigger_500 +from netbox.views import home, handle_500, trigger_500 from users.views import login, logout diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 3adc9c13f..3a5ad2b83 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -9,6 +9,14 @@ $(document).ready(function() { $('#select_all').prop('checked', false); } }); + // Enable hidden buttons when "select all" is checked + $('#select_all').click(function (event) { + if ($(this).is(':checked')) { + $('#select_all_box').find('button').prop('disabled', ''); + } else { + $('#select_all_box').find('button').prop('disabled', 'disabled'); + } + }); // Uncheck the "toggle all" checkbox if an item is unchecked $('input:checkbox[name=pk]').click(function (event) { if (!$(this).attr('checked')) { diff --git a/netbox/project-static/js/secrets.js b/netbox/project-static/js/secrets.js index fadcc0d39..22710236b 100644 --- a/netbox/project-static/js/secrets.js +++ b/netbox/project-static/js/secrets.js @@ -48,7 +48,7 @@ $(document).ready(function() { $('#generate_keypair').click(function() { $('#new_keypair_modal').modal('show'); $.ajax({ - url: '/api/secrets/generate-keys/', + url: netbox_api_path + 'secrets/generate-keys/', type: 'GET', dataType: 'json', success: function (response, status) { @@ -75,7 +75,7 @@ $(document).ready(function() { function unlock_secret(secret_id, private_key) { var csrf_token = $('input[name=csrfmiddlewaretoken]').val(); $.ajax({ - url: '/api/secrets/secrets/' + secret_id + '/', + url: netbox_api_path + 'secrets/secrets/' + secret_id + '/', type: 'POST', data: { private_key: private_key diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index 8012e2c55..b4c64b485 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -100,6 +100,7 @@ class SecretBulkEditForm(BootstrapMixin, BulkEditForm): class SecretFilterForm(BootstrapMixin, forms.Form): + q = forms.CharField(required=False, label='Search') role = FilterChoiceField(queryset=SecretRole.objects.annotate(filter_count=Count('secrets')), to_field_name='slug') diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index 930d6e032..a0c3e6f8b 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -8,7 +8,7 @@ from django.contrib.auth.models import Group, User from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.db import models -from django.utils.encoding import force_bytes +from django.utils.encoding import force_bytes, python_2_unicode_compatible from dcim.models import Device from utilities.models import CreatedUpdatedModel @@ -51,6 +51,7 @@ class UserKeyQuerySet(models.QuerySet): raise Exception("Bulk deletion has been disabled.") +@python_2_unicode_compatible class UserKey(CreatedUpdatedModel): """ A UserKey stores a user's personal RSA (public) encryption key, which is used to generate their unique encrypted @@ -76,7 +77,7 @@ class UserKey(CreatedUpdatedModel): self.__initial_public_key = self.public_key self.__initial_master_key_cipher = self.master_key_cipher - def __unicode__(self): + def __str__(self): return self.user.username def clean(self, *args, **kwargs): @@ -170,6 +171,7 @@ class UserKey(CreatedUpdatedModel): self.save() +@python_2_unicode_compatible class SecretRole(models.Model): """ A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles @@ -186,7 +188,7 @@ class SecretRole(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): @@ -201,6 +203,7 @@ class SecretRole(models.Model): return user in self.users.all() or user.groups.filter(pk__in=self.groups.all()).exists() +@python_2_unicode_compatible class Secret(CreatedUpdatedModel): """ A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible @@ -227,7 +230,7 @@ class Secret(CreatedUpdatedModel): self.plaintext = kwargs.pop('plaintext', None) super(Secret, self).__init__(*args, **kwargs) - def __unicode__(self): + def __str__(self): if self.role and self.device: return u'{} for {}'.format(self.role, self.device) return u'Secret' diff --git a/netbox/secrets/tests/__init__.py b/netbox/secrets/tests/__init__.py index b04a14228..e69de29bb 100644 --- a/netbox/secrets/tests/__init__.py +++ b/netbox/secrets/tests/__init__.py @@ -1 +0,0 @@ -from test_models import * diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 7fd3b4380..d67cd18a0 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -22,7 +22,6 @@ from .models import SecretRole, Secret, UserKey class SecretRoleListView(ObjectListView): queryset = SecretRole.objects.annotate(secret_count=Count('secrets')) table = tables.SecretRoleTable - edit_permissions = ['secrets.change_secretrole', 'secrets.delete_secretrole'] template_name = 'secrets/secretrole_list.html' @@ -38,7 +37,7 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView): class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'secrets.delete_secretrole' cls = SecretRole - default_redirect_url = 'secrets:secretrole_list' + default_return_url = 'secrets:secretrole_list' # @@ -51,7 +50,6 @@ class SecretListView(ObjectListView): filter = filters.SecretFilter filter_form = forms.SecretFilterForm table = tables.SecretTable - edit_permissions = ['secrets.change_secret', 'secrets.delete_secret'] template_name = 'secrets/secret_list.html' @@ -103,7 +101,7 @@ def secret_add(request, pk): return render(request, 'secrets/secret_edit.html', { 'secret': secret, 'form': form, - 'cancel_url': device.get_absolute_url(), + 'return_url': device.get_absolute_url(), }) @@ -145,7 +143,7 @@ def secret_edit(request, pk): return render(request, 'secrets/secret_edit.html', { 'secret': secret, 'form': form, - 'cancel_url': reverse('secrets:secret', kwargs={'pk': secret.pk}), + 'return_url': reverse('secrets:secret', kwargs={'pk': secret.pk}), }) @@ -195,19 +193,21 @@ def secret_import(request): return render(request, 'secrets/secret_import.html', { 'form': form, - 'cancel_url': reverse('secrets:secret_list'), + 'return_url': reverse('secrets:secret_list'), }) class SecretBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'secrets.change_secret' cls = Secret + filter = filters.SecretFilter form = forms.SecretBulkEditForm template_name = 'secrets/secret_bulk_edit.html' - default_redirect_url = 'secrets:secret_list' + default_return_url = 'secrets:secret_list' class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'secrets.delete_secret' cls = Secret - default_redirect_url = 'secrets:secret_list' + filter = filters.SecretFilter + default_return_url = 'secrets:secret_list' diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 9ef5ef4eb..4e63cf337 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -296,6 +296,9 @@ + diff --git a/netbox/templates/circuits/circuit_import.html b/netbox/templates/circuits/circuit_import.html index fec7ff65e..e2fc9fa36 100644 --- a/netbox/templates/circuits/circuit_import.html +++ b/netbox/templates/circuits/circuit_import.html @@ -13,7 +13,7 @@ {% render_form form %}
- Cancel + Cancel
diff --git a/netbox/templates/circuits/circuit_list.html b/netbox/templates/circuits/circuit_list.html index db6861b2e..63ee92f2d 100644 --- a/netbox/templates/circuits/circuit_list.html +++ b/netbox/templates/circuits/circuit_list.html @@ -24,7 +24,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/filter_panel.html' %}
{% endblock %} diff --git a/netbox/templates/circuits/circuittermination_edit.html b/netbox/templates/circuits/circuittermination_edit.html index 186b0e56c..8ccc05de2 100644 --- a/netbox/templates/circuits/circuittermination_edit.html +++ b/netbox/templates/circuits/circuittermination_edit.html @@ -83,7 +83,7 @@ {% else %} {% endif %} - Cancel + Cancel diff --git a/netbox/templates/circuits/provider_import.html b/netbox/templates/circuits/provider_import.html index d197c648f..a605164df 100644 --- a/netbox/templates/circuits/provider_import.html +++ b/netbox/templates/circuits/provider_import.html @@ -13,7 +13,7 @@ {% render_form form %}
- Cancel + Cancel
diff --git a/netbox/templates/circuits/provider_list.html b/netbox/templates/circuits/provider_list.html index ca3dbfc09..36438d66b 100644 --- a/netbox/templates/circuits/provider_list.html +++ b/netbox/templates/circuits/provider_list.html @@ -23,7 +23,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/filter_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/console_connections_list.html b/netbox/templates/dcim/console_connections_list.html index eb9531069..68b57dc27 100644 --- a/netbox/templates/dcim/console_connections_list.html +++ b/netbox/templates/dcim/console_connections_list.html @@ -19,7 +19,7 @@ {% render_table table 'table.html' %}
- {% include 'inc/filter_panel.html' %} + {% include 'inc/search_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/consoleport_connect.html b/netbox/templates/dcim/consoleport_connect.html index 46d636d8c..c237bc2c9 100644 --- a/netbox/templates/dcim/consoleport_connect.html +++ b/netbox/templates/dcim/consoleport_connect.html @@ -40,7 +40,7 @@
- Cancel + Cancel
diff --git a/netbox/templates/dcim/consoleserverport_connect.html b/netbox/templates/dcim/consoleserverport_connect.html index 9d2bbf0c8..e747a9d57 100644 --- a/netbox/templates/dcim/consoleserverport_connect.html +++ b/netbox/templates/dcim/consoleserverport_connect.html @@ -40,7 +40,7 @@
- Cancel + Cancel
diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 53273da47..0ce898e01 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -550,7 +550,7 @@ function toggleConnection(elem, api_url) { if (elem.hasClass('connected')) { $.ajax({ - url: api_url + elem.attr('data') + "/", + url: netbox_api_path + api_url + elem.attr('data') + "/", method: 'PATCH', dataType: 'json', beforeSend: function(xhr, settings) { @@ -590,13 +590,13 @@ function toggleConnection(elem, api_url) { return false; } $(".consoleport-toggle").click(function() { - return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/console-ports/"); + return toggleConnection($(this), "dcim/console-ports/"); }); $(".powerport-toggle").click(function() { - return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/power-ports/"); + return toggleConnection($(this), "dcim/power-ports/"); }); $(".interface-toggle").click(function() { - return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/interface-connections/"); + return toggleConnection($(this), "dcim/interface-connections/"); }); diff --git a/netbox/templates/dcim/device_bulk_add_component.html b/netbox/templates/dcim/device_bulk_add_component.html index 2854954cd..697942998 100644 --- a/netbox/templates/dcim/device_bulk_add_component.html +++ b/netbox/templates/dcim/device_bulk_add_component.html @@ -5,8 +5,8 @@

Add {{ component_name|title }}

{% csrf_token %} - {% if request.POST.redirect_url %} - + {% if request.POST.return_url %} + {% endif %} {% for field in form.hidden_fields %} {{ field }} @@ -51,7 +51,7 @@
- Cancel + Cancel
diff --git a/netbox/templates/dcim/device_component_add.html b/netbox/templates/dcim/device_component_add.html index 06b04a326..ab8f3bb21 100644 --- a/netbox/templates/dcim/device_component_add.html +++ b/netbox/templates/dcim/device_component_add.html @@ -34,7 +34,7 @@
- Cancel + Cancel
diff --git a/netbox/templates/dcim/device_import.html b/netbox/templates/dcim/device_import.html index c075abe34..c3915b9c3 100644 --- a/netbox/templates/dcim/device_import.html +++ b/netbox/templates/dcim/device_import.html @@ -13,7 +13,7 @@ {% render_form form %}
- Cancel + Cancel

CSV Format

diff --git a/netbox/templates/dcim/device_import_child.html b/netbox/templates/dcim/device_import_child.html index cf5d96e79..ca69d7aa5 100644 --- a/netbox/templates/dcim/device_import_child.html +++ b/netbox/templates/dcim/device_import_child.html @@ -13,7 +13,7 @@ {% render_form form %}
- Cancel + Cancel

CSV Format

diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html index 18bc7d627..7b0984a69 100644 --- a/netbox/templates/dcim/device_list.html +++ b/netbox/templates/dcim/device_list.html @@ -24,7 +24,31 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/filter_panel.html' %}
{% endblock %} + +{% block javascript %} + +{% endblock %} diff --git a/netbox/templates/dcim/devicebay_populate.html b/netbox/templates/dcim/devicebay_populate.html index a9d84c5e7..94a4ef0d0 100644 --- a/netbox/templates/dcim/devicebay_populate.html +++ b/netbox/templates/dcim/devicebay_populate.html @@ -37,7 +37,7 @@
- Cancel + Cancel
diff --git a/netbox/templates/dcim/devicetype_component_add.html b/netbox/templates/dcim/devicetype_component_add.html index d64dd4775..9645b7813 100644 --- a/netbox/templates/dcim/devicetype_component_add.html +++ b/netbox/templates/dcim/devicetype_component_add.html @@ -33,7 +33,7 @@
- Cancel + Cancel
diff --git a/netbox/templates/dcim/devicetype_list.html b/netbox/templates/dcim/devicetype_list.html index b8ce0e719..5ab97a481 100644 --- a/netbox/templates/dcim/devicetype_list.html +++ b/netbox/templates/dcim/devicetype_list.html @@ -19,7 +19,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/filter_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/inc/consoleport.html b/netbox/templates/dcim/inc/consoleport.html index ebe96a660..e6c816780 100644 --- a/netbox/templates/dcim/inc/consoleport.html +++ b/netbox/templates/dcim/inc/consoleport.html @@ -50,7 +50,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/consoleserverport.html b/netbox/templates/dcim/inc/consoleserverport.html index 848bb8f9d..d3c923e43 100644 --- a/netbox/templates/dcim/inc/consoleserverport.html +++ b/netbox/templates/dcim/inc/consoleserverport.html @@ -49,7 +49,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/device_table.html b/netbox/templates/dcim/inc/device_table.html index b11527748..33f7e93aa 100644 --- a/netbox/templates/dcim/inc/device_table.html +++ b/netbox/templates/dcim/inc/device_table.html @@ -7,12 +7,12 @@ Add Components {% endif %} diff --git a/netbox/templates/dcim/inc/devicebay.html b/netbox/templates/dcim/inc/devicebay.html index bc0934283..eacb27440 100644 --- a/netbox/templates/dcim/inc/devicebay.html +++ b/netbox/templates/dcim/inc/devicebay.html @@ -40,7 +40,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index d249b8b6e..bfb44b75d 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -85,7 +85,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html index 929c9c903..241ebc15c 100644 --- a/netbox/templates/dcim/inc/poweroutlet.html +++ b/netbox/templates/dcim/inc/poweroutlet.html @@ -49,7 +49,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/powerport.html b/netbox/templates/dcim/inc/powerport.html index c06d38b5a..aacb96839 100644 --- a/netbox/templates/dcim/inc/powerport.html +++ b/netbox/templates/dcim/inc/powerport.html @@ -50,7 +50,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/interface_connections_list.html b/netbox/templates/dcim/interface_connections_list.html index 56011af1d..23c7b8a9a 100644 --- a/netbox/templates/dcim/interface_connections_list.html +++ b/netbox/templates/dcim/interface_connections_list.html @@ -19,7 +19,7 @@ {% render_table table 'table.html' %}
- {% include 'inc/filter_panel.html' %} + {% include 'inc/search_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/interfaceconnection_edit.html b/netbox/templates/dcim/interfaceconnection_edit.html index 5da19d4fa..ea30ad006 100644 --- a/netbox/templates/dcim/interfaceconnection_edit.html +++ b/netbox/templates/dcim/interfaceconnection_edit.html @@ -86,7 +86,7 @@
- Cancel + Cancel
diff --git a/netbox/templates/dcim/ipaddress_assign.html b/netbox/templates/dcim/ipaddress_assign.html index 533317baf..3312a39c3 100644 --- a/netbox/templates/dcim/ipaddress_assign.html +++ b/netbox/templates/dcim/ipaddress_assign.html @@ -53,7 +53,7 @@
- Cancel + Cancel
diff --git a/netbox/templates/dcim/power_connections_list.html b/netbox/templates/dcim/power_connections_list.html index 55c9e2ce5..1b6528d86 100644 --- a/netbox/templates/dcim/power_connections_list.html +++ b/netbox/templates/dcim/power_connections_list.html @@ -19,7 +19,7 @@ {% render_table table 'table.html' %}
- {% include 'inc/filter_panel.html' %} + {% include 'inc/search_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/poweroutlet_connect.html b/netbox/templates/dcim/poweroutlet_connect.html index 4e6356cb1..a302722df 100644 --- a/netbox/templates/dcim/poweroutlet_connect.html +++ b/netbox/templates/dcim/poweroutlet_connect.html @@ -40,7 +40,7 @@
- Cancel + Cancel
diff --git a/netbox/templates/dcim/powerport_connect.html b/netbox/templates/dcim/powerport_connect.html index 1365eafb0..94e567e68 100644 --- a/netbox/templates/dcim/powerport_connect.html +++ b/netbox/templates/dcim/powerport_connect.html @@ -40,7 +40,7 @@
- Cancel + Cancel
diff --git a/netbox/templates/dcim/rack_import.html b/netbox/templates/dcim/rack_import.html index 807bff8eb..c462a0be9 100644 --- a/netbox/templates/dcim/rack_import.html +++ b/netbox/templates/dcim/rack_import.html @@ -13,7 +13,7 @@ {% render_form form %}
- Cancel + Cancel
diff --git a/netbox/templates/dcim/rack_list.html b/netbox/templates/dcim/rack_list.html index 10ee4ff04..fa5371f6f 100644 --- a/netbox/templates/dcim/rack_list.html +++ b/netbox/templates/dcim/rack_list.html @@ -24,7 +24,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/filter_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/rackgroup_list.html b/netbox/templates/dcim/rackgroup_list.html index 7b9b0677d..dee6472fb 100644 --- a/netbox/templates/dcim/rackgroup_list.html +++ b/netbox/templates/dcim/rackgroup_list.html @@ -18,7 +18,7 @@ {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackgroup_bulk_delete' %}
- {% include 'inc/filter_panel.html' %} + {% include 'inc/search_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/site_import.html b/netbox/templates/dcim/site_import.html index 7f58e6396..3018cc2f1 100644 --- a/netbox/templates/dcim/site_import.html +++ b/netbox/templates/dcim/site_import.html @@ -13,7 +13,7 @@ {% render_form form %}
- Cancel + Cancel
diff --git a/netbox/templates/dcim/site_list.html b/netbox/templates/dcim/site_list.html index 45169afe6..895f90804 100644 --- a/netbox/templates/dcim/site_list.html +++ b/netbox/templates/dcim/site_list.html @@ -23,7 +23,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/filter_panel.html' %}
{% endblock %} diff --git a/netbox/templates/inc/filter_panel.html b/netbox/templates/inc/filter_panel.html deleted file mode 100644 index cde76a21c..000000000 --- a/netbox/templates/inc/filter_panel.html +++ /dev/null @@ -1,32 +0,0 @@ -{% load form_helpers %} - -{% if filter_form %} -
-
- - Filter -
-
-
- {% for field in filter_form %} -
- {% if field|widget_type == 'checkboxinput' %} - - {% else %} - {{ field.label_tag }} - {{ field }} - {% endif %} -
- {% endfor %} -
- - - Clear - -
-
-
-
-{% endif %} diff --git a/netbox/templates/inc/search_panel.html b/netbox/templates/inc/search_panel.html index 692ef3fd2..c49b60ac4 100644 --- a/netbox/templates/inc/search_panel.html +++ b/netbox/templates/inc/search_panel.html @@ -1,18 +1,39 @@ +{% load form_helpers %} +
Search
-
-
- - + + {% for field in filter_form %} +
+ {% if field.name == "q" %} +
+ + + + +
+ {% elif field|widget_type == 'checkboxinput' %} + + {% else %} + {{ field.label_tag }} + {{ field }} + {% endif %} +
+ {% endfor %} +
- -
+ + Clear + +
diff --git a/netbox/templates/ipam/aggregate_import.html b/netbox/templates/ipam/aggregate_import.html index 51a0f95c2..8075b4874 100644 --- a/netbox/templates/ipam/aggregate_import.html +++ b/netbox/templates/ipam/aggregate_import.html @@ -13,7 +13,7 @@ {% render_form form %}
- Cancel + Cancel
diff --git a/netbox/templates/ipam/aggregate_list.html b/netbox/templates/ipam/aggregate_list.html index aef7d84c1..f43274876 100644 --- a/netbox/templates/ipam/aggregate_list.html +++ b/netbox/templates/ipam/aggregate_list.html @@ -27,7 +27,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/filter_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/ipaddress_assign.html b/netbox/templates/ipam/ipaddress_assign.html index 10a582a2f..4b27eae0b 100644 --- a/netbox/templates/ipam/ipaddress_assign.html +++ b/netbox/templates/ipam/ipaddress_assign.html @@ -59,7 +59,7 @@
- Cancel + Cancel
diff --git a/netbox/templates/ipam/ipaddress_import.html b/netbox/templates/ipam/ipaddress_import.html index ad62b44df..3c01b4af0 100644 --- a/netbox/templates/ipam/ipaddress_import.html +++ b/netbox/templates/ipam/ipaddress_import.html @@ -13,7 +13,7 @@ {% render_form form %}
- Cancel + Cancel
diff --git a/netbox/templates/ipam/ipaddress_list.html b/netbox/templates/ipam/ipaddress_list.html index 52391a2b4..ac1d980a2 100644 --- a/netbox/templates/ipam/ipaddress_list.html +++ b/netbox/templates/ipam/ipaddress_list.html @@ -1,5 +1,4 @@ {% extends '_base.html' %} -{% load render_table from django_tables2 %} {% load helpers %} {% block title %}IP Addresses{% endblock %} @@ -25,7 +24,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/filter_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/prefix_import.html b/netbox/templates/ipam/prefix_import.html index c42958cf4..0a9cc8694 100644 --- a/netbox/templates/ipam/prefix_import.html +++ b/netbox/templates/ipam/prefix_import.html @@ -13,7 +13,7 @@ {% render_form form %}
- Cancel + Cancel
diff --git a/netbox/templates/ipam/prefix_list.html b/netbox/templates/ipam/prefix_list.html index df790f9c6..10631ae27 100644 --- a/netbox/templates/ipam/prefix_list.html +++ b/netbox/templates/ipam/prefix_list.html @@ -34,7 +34,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/filter_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/rir_list.html b/netbox/templates/ipam/rir_list.html index 4d71431a2..33bef6d99 100644 --- a/netbox/templates/ipam/rir_list.html +++ b/netbox/templates/ipam/rir_list.html @@ -33,7 +33,7 @@ {% endif %}
- {% include 'inc/filter_panel.html' %} + {% include 'inc/search_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/vlan_import.html b/netbox/templates/ipam/vlan_import.html index 2ba22feb7..16456ba01 100644 --- a/netbox/templates/ipam/vlan_import.html +++ b/netbox/templates/ipam/vlan_import.html @@ -13,7 +13,7 @@ {% render_form form %}
- Cancel + Cancel
diff --git a/netbox/templates/ipam/vlan_list.html b/netbox/templates/ipam/vlan_list.html index 78ad140ff..2db914721 100644 --- a/netbox/templates/ipam/vlan_list.html +++ b/netbox/templates/ipam/vlan_list.html @@ -25,7 +25,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/filter_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/vlangroup_list.html b/netbox/templates/ipam/vlangroup_list.html index 1c8f92387..b6e1d4579 100644 --- a/netbox/templates/ipam/vlangroup_list.html +++ b/netbox/templates/ipam/vlangroup_list.html @@ -18,7 +18,7 @@ {% include 'utilities/obj_table.html' with bulk_delete_url='ipam:vlangroup_bulk_delete' %}
- {% include 'inc/filter_panel.html' %} + {% include 'inc/search_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/vrf_import.html b/netbox/templates/ipam/vrf_import.html index cbdee420d..9953542d2 100644 --- a/netbox/templates/ipam/vrf_import.html +++ b/netbox/templates/ipam/vrf_import.html @@ -13,7 +13,7 @@ {% render_form form %}
- Cancel + Cancel
diff --git a/netbox/templates/ipam/vrf_list.html b/netbox/templates/ipam/vrf_list.html index e5506bfaa..12f0b6bc3 100644 --- a/netbox/templates/ipam/vrf_list.html +++ b/netbox/templates/ipam/vrf_list.html @@ -25,7 +25,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/filter_panel.html' %}
{% endblock %} diff --git a/netbox/templates/secrets/secret_edit.html b/netbox/templates/secrets/secret_edit.html index c2426391f..a83024fb4 100644 --- a/netbox/templates/secrets/secret_edit.html +++ b/netbox/templates/secrets/secret_edit.html @@ -59,7 +59,7 @@ {% else %} - Cancel + Cancel {% endif %} diff --git a/netbox/templates/secrets/secret_import.html b/netbox/templates/secrets/secret_import.html index 9c7f67640..0a9a11c69 100644 --- a/netbox/templates/secrets/secret_import.html +++ b/netbox/templates/secrets/secret_import.html @@ -22,7 +22,7 @@ {% render_form form %}
- Cancel + Cancel
diff --git a/netbox/templates/secrets/secret_list.html b/netbox/templates/secrets/secret_list.html index 70bdcf1f1..29657d3f3 100644 --- a/netbox/templates/secrets/secret_list.html +++ b/netbox/templates/secrets/secret_list.html @@ -19,7 +19,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/filter_panel.html' %}
{% endblock %} diff --git a/netbox/templates/tenancy/tenant_import.html b/netbox/templates/tenancy/tenant_import.html index eb0c62c99..81f82989f 100644 --- a/netbox/templates/tenancy/tenant_import.html +++ b/netbox/templates/tenancy/tenant_import.html @@ -13,7 +13,7 @@ {% render_form form %}
- Cancel + Cancel
diff --git a/netbox/templates/tenancy/tenant_list.html b/netbox/templates/tenancy/tenant_list.html index 529f01c76..81173b368 100644 --- a/netbox/templates/tenancy/tenant_list.html +++ b/netbox/templates/tenancy/tenant_list.html @@ -24,7 +24,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/filter_panel.html' %}
{% endblock %} diff --git a/netbox/templates/utilities/bulk_edit_form.html b/netbox/templates/utilities/bulk_edit_form.html index 24a1b1077..3e3bbc187 100644 --- a/netbox/templates/utilities/bulk_edit_form.html +++ b/netbox/templates/utilities/bulk_edit_form.html @@ -5,8 +5,8 @@

{% block title %}{% endblock %}

{% csrf_token %} - {% if request.POST.redirect_url %} - + {% if request.POST.return_url %} + {% endif %} {% for field in form.hidden_fields %} {{ field }} @@ -44,7 +44,7 @@
- Cancel + Cancel
diff --git a/netbox/templates/utilities/confirm_bulk_delete.html b/netbox/templates/utilities/confirm_bulk_delete.html index 97b9cd277..6f3100cba 100644 --- a/netbox/templates/utilities/confirm_bulk_delete.html +++ b/netbox/templates/utilities/confirm_bulk_delete.html @@ -5,7 +5,7 @@ {% block message %}

- Are you sure you want to delete these {{ obj_type_plural|default:"objects" }}{% if parent_obj %} from {{ parent_obj }}{% endif %}? + Are you sure you want to delete these {{ selected_objects|length }} {{ obj_type_plural|default:"objects" }}{% if parent_obj %} from {{ parent_obj }}{% endif %}?