Merge pull request #849 from digitalocean/develop

Release v1.8.3
This commit is contained in:
Jeremy Stretch 2017-01-26 13:58:52 -05:00 committed by GitHub
commit c90cecc2fb
99 changed files with 662 additions and 459 deletions

2
.gitignore vendored
View File

@ -1,8 +1,10 @@
*.pyc *.pyc
/netbox/netbox/configuration.py /netbox/netbox/configuration.py
/netbox/netbox/ldap_config.py
/netbox/static /netbox/static
.idea .idea
/*.sh /*.sh
!upgrade.sh !upgrade.sh
fabfile.py fabfile.py
*.swp *.swp
gunicorn_config.py

View File

@ -9,6 +9,9 @@ env:
language: python language: python
python: python:
- "2.7" - "2.7"
- "3.4"
- "3.5"
- "3.6"
install: install:
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install pep8 - pip install pep8

View File

@ -2,12 +2,29 @@
**Debian/Ubuntu** **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 ```no-highlight
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev # apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
``` ```
**CentOS/RHEL** **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 ```no-highlight
# yum install -y epel-release # yum install -y epel-release
# yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel # yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel

View File

@ -0,0 +1 @@
default_app_config = 'circuits.apps.CircuitsConfig'

9
netbox/circuits/apps.py Normal file
View File

@ -0,0 +1,9 @@
from django.apps import AppConfig
class CircuitsConfig(AppConfig):
name = "circuits"
verbose_name = "Circuits"
def ready(self):
import circuits.signals

View File

@ -62,6 +62,7 @@ class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Provider model = Provider
q = forms.CharField(required=False, label='Search')
site = FilterChoiceField(queryset=Site.objects.all(), to_field_name='slug') site = FilterChoiceField(queryset=Site.objects.all(), to_field_name='slug')
@ -126,6 +127,7 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Circuit model = Circuit
q = forms.CharField(required=False, label='Search')
type = FilterChoiceField(queryset=CircuitType.objects.annotate(filter_count=Count('circuits')), type = FilterChoiceField(queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
to_field_name='slug') to_field_name='slug')
provider = FilterChoiceField(queryset=Provider.objects.annotate(filter_count=Count('circuits')), provider = FilterChoiceField(queryset=Provider.objects.annotate(filter_count=Count('circuits')),

View File

@ -1,6 +1,7 @@
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from dcim.fields import ASNField from dcim.fields import ASNField
from extras.models import CustomFieldModel, CustomFieldValue from extras.models import CustomFieldModel, CustomFieldValue
@ -33,6 +34,7 @@ def humanize_speed(speed):
return '{} Kbps'.format(speed) return '{} Kbps'.format(speed)
@python_2_unicode_compatible
class Provider(CreatedUpdatedModel, CustomFieldModel): class Provider(CreatedUpdatedModel, CustomFieldModel):
""" """
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model 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: class Meta:
ordering = ['name'] ordering = ['name']
def __unicode__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
@ -67,6 +69,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
]) ])
@python_2_unicode_compatible
class CircuitType(models.Model): class CircuitType(models.Model):
""" """
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named 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: class Meta:
ordering = ['name'] ordering = ['name']
def __unicode__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug) return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
@python_2_unicode_compatible
class Circuit(CreatedUpdatedModel, CustomFieldModel): class Circuit(CreatedUpdatedModel, CustomFieldModel):
""" """
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple 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'] ordering = ['provider', 'cid']
unique_together = ['provider', 'cid'] unique_together = ['provider', 'cid']
def __unicode__(self): def __str__(self):
return u'{} {}'.format(self.provider, self.cid) return u'{} {}'.format(self.provider, self.cid)
def get_absolute_url(self): def get_absolute_url(self):
@ -141,6 +145,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
commit_rate_human.admin_order_field = 'commit_rate' commit_rate_human.admin_order_field = 'commit_rate'
@python_2_unicode_compatible
class CircuitTermination(models.Model): class CircuitTermination(models.Model):
circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE) 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') 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'] ordering = ['circuit', 'term_side']
unique_together = ['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()) return u'{} (Side {})'.format(self.circuit, self.get_term_side_display())
def get_peer_termination(self): def get_peer_termination(self):

View File

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

View File

@ -25,7 +25,6 @@ class ProviderListView(ObjectListView):
filter = filters.ProviderFilter filter = filters.ProviderFilter
filter_form = forms.ProviderFilterForm filter_form = forms.ProviderFilterForm
table = tables.ProviderTable table = tables.ProviderTable
edit_permissions = ['circuits.change_provider', 'circuits.delete_provider']
template_name = 'circuits/provider_list.html' template_name = 'circuits/provider_list.html'
@ -47,7 +46,7 @@ class ProviderEditView(PermissionRequiredMixin, ObjectEditView):
model = Provider model = Provider
form_class = forms.ProviderForm form_class = forms.ProviderForm
template_name = 'circuits/provider_edit.html' template_name = 'circuits/provider_edit.html'
obj_list_url = 'circuits:provider_list' default_return_url = 'circuits:provider_list'
class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView): class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@ -61,21 +60,23 @@ class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.ProviderImportForm form = forms.ProviderImportForm
table = tables.ProviderTable table = tables.ProviderTable
template_name = 'circuits/provider_import.html' template_name = 'circuits/provider_import.html'
obj_list_url = 'circuits:provider_list' default_return_url = 'circuits:provider_list'
class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView): class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'circuits.change_provider' permission_required = 'circuits.change_provider'
cls = Provider cls = Provider
filter = filters.ProviderFilter
form = forms.ProviderBulkEditForm form = forms.ProviderBulkEditForm
template_name = 'circuits/provider_bulk_edit.html' template_name = 'circuits/provider_bulk_edit.html'
default_redirect_url = 'circuits:provider_list' default_return_url = 'circuits:provider_list'
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_provider' permission_required = 'circuits.delete_provider'
cls = 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): class CircuitTypeListView(ObjectListView):
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')) queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
table = tables.CircuitTypeTable table = tables.CircuitTypeTable
edit_permissions = ['circuits.change_circuittype', 'circuits.delete_circuittype']
template_name = 'circuits/circuittype_list.html' template_name = 'circuits/circuittype_list.html'
@ -101,7 +101,7 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuittype' permission_required = 'circuits.delete_circuittype'
cls = 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 = filters.CircuitFilter
filter_form = forms.CircuitFilterForm filter_form = forms.CircuitFilterForm
table = tables.CircuitTable table = tables.CircuitTable
edit_permissions = ['circuits.change_circuit', 'circuits.delete_circuit']
template_name = 'circuits/circuit_list.html' template_name = 'circuits/circuit_list.html'
@ -136,7 +135,7 @@ class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
form_class = forms.CircuitForm form_class = forms.CircuitForm
fields_initial = ['provider'] fields_initial = ['provider']
template_name = 'circuits/circuit_edit.html' template_name = 'circuits/circuit_edit.html'
obj_list_url = 'circuits:circuit_list' default_return_url = 'circuits:circuit_list'
class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView): class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@ -150,21 +149,23 @@ class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.CircuitImportForm form = forms.CircuitImportForm
table = tables.CircuitTable table = tables.CircuitTable
template_name = 'circuits/circuit_import.html' template_name = 'circuits/circuit_import.html'
obj_list_url = 'circuits:circuit_list' default_return_url = 'circuits:circuit_list'
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView): class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'circuits.change_circuit' permission_required = 'circuits.change_circuit'
cls = Circuit cls = Circuit
filter = filters.CircuitFilter
form = forms.CircuitBulkEditForm form = forms.CircuitBulkEditForm
template_name = 'circuits/circuit_bulk_edit.html' template_name = 'circuits/circuit_bulk_edit.html'
default_redirect_url = 'circuits:circuit_list' default_return_url = 'circuits:circuit_list'
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuit' permission_required = 'circuits.delete_circuit'
cls = Circuit cls = Circuit
default_redirect_url = 'circuits:circuit_list' filter = filters.CircuitFilter
default_return_url = 'circuits:circuit_list'
@permission_required('circuits.change_circuittermination') @permission_required('circuits.change_circuittermination')
@ -208,7 +209,7 @@ def circuit_terminations_swap(request, pk):
'form': form, 'form': form,
'panel_class': 'default', 'panel_class': 'default',
'button_class': 'primary', 'button_class': 'primary',
'cancel_url': circuit.get_absolute_url(), 'return_url': circuit.get_absolute_url(),
}) })

View File

@ -134,12 +134,13 @@ class ManufacturerNestedSerializer(ManufacturerSerializer):
class DeviceTypeSerializer(CustomFieldSerializer, serializers.ModelSerializer): class DeviceTypeSerializer(CustomFieldSerializer, serializers.ModelSerializer):
manufacturer = ManufacturerNestedSerializer() manufacturer = ManufacturerNestedSerializer()
subdevice_role = serializers.SerializerMethodField() subdevice_role = serializers.SerializerMethodField()
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
class Meta: class Meta:
model = DeviceType model = DeviceType
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', '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): def get_subdevice_role(self, obj):
return { return {

View File

@ -331,7 +331,8 @@ class InterfaceListView(generics.ListAPIView):
def get_queryset(self): def get_queryset(self):
device = get_object_or_404(Device, pk=self.kwargs['pk']) 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) # Filter by type (physical or virtual)
iface_type = self.request.query_params.get('type') iface_type = self.request.query_params.get('type')
@ -489,8 +490,8 @@ class RelatedConnectionsView(APIView):
response['power-ports'].append(data) response['power-ports'].append(data)
# Interface connections # Interface connections
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)\
'circuit_termination') .select_related('connected_as_a', 'connected_as_b', 'circuit_termination')
for iface in interfaces: for iface in interfaces:
data = serializers.InterfaceDetailSerializer(instance=iface).data data = serializers.InterfaceDetailSerializer(instance=iface).data
del(data['device']) del(data['device'])

View File

@ -13,7 +13,7 @@ from utilities.forms import (
SlugField, SlugField,
) )
from formfields import MACAddressFormField from .formfields import MACAddressFormField
from .models import ( from .models import (
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
@ -101,6 +101,7 @@ class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Site model = Site
q = forms.CharField(required=False, label='Search')
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('sites')), to_field_name='slug', tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('sites')), to_field_name='slug',
null_option=(0, 'None')) null_option=(0, 'None'))
@ -232,6 +233,7 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Rack model = Rack
q = forms.CharField(required=False, label='Search')
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks')), to_field_name='slug') site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks')), to_field_name='slug')
group_id = FilterChoiceField(queryset=RackGroup.objects.select_related('site') group_id = FilterChoiceField(queryset=RackGroup.objects.select_related('site')
.annotate(filter_count=Count('racks')), label='Rack group', null_option=(0, 'None')) .annotate(filter_count=Count('racks')), label='Rack group', null_option=(0, 'None'))
@ -281,6 +283,7 @@ class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm): class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = DeviceType model = DeviceType
q = forms.CharField(required=False, label='Search')
manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')), manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
to_field_name='slug') to_field_name='slug')
@ -639,18 +642,46 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Device model = Device
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')), to_field_name='slug') q = forms.CharField(required=False, label='Search')
rack_group_id = FilterChoiceField(queryset=RackGroup.objects.annotate(filter_count=Count('racks__devices')), site = FilterChoiceField(
label='Rack Group') queryset=Site.objects.annotate(filter_count=Count('racks__devices')),
role = FilterChoiceField(queryset=DeviceRole.objects.annotate(filter_count=Count('devices')), to_field_name='slug') to_field_name='slug',
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug', )
null_option=(0, 'None')) rack_group_id = FilterChoiceField(
device_type_id = FilterChoiceField(queryset=DeviceType.objects.select_related('manufacturer') queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__devices')),
.annotate(filter_count=Count('instances')), label='Type') label='Rack Group',
platform = FilterChoiceField(queryset=Platform.objects.annotate(filter_count=Count('devices')), )
to_field_name='slug', null_option=(0, 'None')) role = FilterChoiceField(
status = forms.NullBooleanField(required=False, widget=forms.Select(choices=FORM_STATUS_CHOICES)) queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
mac_address = forms.CharField(required=False, label='MAC address') 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',
)
# #

View File

@ -8,6 +8,7 @@ from django.core.urlresolvers import reverse
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Count, Q, ObjectDoesNotExist from django.db.models import Count, Q, ObjectDoesNotExist
from django.utils.encoding import python_2_unicode_compatible
from circuits.models import Circuit from circuits.models import Circuit
from extras.models import CustomFieldModel, CustomField, CustomFieldValue from extras.models import CustomFieldModel, CustomField, CustomFieldValue
@ -199,6 +200,7 @@ class SiteManager(NaturalOrderByManager):
return self.natural_order_by('name') return self.natural_order_by('name')
@python_2_unicode_compatible
class Site(CreatedUpdatedModel, CustomFieldModel): class Site(CreatedUpdatedModel, CustomFieldModel):
""" """
A Site represents a geographic location within a network; typically a building or campus. The optional facility 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: class Meta:
ordering = ['name'] ordering = ['name']
def __unicode__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
@ -265,6 +267,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
# Racks # Racks
# #
@python_2_unicode_compatible
class RackGroup(models.Model): 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 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'], ['site', 'slug'],
] ]
def __unicode__(self): def __str__(self):
return u'{} - {}'.format(self.site.name, self.name) return u'{} - {}'.format(self.site.name, self.name)
def get_absolute_url(self): def get_absolute_url(self):
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk) return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
@python_2_unicode_compatible
class RackRole(models.Model): class RackRole(models.Model):
""" """
Racks can be organized by functional role, similar to Devices. Racks can be organized by functional role, similar to Devices.
@ -300,7 +304,7 @@ class RackRole(models.Model):
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
def __unicode__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
@ -313,6 +317,7 @@ class RackManager(NaturalOrderByManager):
return self.natural_order_by('site__name', 'name') return self.natural_order_by('site__name', 'name')
@python_2_unicode_compatible
class Rack(CreatedUpdatedModel, CustomFieldModel): 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. 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'], ['site', 'facility_id'],
] ]
def __unicode__(self): def __str__(self):
return self.display_name return self.display_name
def get_absolute_url(self): 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) devices = self.devices.select_related('device_type').filter(position__gte=1).exclude(pk__in=exclude)
# Initialize the rack unit skeleton # 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 # Remove units consumed by installed devices
for d in devices: for d in devices:
@ -477,6 +482,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
# Device Types # Device Types
# #
@python_2_unicode_compatible
class Manufacturer(models.Model): class Manufacturer(models.Model):
""" """
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell. A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
@ -487,13 +493,14 @@ class Manufacturer(models.Model):
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
def __unicode__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug) return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
@python_2_unicode_compatible
class DeviceType(models.Model, CustomFieldModel): class DeviceType(models.Model, CustomFieldModel):
""" """
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as 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'], ['manufacturer', 'slug'],
] ]
def __unicode__(self): def __str__(self):
return self.model return self.model
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -608,6 +615,7 @@ class DeviceType(models.Model, CustomFieldModel):
return bool(self.subdevice_role is False) return bool(self.subdevice_role is False)
@python_2_unicode_compatible
class ConsolePortTemplate(models.Model): class ConsolePortTemplate(models.Model):
""" """
A template for a ConsolePort to be created for a new Device. A template for a ConsolePort to be created for a new Device.
@ -619,10 +627,11 @@ class ConsolePortTemplate(models.Model):
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
unique_together = ['device_type', 'name'] unique_together = ['device_type', 'name']
def __unicode__(self): def __str__(self):
return self.name return self.name
@python_2_unicode_compatible
class ConsoleServerPortTemplate(models.Model): class ConsoleServerPortTemplate(models.Model):
""" """
A template for a ConsoleServerPort to be created for a new Device. A template for a ConsoleServerPort to be created for a new Device.
@ -634,10 +643,11 @@ class ConsoleServerPortTemplate(models.Model):
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
unique_together = ['device_type', 'name'] unique_together = ['device_type', 'name']
def __unicode__(self): def __str__(self):
return self.name return self.name
@python_2_unicode_compatible
class PowerPortTemplate(models.Model): class PowerPortTemplate(models.Model):
""" """
A template for a PowerPort to be created for a new Device. A template for a PowerPort to be created for a new Device.
@ -649,10 +659,11 @@ class PowerPortTemplate(models.Model):
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
unique_together = ['device_type', 'name'] unique_together = ['device_type', 'name']
def __unicode__(self): def __str__(self):
return self.name return self.name
@python_2_unicode_compatible
class PowerOutletTemplate(models.Model): class PowerOutletTemplate(models.Model):
""" """
A template for a PowerOutlet to be created for a new Device. A template for a PowerOutlet to be created for a new Device.
@ -664,7 +675,7 @@ class PowerOutletTemplate(models.Model):
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
unique_together = ['device_type', 'name'] unique_together = ['device_type', 'name']
def __unicode__(self): def __str__(self):
return self.name return self.name
@ -706,6 +717,7 @@ class InterfaceManager(models.Manager):
}).order_by(*ordering) }).order_by(*ordering)
@python_2_unicode_compatible
class InterfaceTemplate(models.Model): class InterfaceTemplate(models.Model):
""" """
A template for a physical data interface on a new Device. A template for a physical data interface on a new Device.
@ -721,10 +733,11 @@ class InterfaceTemplate(models.Model):
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
unique_together = ['device_type', 'name'] unique_together = ['device_type', 'name']
def __unicode__(self): def __str__(self):
return self.name return self.name
@python_2_unicode_compatible
class DeviceBayTemplate(models.Model): class DeviceBayTemplate(models.Model):
""" """
A template for a DeviceBay to be created for a new parent Device. 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'] ordering = ['device_type', 'name']
unique_together = ['device_type', 'name'] unique_together = ['device_type', 'name']
def __unicode__(self): def __str__(self):
return self.name return self.name
@ -744,6 +757,7 @@ class DeviceBayTemplate(models.Model):
# Devices # Devices
# #
@python_2_unicode_compatible
class DeviceRole(models.Model): class DeviceRole(models.Model):
""" """
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a 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: class Meta:
ordering = ['name'] ordering = ['name']
def __unicode__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?role={}".format(reverse('dcim:device_list'), self.slug) return "{}?role={}".format(reverse('dcim:device_list'), self.slug)
@python_2_unicode_compatible
class Platform(models.Model): class Platform(models.Model):
""" """
Platform refers to the software or firmware running on a Device; for example, "Cisco IOS-XR" or "Juniper Junos". 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: class Meta:
ordering = ['name'] ordering = ['name']
def __unicode__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
@ -789,6 +804,7 @@ class DeviceManager(NaturalOrderByManager):
return self.natural_order_by('name') return self.natural_order_by('name')
@python_2_unicode_compatible
class Device(CreatedUpdatedModel, CustomFieldModel): class Device(CreatedUpdatedModel, CustomFieldModel):
""" """
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType, 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'] ordering = ['name']
unique_together = ['rack', 'position', 'face'] unique_together = ['rack', 'position', 'face']
def __unicode__(self): def __str__(self):
return self.display_name return self.display_name
def get_absolute_url(self): def get_absolute_url(self):
@ -968,6 +984,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
return RPC_CLIENTS.get(self.platform.rpc_client) return RPC_CLIENTS.get(self.platform.rpc_client)
@python_2_unicode_compatible
class ConsolePort(models.Model): class ConsolePort(models.Model):
""" """
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
@ -982,7 +999,7 @@ class ConsolePort(models.Model):
ordering = ['device', 'name'] ordering = ['device', 'name']
unique_together = ['device', 'name'] unique_together = ['device', 'name']
def __unicode__(self): def __str__(self):
return self.name return self.name
# Used for connections export # Used for connections export
@ -1011,6 +1028,7 @@ class ConsoleServerPortManager(models.Manager):
}).order_by('device', 'name_as_integer') }).order_by('device', 'name_as_integer')
@python_2_unicode_compatible
class ConsoleServerPort(models.Model): class ConsoleServerPort(models.Model):
""" """
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. 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: class Meta:
unique_together = ['device', 'name'] unique_together = ['device', 'name']
def __unicode__(self): def __str__(self):
return self.name return self.name
@python_2_unicode_compatible
class PowerPort(models.Model): class PowerPort(models.Model):
""" """
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
@ -1041,7 +1060,7 @@ class PowerPort(models.Model):
ordering = ['device', 'name'] ordering = ['device', 'name']
unique_together = ['device', 'name'] unique_together = ['device', 'name']
def __unicode__(self): def __str__(self):
return self.name return self.name
# Used for connections export # Used for connections export
@ -1064,6 +1083,7 @@ class PowerOutletManager(models.Manager):
}).order_by('device', 'name_padded') }).order_by('device', 'name_padded')
@python_2_unicode_compatible
class PowerOutlet(models.Model): class PowerOutlet(models.Model):
""" """
A physical power outlet (output) within a Device which provides power to a PowerPort. A physical power outlet (output) within a Device which provides power to a PowerPort.
@ -1076,10 +1096,11 @@ class PowerOutlet(models.Model):
class Meta: class Meta:
unique_together = ['device', 'name'] unique_together = ['device', 'name']
def __unicode__(self): def __str__(self):
return self.name return self.name
@python_2_unicode_compatible
class Interface(models.Model): class Interface(models.Model):
""" """
A physical data interface within a Device. An Interface can connect to exactly one other Interface via the creation 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'] ordering = ['device', 'name']
unique_together = ['device', 'name'] unique_together = ['device', 'name']
def __unicode__(self): def __str__(self):
return self.name return self.name
def clean(self): def clean(self):
@ -1176,6 +1197,7 @@ class InterfaceConnection(models.Model):
]) ])
@python_2_unicode_compatible
class DeviceBay(models.Model): class DeviceBay(models.Model):
""" """
An empty space within a Device which can house a child device An empty space within a Device which can house a child device
@ -1189,7 +1211,7 @@ class DeviceBay(models.Model):
ordering = ['device', 'name'] ordering = ['device', 'name']
unique_together = ['device', 'name'] unique_together = ['device', 'name']
def __unicode__(self): def __str__(self):
return u'{} - {}'.format(self.device.name, self.name) return u'{} - {}'.format(self.device.name, self.name)
def clean(self): def clean(self):
@ -1205,6 +1227,7 @@ class DeviceBay(models.Model):
raise ValidationError("Cannot install a device into itself.") raise ValidationError("Cannot install a device into itself.")
@python_2_unicode_compatible
class Module(models.Model): 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 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'] ordering = ['device__id', 'parent__id', 'name']
unique_together = ['device', 'parent', 'name'] unique_together = ['device', 'parent', 'name']
def __unicode__(self): def __str__(self):
return self.name return self.name

View File

@ -65,7 +65,7 @@ class SiteTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/sites/'.format(settings.BASE_PATH)): def test_get_list(self, endpoint='/{}api/dcim/sites/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
for i in content: for i in content:
self.assertEqual( self.assertEqual(
@ -75,7 +75,7 @@ class SiteTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/sites/1/'.format(settings.BASE_PATH)): def test_get_detail(self, endpoint='/{}api/dcim/sites/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
sorted(content.keys()), 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)): def test_get_site_list_rack(self, endpoint='/{}api/dcim/sites/1/racks/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(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( self.assertEqual(
sorted(i.keys()), sorted(i.keys()),
sorted(self.rack_fields), 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)): def test_get_site_list_graphs(self, endpoint='/{}api/dcim/sites/1/graphs/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(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( self.assertEqual(
sorted(i.keys()), sorted(i.keys()),
sorted(self.graph_fields), sorted(self.graph_fields),
@ -159,7 +159,7 @@ class RackTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/racks/'.format(settings.BASE_PATH)): def test_get_list(self, endpoint='/{}api/dcim/racks/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
for i in content: for i in content:
self.assertEqual( self.assertEqual(
@ -173,7 +173,7 @@ class RackTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/racks/1/'.format(settings.BASE_PATH)): def test_get_detail(self, endpoint='/{}api/dcim/racks/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
sorted(content.keys()), sorted(content.keys()),
@ -202,7 +202,7 @@ class ManufacturersTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/manufacturers/'.format(settings.BASE_PATH)): def test_get_list(self, endpoint='/{}api/dcim/manufacturers/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
for i in content: for i in content:
self.assertEqual( self.assertEqual(
@ -212,7 +212,7 @@ class ManufacturersTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/manufacturers/1/'.format(settings.BASE_PATH)): def test_get_detail(self, endpoint='/{}api/dcim/manufacturers/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
sorted(content.keys()), sorted(content.keys()),
@ -239,6 +239,7 @@ class DeviceTypeTest(APITestCase):
'subdevice_role', 'subdevice_role',
'comments', 'comments',
'custom_fields', 'custom_fields',
'instance_count',
] ]
nested_fields = [ nested_fields = [
@ -250,7 +251,7 @@ class DeviceTypeTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/device-types/'.format(settings.BASE_PATH)): def test_get_list(self, endpoint='/{}api/dcim/device-types/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
for i in content: for i in content:
self.assertEqual( self.assertEqual(
@ -261,7 +262,7 @@ class DeviceTypeTest(APITestCase):
def test_detail_list(self, endpoint='/{}api/dcim/device-types/1/'.format(settings.BASE_PATH)): def test_detail_list(self, endpoint='/{}api/dcim/device-types/1/'.format(settings.BASE_PATH)):
# TODO: details returns list view. # TODO: details returns list view.
# response = self.client.get(endpoint) # 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(response.status_code, status.HTTP_200_OK)
# self.assertEqual( # self.assertEqual(
# sorted(content.keys()), # sorted(content.keys()),
@ -284,7 +285,7 @@ class DeviceRolesTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/device-roles/'.format(settings.BASE_PATH)): def test_get_list(self, endpoint='/{}api/dcim/device-roles/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
for i in content: for i in content:
self.assertEqual( self.assertEqual(
@ -294,7 +295,7 @@ class DeviceRolesTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/device-roles/1/'.format(settings.BASE_PATH)): def test_get_detail(self, endpoint='/{}api/dcim/device-roles/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
sorted(content.keys()), sorted(content.keys()),
@ -312,7 +313,7 @@ class PlatformsTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/platforms/'.format(settings.BASE_PATH)): def test_get_list(self, endpoint='/{}api/dcim/platforms/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
for i in content: for i in content:
self.assertEqual( self.assertEqual(
@ -322,7 +323,7 @@ class PlatformsTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/platforms/1/'.format(settings.BASE_PATH)): def test_get_detail(self, endpoint='/{}api/dcim/platforms/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
sorted(content.keys()), sorted(content.keys()),
@ -360,7 +361,7 @@ class DeviceTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/devices/'.format(settings.BASE_PATH)): def test_get_list(self, endpoint='/{}api/dcim/devices/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
for device in content: for device in content:
self.assertEqual( self.assertEqual(
@ -425,7 +426,7 @@ class DeviceTest(APITestCase):
] ]
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
device = content[0] device = content[0]
self.assertEqual( self.assertEqual(
@ -435,7 +436,7 @@ class DeviceTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/devices/1/'.format(settings.BASE_PATH)): def test_get_detail(self, endpoint='/{}api/dcim/devices/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
sorted(content.keys()), 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)): def test_get_list(self, endpoint='/{}api/dcim/devices/9/console-server-ports/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
for console_port in content: for console_port in content:
self.assertEqual( 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)): def test_get_list(self, endpoint='/{}api/dcim/devices/1/console-ports/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
for console_port in content: for console_port in content:
self.assertEqual( self.assertEqual(
@ -493,7 +494,7 @@ class ConsolePortsTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/console-ports/1/'.format(settings.BASE_PATH)): def test_get_detail(self, endpoint='/{}api/dcim/console-ports/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
sorted(content.keys()), 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)): def test_get_list(self, endpoint='/{}api/dcim/devices/1/power-ports/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
for i in content: for i in content:
self.assertEqual( self.assertEqual(
@ -528,7 +529,7 @@ class PowerPortsTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/power-ports/1/'.format(settings.BASE_PATH)): def test_get_detail(self, endpoint='/{}api/dcim/power-ports/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
sorted(content.keys()), 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)): def test_get_list(self, endpoint='/{}api/dcim/devices/11/power-outlets/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
for i in content: for i in content:
self.assertEqual( self.assertEqual(
@ -599,7 +600,7 @@ class InterfaceTest(APITestCase):
def test_get_list(self, endpoint='/{}api/dcim/devices/1/interfaces/'.format(settings.BASE_PATH)): def test_get_list(self, endpoint='/{}api/dcim/devices/1/interfaces/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
for i in content: for i in content:
self.assertEqual( self.assertEqual(
@ -613,7 +614,7 @@ class InterfaceTest(APITestCase):
def test_get_detail(self, endpoint='/{}api/dcim/interfaces/1/'.format(settings.BASE_PATH)): def test_get_detail(self, endpoint='/{}api/dcim/interfaces/1/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
sorted(content.keys()), 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)): def test_get_graph_list(self, endpoint='/{}api/dcim/interfaces/1/graphs/'.format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
for i in content: for i in content:
self.assertEqual( self.assertEqual(
sorted(i.keys()), sorted(i.keys()),
sorted(SiteTest.graph_fields), sorted(SiteTest.graph_fields),
) )
def test_get_interface_connections(self, endpoint='/{}api/dcim/interface-connections/4/' def test_get_interface_connections(self, endpoint='/{}api/dcim/interface-connections/4/'
.format(settings.BASE_PATH)): .format(settings.BASE_PATH)):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
sorted(content.keys()), 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' def test_get_list(self, endpoint=('/{}api/dcim/related-connections/?peer-device=test1-edge1&peer-interface=xe-0/0/3'
.format(settings.BASE_PATH))): .format(settings.BASE_PATH))):
response = self.client.get(endpoint) 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(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
sorted(content.keys()), sorted(content.keys()),

View File

@ -71,7 +71,7 @@ class ComponentCreateView(View):
'parent': parent, 'parent': parent,
'component_type': self.model._meta.verbose_name, 'component_type': self.model._meta.verbose_name,
'form': self.form(initial=request.GET), 'form': self.form(initial=request.GET),
'cancel_url': parent.get_absolute_url(), 'return_url': parent.get_absolute_url(),
}) })
def post(self, request, pk): def post(self, request, pk):
@ -112,10 +112,22 @@ class ComponentCreateView(View):
'parent': parent, 'parent': parent,
'component_type': self.model._meta.verbose_name, 'component_type': self.model._meta.verbose_name,
'form': form, '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 # Sites
# #
@ -125,7 +137,6 @@ class SiteListView(ObjectListView):
filter = filters.SiteFilter filter = filters.SiteFilter
filter_form = forms.SiteFilterForm filter_form = forms.SiteFilterForm
table = tables.SiteTable table = tables.SiteTable
edit_permissions = ['dcim.change_rack', 'dcim.delete_rack']
template_name = 'dcim/site_list.html' template_name = 'dcim/site_list.html'
@ -157,7 +168,7 @@ class SiteEditView(PermissionRequiredMixin, ObjectEditView):
model = Site model = Site
form_class = forms.SiteForm form_class = forms.SiteForm
template_name = 'dcim/site_edit.html' template_name = 'dcim/site_edit.html'
obj_list_url = 'dcim:site_list' default_return_url = 'dcim:site_list'
class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView): class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@ -171,15 +182,16 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.SiteImportForm form = forms.SiteImportForm
table = tables.SiteTable table = tables.SiteTable
template_name = 'dcim/site_import.html' template_name = 'dcim/site_import.html'
obj_list_url = 'dcim:site_list' default_return_url = 'dcim:site_list'
class SiteBulkEditView(PermissionRequiredMixin, BulkEditView): class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_site' permission_required = 'dcim.change_site'
cls = Site cls = Site
filter = filters.SiteFilter
form = forms.SiteBulkEditForm form = forms.SiteBulkEditForm
template_name = 'dcim/site_bulk_edit.html' 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 = filters.RackGroupFilter
filter_form = forms.RackGroupFilterForm filter_form = forms.RackGroupFilterForm
table = tables.RackGroupTable table = tables.RackGroupTable
edit_permissions = ['dcim.change_rackgroup', 'dcim.delete_rackgroup']
template_name = 'dcim/rackgroup_list.html' template_name = 'dcim/rackgroup_list.html'
@ -207,7 +218,8 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackgroup' permission_required = 'dcim.delete_rackgroup'
cls = 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): class RackRoleListView(ObjectListView):
queryset = RackRole.objects.annotate(rack_count=Count('racks')) queryset = RackRole.objects.annotate(rack_count=Count('racks'))
table = tables.RackRoleTable table = tables.RackRoleTable
edit_permissions = ['dcim.change_rackrole', 'dcim.delete_rackrole']
template_name = 'dcim/rackrole_list.html' template_name = 'dcim/rackrole_list.html'
@ -233,7 +244,7 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackrole' permission_required = 'dcim.delete_rackrole'
cls = 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 = filters.RackFilter
filter_form = forms.RackFilterForm filter_form = forms.RackFilterForm
table = tables.RackTable table = tables.RackTable
edit_permissions = ['dcim.change_rack', 'dcim.delete_rack']
template_name = 'dcim/rack_list.html' template_name = 'dcim/rack_list.html'
@ -274,7 +284,7 @@ class RackEditView(PermissionRequiredMixin, ObjectEditView):
model = Rack model = Rack
form_class = forms.RackForm form_class = forms.RackForm
template_name = 'dcim/rack_edit.html' template_name = 'dcim/rack_edit.html'
obj_list_url = 'dcim:rack_list' default_return_url = 'dcim:rack_list'
class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView): class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@ -288,21 +298,23 @@ class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.RackImportForm form = forms.RackImportForm
table = tables.RackImportTable table = tables.RackImportTable
template_name = 'dcim/rack_import.html' template_name = 'dcim/rack_import.html'
obj_list_url = 'dcim:rack_list' default_return_url = 'dcim:rack_list'
class RackBulkEditView(PermissionRequiredMixin, BulkEditView): class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rack' permission_required = 'dcim.change_rack'
cls = Rack cls = Rack
filter = filters.RackFilter
form = forms.RackBulkEditForm form = forms.RackBulkEditForm
template_name = 'dcim/rack_bulk_edit.html' template_name = 'dcim/rack_bulk_edit.html'
default_redirect_url = 'dcim:rack_list' default_return_url = 'dcim:rack_list'
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rack' permission_required = 'dcim.delete_rack'
cls = 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): class ManufacturerListView(ObjectListView):
queryset = Manufacturer.objects.annotate(devicetype_count=Count('device_types')) queryset = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
table = tables.ManufacturerTable table = tables.ManufacturerTable
edit_permissions = ['dcim.change_manufacturer', 'dcim.delete_manufacturer']
template_name = 'dcim/manufacturer_list.html' template_name = 'dcim/manufacturer_list.html'
@ -328,7 +339,7 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_manufacturer' permission_required = 'dcim.delete_manufacturer'
cls = 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 = filters.DeviceTypeFilter
filter_form = forms.DeviceTypeFilterForm filter_form = forms.DeviceTypeFilterForm
table = tables.DeviceTypeTable table = tables.DeviceTypeTable
edit_permissions = ['dcim.change_devicetype', 'dcim.delete_devicetype']
template_name = 'dcim/devicetype_list.html' template_name = 'dcim/devicetype_list.html'
@ -398,7 +408,7 @@ class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView):
model = DeviceType model = DeviceType
form_class = forms.DeviceTypeForm form_class = forms.DeviceTypeForm
template_name = 'dcim/devicetype_edit.html' template_name = 'dcim/devicetype_edit.html'
obj_list_url = 'dcim:devicetype_list' default_return_url = 'dcim:devicetype_list'
class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView): class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@ -410,15 +420,17 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView): class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_devicetype' permission_required = 'dcim.change_devicetype'
cls = DeviceType cls = DeviceType
filter = filters.DeviceTypeFilter
form = forms.DeviceTypeBulkEditForm form = forms.DeviceTypeBulkEditForm
template_name = 'dcim/devicetype_bulk_edit.html' template_name = 'dcim/devicetype_bulk_edit.html'
default_redirect_url = 'dcim:devicetype_list' default_return_url = 'dcim:devicetype_list'
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicetype' permission_required = 'dcim.delete_devicetype'
cls = 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): class DeviceRoleListView(ObjectListView):
queryset = DeviceRole.objects.annotate(device_count=Count('devices')) queryset = DeviceRole.objects.annotate(device_count=Count('devices'))
table = tables.DeviceRoleTable table = tables.DeviceRoleTable
edit_permissions = ['dcim.change_devicerole', 'dcim.delete_devicerole']
template_name = 'dcim/devicerole_list.html' template_name = 'dcim/devicerole_list.html'
@ -548,7 +559,7 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicerole' permission_required = 'dcim.delete_devicerole'
cls = 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): class PlatformListView(ObjectListView):
queryset = Platform.objects.annotate(device_count=Count('devices')) queryset = Platform.objects.annotate(device_count=Count('devices'))
table = tables.PlatformTable table = tables.PlatformTable
edit_permissions = ['dcim.change_platform', 'dcim.delete_platform']
template_name = 'dcim/platform_list.html' template_name = 'dcim/platform_list.html'
@ -574,7 +584,7 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_platform' permission_required = 'dcim.delete_platform'
cls = 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 = filters.DeviceFilter
filter_form = forms.DeviceFilterForm filter_form = forms.DeviceFilterForm
table = tables.DeviceTable table = tables.DeviceTable
edit_permissions = ['dcim.change_device', 'dcim.delete_device']
template_name = 'dcim/device_list.html' template_name = 'dcim/device_list.html'
@ -666,7 +675,7 @@ class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
form_class = forms.DeviceForm form_class = forms.DeviceForm
fields_initial = ['site', 'rack', 'position', 'face', 'device_bay'] fields_initial = ['site', 'rack', 'position', 'face', 'device_bay']
template_name = 'dcim/device_edit.html' template_name = 'dcim/device_edit.html'
obj_list_url = 'dcim:device_list' default_return_url = 'dcim:device_list'
class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView): class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@ -680,7 +689,7 @@ class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.DeviceImportForm form = forms.DeviceImportForm
table = tables.DeviceImportTable table = tables.DeviceImportTable
template_name = 'dcim/device_import.html' template_name = 'dcim/device_import.html'
obj_list_url = 'dcim:device_list' default_return_url = 'dcim:device_list'
class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView): class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
@ -688,7 +697,7 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.ChildDeviceImportForm form = forms.ChildDeviceImportForm
table = tables.DeviceImportTable table = tables.DeviceImportTable
template_name = 'dcim/device_import_child.html' template_name = 'dcim/device_import_child.html'
obj_list_url = 'dcim:device_list' default_return_url = 'dcim:device_list'
def save_obj(self, obj): def save_obj(self, obj):
# Inherent rack from parent device # Inherent rack from parent device
@ -703,15 +712,17 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView): class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_device' permission_required = 'dcim.change_device'
cls = Device cls = Device
filter = filters.DeviceFilter
form = forms.DeviceBulkEditForm form = forms.DeviceBulkEditForm
template_name = 'dcim/device_bulk_edit.html' template_name = 'dcim/device_bulk_edit.html'
default_redirect_url = 'dcim:device_list' default_return_url = 'dcim:device_list'
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_device' permission_required = 'dcim.delete_device'
cls = Device cls = Device
default_redirect_url = 'dcim:device_list' filter = filters.DeviceFilter
default_return_url = 'dcim:device_list'
def device_inventory(request, pk): def device_inventory(request, pk):
@ -729,7 +740,8 @@ def device_inventory(request, pk):
def device_lldp_neighbors(request, pk): def device_lldp_neighbors(request, pk):
device = get_object_or_404(Device, pk=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', { return render(request, 'dcim/device_lldp_neighbors.html', {
'device': device, 'device': device,
@ -776,7 +788,7 @@ def consoleport_connect(request, pk):
return render(request, 'dcim/consoleport_connect.html', { return render(request, 'dcim/consoleport_connect.html', {
'consoleport': consoleport, 'consoleport': consoleport,
'form': form, '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', { return render(request, 'dcim/consoleport_disconnect.html', {
'consoleport': consoleport, 'consoleport': consoleport,
'form': form, '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' permission_required = 'dcim.change_consoleport'
model = ConsolePort model = ConsolePort
form_class = forms.ConsolePortForm form_class = forms.ConsolePortForm
class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView): class ConsolePortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_consoleport' permission_required = 'dcim.delete_consoleport'
model = ConsolePort model = ConsolePort
@ -872,7 +884,7 @@ def consoleserverport_connect(request, pk):
return render(request, 'dcim/consoleserverport_connect.html', { return render(request, 'dcim/consoleserverport_connect.html', {
'consoleserverport': consoleserverport, 'consoleserverport': consoleserverport,
'form': form, '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', { return render(request, 'dcim/consoleserverport_disconnect.html', {
'consoleserverport': consoleserverport, 'consoleserverport': consoleserverport,
'form': form, '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' permission_required = 'dcim.change_consoleserverport'
model = ConsoleServerPort model = ConsoleServerPort
form_class = forms.ConsoleServerPortForm form_class = forms.ConsoleServerPortForm
class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): class ConsoleServerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_consoleserverport' permission_required = 'dcim.delete_consoleserverport'
model = ConsoleServerPort model = ConsoleServerPort
@ -962,7 +974,7 @@ def powerport_connect(request, pk):
return render(request, 'dcim/powerport_connect.html', { return render(request, 'dcim/powerport_connect.html', {
'powerport': powerport, 'powerport': powerport,
'form': form, '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', { return render(request, 'dcim/powerport_disconnect.html', {
'powerport': powerport, 'powerport': powerport,
'form': form, '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' permission_required = 'dcim.change_powerport'
model = PowerPort model = PowerPort
form_class = forms.PowerPortForm form_class = forms.PowerPortForm
class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): class PowerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_powerport' permission_required = 'dcim.delete_powerport'
model = PowerPort model = PowerPort
@ -1058,7 +1070,7 @@ def poweroutlet_connect(request, pk):
return render(request, 'dcim/poweroutlet_connect.html', { return render(request, 'dcim/poweroutlet_connect.html', {
'poweroutlet': poweroutlet, 'poweroutlet': poweroutlet,
'form': form, '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', { return render(request, 'dcim/poweroutlet_disconnect.html', {
'poweroutlet': poweroutlet, 'poweroutlet': poweroutlet,
'form': form, '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' permission_required = 'dcim.change_poweroutlet'
model = PowerOutlet model = PowerOutlet
form_class = forms.PowerOutletForm form_class = forms.PowerOutletForm
class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView): class PowerOutletDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_poweroutlet' permission_required = 'dcim.delete_poweroutlet'
model = PowerOutlet model = PowerOutlet
@ -1121,13 +1133,13 @@ class InterfaceAddView(PermissionRequiredMixin, ComponentCreateView):
model_form = forms.InterfaceForm model_form = forms.InterfaceForm
class InterfaceEditView(PermissionRequiredMixin, ObjectEditView): class InterfaceEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_interface' permission_required = 'dcim.change_interface'
model = Interface model = Interface
form_class = forms.InterfaceForm form_class = forms.InterfaceForm
class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView): class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_interface' permission_required = 'dcim.delete_interface'
model = Interface model = Interface
@ -1159,13 +1171,13 @@ class DeviceBayAddView(PermissionRequiredMixin, ComponentCreateView):
model_form = forms.DeviceBayForm model_form = forms.DeviceBayForm
class DeviceBayEditView(PermissionRequiredMixin, ObjectEditView): class DeviceBayEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_devicebay' permission_required = 'dcim.change_devicebay'
model = DeviceBay model = DeviceBay
form_class = forms.DeviceBayForm form_class = forms.DeviceBayForm
class DeviceBayDeleteView(PermissionRequiredMixin, ObjectDeleteView): class DeviceBayDeleteView(PermissionRequiredMixin, ComponentDeleteView):
permission_required = 'dcim.delete_devicebay' permission_required = 'dcim.delete_devicebay'
model = DeviceBay model = DeviceBay
@ -1192,7 +1204,7 @@ def devicebay_populate(request, pk):
return render(request, 'dcim/devicebay_populate.html', { return render(request, 'dcim/devicebay_populate.html', {
'device_bay': device_bay, 'device_bay': device_bay,
'form': form, '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', { return render(request, 'dcim/devicebay_depopulate.html', {
'device_bay': device_bay, 'device_bay': device_bay,
'form': form, '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? # Are we editing *all* objects in the queryset or just a selected subset?
if request.POST.get('_all'): 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: else:
pk_list = [int(pk) for pk in request.POST.getlist('pk')] pk_list = [int(pk) for pk in request.POST.getlist('pk')]
@ -1291,7 +1303,7 @@ class DeviceBulkAddComponentView(View):
'form': form, 'form': form,
'component_name': self.model._meta.verbose_name_plural, 'component_name': self.model._meta.verbose_name_plural,
'selected_devices': selected_devices, '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', { return render(request, 'dcim/interfaceconnection_edit.html', {
'device': device, 'device': device,
'form': form, '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 # Determine where to direct user upon cancellation
if device_id: if device_id:
cancel_url = reverse('dcim:device', kwargs={'pk': device_id}) return_url = reverse('dcim:device', kwargs={'pk': device_id})
else: else:
cancel_url = reverse('dcim:device_list') return_url = reverse('dcim:device_list')
return render(request, 'dcim/interfaceconnection_delete.html', { return render(request, 'dcim/interfaceconnection_delete.html', {
'interfaceconnection': interfaceconnection, 'interfaceconnection': interfaceconnection,
'device_id': device_id, 'device_id': device_id,
'form': form, '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', { return render(request, 'dcim/ipaddress_assign.html', {
'device': device, 'device': device,
'form': form, '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 # Modules
# #
class ModuleEditView(PermissionRequiredMixin, ObjectEditView): class ModuleEditView(PermissionRequiredMixin, ComponentEditView):
permission_required = 'dcim.change_module' permission_required = 'dcim.change_module'
model = Module model = Module
form_class = forms.ModuleForm form_class = forms.ModuleForm
@ -1510,10 +1522,7 @@ class ModuleEditView(PermissionRequiredMixin, ObjectEditView):
obj.device = get_object_or_404(Device, pk=kwargs['device']) obj.device = get_object_or_404(Device, pk=kwargs['device'])
return obj return obj
def get_return_url(self, obj):
return obj.device.get_absolute_url()
class ModuleDeleteView(PermissionRequiredMixin, ComponentDeleteView):
class ModuleDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_module' permission_required = 'dcim.delete_module'
model = Module model = Module

View File

@ -50,7 +50,7 @@ class FlatJSONRenderer(renderers.BaseRenderer):
def render(self, data, media_type=None, renderer_context=None): def render(self, data, media_type=None, renderer_context=None):
def flatten(entry): def flatten(entry):
for key, val in entry.iteritems(): for key, val in entry.items():
if isinstance(val, dict): if isinstance(val, dict):
for child_key, child_val in flatten(val): for child_key, child_val in flatten(val):
yield "{}_{}".format(key, child_key), child_val yield "{}_{}".format(key, child_key), child_val

View File

@ -8,6 +8,7 @@ from django.core.validators import ValidationError
from django.db import models from django.db import models
from django.http import HttpResponse from django.http import HttpResponse
from django.template import Template, Context from django.template import Template, Context
from django.utils.encoding import python_2_unicode_compatible
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -93,6 +94,7 @@ class CustomFieldModel(object):
return OrderedDict([(field, None) for field in fields]) return OrderedDict([(field, None) for field in fields])
@python_2_unicode_compatible
class CustomField(models.Model): class CustomField(models.Model):
obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)', obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)',
limit_choices_to={'model__in': CUSTOMFIELD_MODELS}, limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
@ -114,7 +116,7 @@ class CustomField(models.Model):
class Meta: class Meta:
ordering = ['weight', 'name'] ordering = ['weight', 'name']
def __unicode__(self): def __str__(self):
return self.label or self.name.replace('_', ' ').capitalize() return self.label or self.name.replace('_', ' ').capitalize()
def serialize_value(self, value): def serialize_value(self, value):
@ -153,6 +155,7 @@ class CustomField(models.Model):
return serialized_value return serialized_value
@python_2_unicode_compatible
class CustomFieldValue(models.Model): class CustomFieldValue(models.Model):
field = models.ForeignKey('CustomField', related_name='values') field = models.ForeignKey('CustomField', related_name='values')
obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT) obj_type = models.ForeignKey(ContentType, related_name='+', on_delete=models.PROTECT)
@ -164,7 +167,7 @@ class CustomFieldValue(models.Model):
ordering = ['obj_type', 'obj_id'] ordering = ['obj_type', 'obj_id']
unique_together = ['field', 'obj_type', 'obj_id'] unique_together = ['field', 'obj_type', 'obj_id']
def __unicode__(self): def __str__(self):
return u'{} {}'.format(self.obj, self.field) return u'{} {}'.format(self.obj, self.field)
@property @property
@ -183,6 +186,7 @@ class CustomFieldValue(models.Model):
super(CustomFieldValue, self).save(*args, **kwargs) super(CustomFieldValue, self).save(*args, **kwargs)
@python_2_unicode_compatible
class CustomFieldChoice(models.Model): class CustomFieldChoice(models.Model):
field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT}, field = models.ForeignKey('CustomField', related_name='choices', limit_choices_to={'type': CF_TYPE_SELECT},
on_delete=models.CASCADE) on_delete=models.CASCADE)
@ -193,7 +197,7 @@ class CustomFieldChoice(models.Model):
ordering = ['field', 'weight', 'value'] ordering = ['field', 'weight', 'value']
unique_together = ['field', 'value'] unique_together = ['field', 'value']
def __unicode__(self): def __str__(self):
return self.value return self.value
def clean(self): def clean(self):
@ -207,6 +211,7 @@ class CustomFieldChoice(models.Model):
CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete() CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
@python_2_unicode_compatible
class Graph(models.Model): class Graph(models.Model):
type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES) type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
weight = models.PositiveSmallIntegerField(default=1000) weight = models.PositiveSmallIntegerField(default=1000)
@ -217,7 +222,7 @@ class Graph(models.Model):
class Meta: class Meta:
ordering = ['type', 'weight', 'name'] ordering = ['type', 'weight', 'name']
def __unicode__(self): def __str__(self):
return self.name return self.name
def embed_url(self, obj): def embed_url(self, obj):
@ -231,6 +236,7 @@ class Graph(models.Model):
return template.render(Context({'obj': obj})) return template.render(Context({'obj': obj}))
@python_2_unicode_compatible
class ExportTemplate(models.Model): class ExportTemplate(models.Model):
content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}) content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
@ -245,7 +251,7 @@ class ExportTemplate(models.Model):
['content_type', 'name'] ['content_type', 'name']
] ]
def __unicode__(self): def __str__(self):
return u'{}: {}'.format(self.content_type, self.name) return u'{}: {}'.format(self.content_type, self.name)
def to_response(self, context_dict, filename): def to_response(self, context_dict, filename):
@ -264,6 +270,7 @@ class ExportTemplate(models.Model):
return response return response
@python_2_unicode_compatible
class TopologyMap(models.Model): class TopologyMap(models.Model):
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
@ -278,7 +285,7 @@ class TopologyMap(models.Model):
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
def __unicode__(self): def __str__(self):
return self.name return self.name
@property @property
@ -328,6 +335,7 @@ class UserActionManager(models.Manager):
self.log_bulk_action(user, content_type, ACTION_BULK_DELETE, message) self.log_bulk_action(user, content_type, ACTION_BULK_DELETE, message)
@python_2_unicode_compatible
class UserAction(models.Model): class UserAction(models.Model):
""" """
A record of an action (add, edit, or delete) performed on an object by a User. 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: class Meta:
ordering = ['-time'] ordering = ['-time']
def __unicode__(self): def __str__(self):
if self.message: if self.message:
return u'{} {}'.format(self.user, self.message) return u'{} {}'.format(self.user, self.message)
return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type) return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type)

View File

@ -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. # This script will generate a random 50-character string suitable for use as a SECRET_KEY.
import os import os
import random import random
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)' charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)'
random.seed = (os.urandom(2048)) 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)))

View File

@ -63,6 +63,7 @@ class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm): class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = VRF model = VRF
q = forms.CharField(required=False, label='Search')
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), to_field_name='slug', tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), to_field_name='slug',
null_option=(0, None)) null_option=(0, None))
@ -128,6 +129,7 @@ class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm): class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Aggregate model = Aggregate
q = forms.CharField(required=False, label='Search')
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family') 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', rir = FilterChoiceField(queryset=RIR.objects.annotate(filter_count=Count('aggregates')), to_field_name='slug',
label='RIR') label='RIR')
@ -256,8 +258,9 @@ def prefix_status_choices():
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Prefix model = Prefix
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={ q = forms.CharField(required=False, label='Search')
'placeholder': 'Network', 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') 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', 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): class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = IPAddress 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', 'placeholder': 'Prefix',
})) }))
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family') family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
@ -560,6 +564,7 @@ def vlan_status_choices():
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = VLAN model = VLAN
q = forms.CharField(required=False, label='Search')
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug') 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', group_id = FilterChoiceField(queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group',
null_option=(0, 'None')) null_option=(0, 'None'))

View File

@ -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'),
),
]

View File

@ -7,6 +7,7 @@ from django.core.urlresolvers import reverse
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models.expressions import RawSQL from django.db.models.expressions import RawSQL
from django.utils.encoding import python_2_unicode_compatible
from dcim.models import Interface from dcim.models import Interface
from extras.models import CustomFieldModel, CustomFieldValue from extras.models import CustomFieldModel, CustomFieldValue
@ -36,10 +37,12 @@ PREFIX_STATUS_CHOICES = (
IPADDRESS_STATUS_ACTIVE = 1 IPADDRESS_STATUS_ACTIVE = 1
IPADDRESS_STATUS_RESERVED = 2 IPADDRESS_STATUS_RESERVED = 2
IPADDRESS_STATUS_DEPRECATED = 3
IPADDRESS_STATUS_DHCP = 5 IPADDRESS_STATUS_DHCP = 5
IPADDRESS_STATUS_CHOICES = ( IPADDRESS_STATUS_CHOICES = (
(IPADDRESS_STATUS_ACTIVE, 'Active'), (IPADDRESS_STATUS_ACTIVE, 'Active'),
(IPADDRESS_STATUS_RESERVED, 'Reserved'), (IPADDRESS_STATUS_RESERVED, 'Reserved'),
(IPADDRESS_STATUS_DEPRECATED, 'Deprecated'),
(IPADDRESS_STATUS_DHCP, 'DHCP') (IPADDRESS_STATUS_DHCP, 'DHCP')
) )
@ -70,6 +73,7 @@ IP_PROTOCOL_CHOICES = (
) )
@python_2_unicode_compatible
class VRF(CreatedUpdatedModel, CustomFieldModel): class VRF(CreatedUpdatedModel, CustomFieldModel):
""" """
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing 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 = 'VRF'
verbose_name_plural = 'VRFs' verbose_name_plural = 'VRFs'
def __unicode__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
@ -105,6 +109,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
]) ])
@python_2_unicode_compatible
class RIR(models.Model): class RIR(models.Model):
""" """
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address 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 = 'RIR'
verbose_name_plural = 'RIRs' verbose_name_plural = 'RIRs'
def __unicode__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug) return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug)
@python_2_unicode_compatible
class Aggregate(CreatedUpdatedModel, CustomFieldModel): class Aggregate(CreatedUpdatedModel, CustomFieldModel):
""" """
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize 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: class Meta:
ordering = ['family', 'prefix'] ordering = ['family', 'prefix']
def __unicode__(self): def __str__(self):
return str(self.prefix) return str(self.prefix)
def get_absolute_url(self): def get_absolute_url(self):
@ -204,6 +210,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
return int(children_size / self.prefix.size * 100) return int(children_size / self.prefix.size * 100)
@python_2_unicode_compatible
class Role(models.Model): class Role(models.Model):
""" """
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or 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: class Meta:
ordering = ['weight', 'name'] ordering = ['weight', 'name']
def __unicode__(self): def __str__(self):
return self.name return self.name
@property @property
@ -263,6 +270,7 @@ class PrefixQuerySet(NullsFirstQuerySet):
return filter(lambda p: p.depth <= limit, queryset) return filter(lambda p: p.depth <= limit, queryset)
@python_2_unicode_compatible
class Prefix(CreatedUpdatedModel, CustomFieldModel): class Prefix(CreatedUpdatedModel, CustomFieldModel):
""" """
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and 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'] ordering = ['vrf', 'family', 'prefix']
verbose_name_plural = 'prefixes' verbose_name_plural = 'prefixes'
def __unicode__(self): def __str__(self):
return str(self.prefix) return str(self.prefix)
def get_absolute_url(self): 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') return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host')
@python_2_unicode_compatible
class IPAddress(CreatedUpdatedModel, CustomFieldModel): class IPAddress(CreatedUpdatedModel, CustomFieldModel):
""" """
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is 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 = 'IP address'
verbose_name_plural = 'IP addresses' verbose_name_plural = 'IP addresses'
def __unicode__(self): def __str__(self):
return str(self.address) return str(self.address)
def get_absolute_url(self): def get_absolute_url(self):
@ -469,6 +478,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
return STATUS_CHOICE_CLASSES[self.status] return STATUS_CHOICE_CLASSES[self.status]
@python_2_unicode_compatible
class VLANGroup(models.Model): class VLANGroup(models.Model):
""" """
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique. 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 = 'VLAN group'
verbose_name_plural = 'VLAN groups' verbose_name_plural = 'VLAN groups'
def __unicode__(self): def __str__(self):
return u'{} - {}'.format(self.site.name, self.name) return u'{} - {}'.format(self.site.name, self.name)
def get_absolute_url(self): def get_absolute_url(self):
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk) return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
@python_2_unicode_compatible
class VLAN(CreatedUpdatedModel, CustomFieldModel): 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 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 = 'VLAN'
verbose_name_plural = 'VLANs' verbose_name_plural = 'VLANs'
def __unicode__(self): def __str__(self):
return self.display_name return self.display_name
def get_absolute_url(self): def get_absolute_url(self):
@ -558,6 +569,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
return STATUS_CHOICE_CLASSES[self.status] return STATUS_CHOICE_CLASSES[self.status]
@python_2_unicode_compatible
class Service(CreatedUpdatedModel): 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 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'] ordering = ['device', 'protocol', 'port']
unique_together = ['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()) return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())

View File

@ -234,11 +234,12 @@ class PrefixBriefTable(BaseTable):
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF') 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') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') 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') role = tables.Column(verbose_name='Role')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Prefix model = Prefix
fields = ('prefix', 'vrf', 'status', 'site', 'role') fields = ('prefix', 'vrf', 'status', 'site', 'vlan', 'role')
orderable = False orderable = False

View File

@ -95,7 +95,6 @@ class VRFListView(ObjectListView):
filter = filters.VRFFilter filter = filters.VRFFilter
filter_form = forms.VRFFilterForm filter_form = forms.VRFFilterForm
table = tables.VRFTable table = tables.VRFTable
edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf']
template_name = 'ipam/vrf_list.html' template_name = 'ipam/vrf_list.html'
@ -118,7 +117,7 @@ class VRFEditView(PermissionRequiredMixin, ObjectEditView):
model = VRF model = VRF
form_class = forms.VRFForm form_class = forms.VRFForm
template_name = 'ipam/vrf_edit.html' template_name = 'ipam/vrf_edit.html'
obj_list_url = 'ipam:vrf_list' default_return_url = 'ipam:vrf_list'
class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView): class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@ -132,21 +131,23 @@ class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.VRFImportForm form = forms.VRFImportForm
table = tables.VRFTable table = tables.VRFTable
template_name = 'ipam/vrf_import.html' template_name = 'ipam/vrf_import.html'
obj_list_url = 'ipam:vrf_list' default_return_url = 'ipam:vrf_list'
class VRFBulkEditView(PermissionRequiredMixin, BulkEditView): class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_vrf' permission_required = 'ipam.change_vrf'
cls = VRF cls = VRF
filter = filters.VRFFilter
form = forms.VRFBulkEditForm form = forms.VRFBulkEditForm
template_name = 'ipam/vrf_bulk_edit.html' template_name = 'ipam/vrf_bulk_edit.html'
default_redirect_url = 'ipam:vrf_list' default_return_url = 'ipam:vrf_list'
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vrf' permission_required = 'ipam.delete_vrf'
cls = 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 = filters.RIRFilter
filter_form = forms.RIRFilterForm filter_form = forms.RIRFilterForm
table = tables.RIRTable table = tables.RIRTable
edit_permissions = ['ipam.change_rir', 'ipam.delete_rir']
template_name = 'ipam/rir_list.html' template_name = 'ipam/rir_list.html'
def alter_queryset(self, request): def alter_queryset(self, request):
@ -250,7 +250,8 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView):
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_rir' permission_required = 'ipam.delete_rir'
cls = 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 = filters.AggregateFilter
filter_form = forms.AggregateFilterForm filter_form = forms.AggregateFilterForm
table = tables.AggregateTable table = tables.AggregateTable
edit_permissions = ['ipam.change_aggregate', 'ipam.delete_aggregate']
template_name = 'ipam/aggregate_list.html' template_name = 'ipam/aggregate_list.html'
def extra_context(self): def extra_context(self):
@ -308,7 +308,7 @@ class AggregateEditView(PermissionRequiredMixin, ObjectEditView):
model = Aggregate model = Aggregate
form_class = forms.AggregateForm form_class = forms.AggregateForm
template_name = 'ipam/aggregate_edit.html' template_name = 'ipam/aggregate_edit.html'
obj_list_url = 'ipam:aggregate_list' default_return_url = 'ipam:aggregate_list'
class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView): class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@ -322,21 +322,23 @@ class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.AggregateImportForm form = forms.AggregateImportForm
table = tables.AggregateTable table = tables.AggregateTable
template_name = 'ipam/aggregate_import.html' template_name = 'ipam/aggregate_import.html'
obj_list_url = 'ipam:aggregate_list' default_return_url = 'ipam:aggregate_list'
class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView): class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_aggregate' permission_required = 'ipam.change_aggregate'
cls = Aggregate cls = Aggregate
filter = filters.AggregateFilter
form = forms.AggregateBulkEditForm form = forms.AggregateBulkEditForm
template_name = 'ipam/aggregate_bulk_edit.html' template_name = 'ipam/aggregate_bulk_edit.html'
default_redirect_url = 'ipam:aggregate_list' default_return_url = 'ipam:aggregate_list'
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_aggregate' permission_required = 'ipam.delete_aggregate'
cls = 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): class RoleListView(ObjectListView):
queryset = Role.objects.all() queryset = Role.objects.all()
table = tables.RoleTable table = tables.RoleTable
edit_permissions = ['ipam.change_role', 'ipam.delete_role']
template_name = 'ipam/role_list.html' template_name = 'ipam/role_list.html'
@ -362,7 +363,7 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_role' permission_required = 'ipam.delete_role'
cls = 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 = filters.PrefixFilter
filter_form = forms.PrefixFilterForm filter_form = forms.PrefixFilterForm
table = tables.PrefixTable table = tables.PrefixTable
edit_permissions = ['ipam.change_prefix', 'ipam.delete_prefix']
template_name = 'ipam/prefix_list.html' template_name = 'ipam/prefix_list.html'
def alter_queryset(self, request): def alter_queryset(self, request):
@ -401,11 +401,13 @@ def prefix(request, pk):
.filter(prefix__net_contains=str(prefix.prefix))\ .filter(prefix__net_contains=str(prefix.prefix))\
.select_related('site', 'role').annotate_depth() .select_related('site', 'role').annotate_depth()
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes) parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
parent_prefix_table.exclude = ('vrf',)
# Duplicate prefixes table # Duplicate prefixes table
duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\ duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\
.select_related('site', 'role') .select_related('site', 'role')
duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes)) duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes))
duplicate_prefix_table.exclude = ('vrf',)
# Child prefixes table # Child prefixes table
if prefix.vrf: if prefix.vrf:
@ -430,6 +432,7 @@ def prefix(request, pk):
'parent_prefix_table': parent_prefix_table, 'parent_prefix_table': parent_prefix_table,
'child_prefix_table': child_prefix_table, 'child_prefix_table': child_prefix_table,
'duplicate_prefix_table': duplicate_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 form_class = forms.PrefixForm
template_name = 'ipam/prefix_edit.html' template_name = 'ipam/prefix_edit.html'
fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan'] fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan']
obj_list_url = 'ipam:prefix_list' default_return_url = 'ipam:prefix_list'
class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView): class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_prefix' permission_required = 'ipam.delete_prefix'
model = Prefix model = Prefix
default_return_url = 'ipam:prefix_list'
template_name = 'ipam/prefix_delete.html' template_name = 'ipam/prefix_delete.html'
default_return_url = 'ipam:prefix_list'
class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView): class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
@ -454,21 +457,23 @@ class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.PrefixImportForm form = forms.PrefixImportForm
table = tables.PrefixTable table = tables.PrefixTable
template_name = 'ipam/prefix_import.html' template_name = 'ipam/prefix_import.html'
obj_list_url = 'ipam:prefix_list' default_return_url = 'ipam:prefix_list'
class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView): class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_prefix' permission_required = 'ipam.change_prefix'
cls = Prefix cls = Prefix
filter = filters.PrefixFilter
form = forms.PrefixBulkEditForm form = forms.PrefixBulkEditForm
template_name = 'ipam/prefix_bulk_edit.html' template_name = 'ipam/prefix_bulk_edit.html'
default_redirect_url = 'ipam:prefix_list' default_return_url = 'ipam:prefix_list'
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_prefix' permission_required = 'ipam.delete_prefix'
cls = Prefix cls = Prefix
default_redirect_url = 'ipam:prefix_list' filter = filters.PrefixFilter
default_return_url = 'ipam:prefix_list'
def prefix_ipaddresses(request, pk): def prefix_ipaddresses(request, pk):
@ -500,7 +505,6 @@ class IPAddressListView(ObjectListView):
filter = filters.IPAddressFilter filter = filters.IPAddressFilter
filter_form = forms.IPAddressFilterForm filter_form = forms.IPAddressFilterForm
table = tables.IPAddressTable table = tables.IPAddressTable
edit_permissions = ['ipam.change_ipaddress', 'ipam.delete_ipaddress']
template_name = 'ipam/ipaddress_list.html' template_name = 'ipam/ipaddress_list.html'
@ -562,7 +566,7 @@ def ipaddress_assign(request, pk):
return render(request, 'ipam/ipaddress_assign.html', { return render(request, 'ipam/ipaddress_assign.html', {
'ipaddress': ipaddress, 'ipaddress': ipaddress,
'form': form, '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', { return render(request, 'ipam/ipaddress_unassign.html', {
'ipaddress': ipaddress, 'ipaddress': ipaddress,
'form': form, '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 form_class = forms.IPAddressForm
fields_initial = ['address', 'vrf'] fields_initial = ['address', 'vrf']
template_name = 'ipam/ipaddress_edit.html' template_name = 'ipam/ipaddress_edit.html'
obj_list_url = 'ipam:ipaddress_list' default_return_url = 'ipam:ipaddress_list'
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView): class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@ -619,7 +623,7 @@ class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
form = forms.IPAddressBulkAddForm form = forms.IPAddressBulkAddForm
model = IPAddress model = IPAddress
template_name = 'ipam/ipaddress_bulk_add.html' template_name = 'ipam/ipaddress_bulk_add.html'
redirect_url = 'ipam:ipaddress_list' default_return_url = 'ipam:ipaddress_list'
class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView): class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
@ -627,7 +631,7 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.IPAddressImportForm form = forms.IPAddressImportForm
table = tables.IPAddressTable table = tables.IPAddressTable
template_name = 'ipam/ipaddress_import.html' template_name = 'ipam/ipaddress_import.html'
obj_list_url = 'ipam:ipaddress_list' default_return_url = 'ipam:ipaddress_list'
def save_obj(self, obj): def save_obj(self, obj):
obj.save() obj.save()
@ -648,15 +652,17 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView): class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_ipaddress' permission_required = 'ipam.change_ipaddress'
cls = IPAddress cls = IPAddress
filter = filters.IPAddressFilter
form = forms.IPAddressBulkEditForm form = forms.IPAddressBulkEditForm
template_name = 'ipam/ipaddress_bulk_edit.html' template_name = 'ipam/ipaddress_bulk_edit.html'
default_redirect_url = 'ipam:ipaddress_list' default_return_url = 'ipam:ipaddress_list'
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_ipaddress' permission_required = 'ipam.delete_ipaddress'
cls = 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 = filters.VLANGroupFilter
filter_form = forms.VLANGroupFilterForm filter_form = forms.VLANGroupFilterForm
table = tables.VLANGroupTable table = tables.VLANGroupTable
edit_permissions = ['ipam.change_vlangroup', 'ipam.delete_vlangroup']
template_name = 'ipam/vlangroup_list.html' template_name = 'ipam/vlangroup_list.html'
@ -684,7 +689,8 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlangroup' permission_required = 'ipam.delete_vlangroup'
cls = 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 = filters.VLANFilter
filter_form = forms.VLANFilterForm filter_form = forms.VLANFilterForm
table = tables.VLANTable table = tables.VLANTable
edit_permissions = ['ipam.change_vlan', 'ipam.delete_vlan']
template_name = 'ipam/vlan_list.html' 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) vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk)
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role') prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
prefix_table = tables.PrefixBriefTable(list(prefixes)) prefix_table = tables.PrefixBriefTable(list(prefixes))
prefix_table.exclude = ('vlan',)
return render(request, 'ipam/vlan.html', { return render(request, 'ipam/vlan.html', {
'vlan': vlan, 'vlan': vlan,
@ -717,7 +723,7 @@ class VLANEditView(PermissionRequiredMixin, ObjectEditView):
model = VLAN model = VLAN
form_class = forms.VLANForm form_class = forms.VLANForm
template_name = 'ipam/vlan_edit.html' template_name = 'ipam/vlan_edit.html'
obj_list_url = 'ipam:vlan_list' default_return_url = 'ipam:vlan_list'
class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView): class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@ -731,21 +737,23 @@ class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.VLANImportForm form = forms.VLANImportForm
table = tables.VLANTable table = tables.VLANTable
template_name = 'ipam/vlan_import.html' template_name = 'ipam/vlan_import.html'
obj_list_url = 'ipam:vlan_list' default_return_url = 'ipam:vlan_list'
class VLANBulkEditView(PermissionRequiredMixin, BulkEditView): class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_vlan' permission_required = 'ipam.change_vlan'
cls = VLAN cls = VLAN
filter = filters.VLANFilter
form = forms.VLANBulkEditForm form = forms.VLANBulkEditForm
template_name = 'ipam/vlan_bulk_edit.html' template_name = 'ipam/vlan_bulk_edit.html'
default_redirect_url = 'ipam:vlan_list' default_return_url = 'ipam:vlan_list'
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlan' permission_required = 'ipam.delete_vlan'
cls = VLAN cls = VLAN
default_redirect_url = 'ipam:vlan_list' filter = filters.VLANFilter
default_return_url = 'ipam:vlan_list'
# #

View File

@ -6,13 +6,13 @@ from django.contrib.messages import constants as messages
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
try: try:
import configuration from netbox import configuration
except ImportError: except ImportError:
raise ImproperlyConfigured("Configuration file is not present. Please define netbox/netbox/configuration.py per " raise ImproperlyConfigured("Configuration file is not present. Please define netbox/netbox/configuration.py per "
"the documentation.") "the documentation.")
VERSION = '1.8.2' VERSION = '1.8.3'
# Import local configuration # Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: 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 # Attempt to import LDAP configuration if it has been defined
LDAP_IGNORE_CERT_ERRORS = False LDAP_IGNORE_CERT_ERRORS = False
try: try:
from ldap_config import * from netbox.ldap_config import *
LDAP_CONFIGURED = True LDAP_CONFIGURED = True
except ImportError: except ImportError:
LDAP_CONFIGURED = False LDAP_CONFIGURED = False

View File

@ -2,7 +2,7 @@ from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from django.contrib import admin 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 from users.views import login, logout

View File

@ -9,6 +9,14 @@ $(document).ready(function() {
$('#select_all').prop('checked', false); $('#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 // Uncheck the "toggle all" checkbox if an item is unchecked
$('input:checkbox[name=pk]').click(function (event) { $('input:checkbox[name=pk]').click(function (event) {
if (!$(this).attr('checked')) { if (!$(this).attr('checked')) {

View File

@ -48,7 +48,7 @@ $(document).ready(function() {
$('#generate_keypair').click(function() { $('#generate_keypair').click(function() {
$('#new_keypair_modal').modal('show'); $('#new_keypair_modal').modal('show');
$.ajax({ $.ajax({
url: '/api/secrets/generate-keys/', url: netbox_api_path + 'secrets/generate-keys/',
type: 'GET', type: 'GET',
dataType: 'json', dataType: 'json',
success: function (response, status) { success: function (response, status) {
@ -75,7 +75,7 @@ $(document).ready(function() {
function unlock_secret(secret_id, private_key) { function unlock_secret(secret_id, private_key) {
var csrf_token = $('input[name=csrfmiddlewaretoken]').val(); var csrf_token = $('input[name=csrfmiddlewaretoken]').val();
$.ajax({ $.ajax({
url: '/api/secrets/secrets/' + secret_id + '/', url: netbox_api_path + 'secrets/secrets/' + secret_id + '/',
type: 'POST', type: 'POST',
data: { data: {
private_key: private_key private_key: private_key

View File

@ -100,6 +100,7 @@ class SecretBulkEditForm(BootstrapMixin, BulkEditForm):
class SecretFilterForm(BootstrapMixin, forms.Form): 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') role = FilterChoiceField(queryset=SecretRole.objects.annotate(filter_count=Count('secrets')), to_field_name='slug')

View File

@ -8,7 +8,7 @@ from django.contrib.auth.models import Group, User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models 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 dcim.models import Device
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
@ -51,6 +51,7 @@ class UserKeyQuerySet(models.QuerySet):
raise Exception("Bulk deletion has been disabled.") raise Exception("Bulk deletion has been disabled.")
@python_2_unicode_compatible
class UserKey(CreatedUpdatedModel): class UserKey(CreatedUpdatedModel):
""" """
A UserKey stores a user's personal RSA (public) encryption key, which is used to generate their unique encrypted 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_public_key = self.public_key
self.__initial_master_key_cipher = self.master_key_cipher self.__initial_master_key_cipher = self.master_key_cipher
def __unicode__(self): def __str__(self):
return self.user.username return self.user.username
def clean(self, *args, **kwargs): def clean(self, *args, **kwargs):
@ -170,6 +171,7 @@ class UserKey(CreatedUpdatedModel):
self.save() self.save()
@python_2_unicode_compatible
class SecretRole(models.Model): class SecretRole(models.Model):
""" """
A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles 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: class Meta:
ordering = ['name'] ordering = ['name']
def __unicode__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): 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() return user in self.users.all() or user.groups.filter(pk__in=self.groups.all()).exists()
@python_2_unicode_compatible
class Secret(CreatedUpdatedModel): class Secret(CreatedUpdatedModel):
""" """
A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible 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) self.plaintext = kwargs.pop('plaintext', None)
super(Secret, self).__init__(*args, **kwargs) super(Secret, self).__init__(*args, **kwargs)
def __unicode__(self): def __str__(self):
if self.role and self.device: if self.role and self.device:
return u'{} for {}'.format(self.role, self.device) return u'{} for {}'.format(self.role, self.device)
return u'Secret' return u'Secret'

View File

@ -1 +0,0 @@
from test_models import *

View File

@ -22,7 +22,6 @@ from .models import SecretRole, Secret, UserKey
class SecretRoleListView(ObjectListView): class SecretRoleListView(ObjectListView):
queryset = SecretRole.objects.annotate(secret_count=Count('secrets')) queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
table = tables.SecretRoleTable table = tables.SecretRoleTable
edit_permissions = ['secrets.change_secretrole', 'secrets.delete_secretrole']
template_name = 'secrets/secretrole_list.html' template_name = 'secrets/secretrole_list.html'
@ -38,7 +37,7 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'secrets.delete_secretrole' permission_required = 'secrets.delete_secretrole'
cls = 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 = filters.SecretFilter
filter_form = forms.SecretFilterForm filter_form = forms.SecretFilterForm
table = tables.SecretTable table = tables.SecretTable
edit_permissions = ['secrets.change_secret', 'secrets.delete_secret']
template_name = 'secrets/secret_list.html' template_name = 'secrets/secret_list.html'
@ -103,7 +101,7 @@ def secret_add(request, pk):
return render(request, 'secrets/secret_edit.html', { return render(request, 'secrets/secret_edit.html', {
'secret': secret, 'secret': secret,
'form': form, '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', { return render(request, 'secrets/secret_edit.html', {
'secret': secret, 'secret': secret,
'form': form, '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', { return render(request, 'secrets/secret_import.html', {
'form': form, 'form': form,
'cancel_url': reverse('secrets:secret_list'), 'return_url': reverse('secrets:secret_list'),
}) })
class SecretBulkEditView(PermissionRequiredMixin, BulkEditView): class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'secrets.change_secret' permission_required = 'secrets.change_secret'
cls = Secret cls = Secret
filter = filters.SecretFilter
form = forms.SecretBulkEditForm form = forms.SecretBulkEditForm
template_name = 'secrets/secret_bulk_edit.html' template_name = 'secrets/secret_bulk_edit.html'
default_redirect_url = 'secrets:secret_list' default_return_url = 'secrets:secret_list'
class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'secrets.delete_secret' permission_required = 'secrets.delete_secret'
cls = Secret cls = Secret
default_redirect_url = 'secrets:secret_list' filter = filters.SecretFilter
default_return_url = 'secrets:secret_list'

View File

@ -296,6 +296,9 @@
</div> </div>
</div> </div>
</footer> </footer>
<script type="text/javascript">
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
</script>
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script> <script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'jquery-ui-1.11.4/jquery-ui.min.js' %}"></script> <script src="{% static 'jquery-ui-1.11.4/jquery-ui.min.js' %}"></script>
<script src="{% static 'bootstrap-3.3.6-dist/js/bootstrap.min.js' %}"></script> <script src="{% static 'bootstrap-3.3.6-dist/js/bootstrap.min.js' %}"></script>

View File

@ -13,7 +13,7 @@
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a> <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -24,7 +24,6 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -83,7 +83,7 @@
{% else %} {% else %}
<button type="submit" name="_create" class="btn btn-primary">Create</button> <button type="submit" name="_create" class="btn btn-primary">Create</button>
{% endif %} {% endif %}
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</div> </div>
</form> </form>

View File

@ -13,7 +13,7 @@
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a> <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -23,7 +23,6 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -19,7 +19,7 @@
{% render_table table 'table.html' %} {% render_table table 'table.html' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/filter_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -40,7 +40,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-md-9 col-md-offset-3"> <div class="col-md-9 col-md-offset-3">
<button type="submit" name="_update" class="btn btn-primary">Connect</button> <button type="submit" name="_update" class="btn btn-primary">Connect</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -40,7 +40,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-md-9 col-md-offset-3"> <div class="col-md-9 col-md-offset-3">
<button type="submit" name="_update" class="btn btn-primary">Connect</button> <button type="submit" name="_update" class="btn btn-primary">Connect</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -550,7 +550,7 @@
function toggleConnection(elem, api_url) { function toggleConnection(elem, api_url) {
if (elem.hasClass('connected')) { if (elem.hasClass('connected')) {
$.ajax({ $.ajax({
url: api_url + elem.attr('data') + "/", url: netbox_api_path + api_url + elem.attr('data') + "/",
method: 'PATCH', method: 'PATCH',
dataType: 'json', dataType: 'json',
beforeSend: function(xhr, settings) { beforeSend: function(xhr, settings) {
@ -590,13 +590,13 @@ function toggleConnection(elem, api_url) {
return false; return false;
} }
$(".consoleport-toggle").click(function() { $(".consoleport-toggle").click(function() {
return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/console-ports/"); return toggleConnection($(this), "dcim/console-ports/");
}); });
$(".powerport-toggle").click(function() { $(".powerport-toggle").click(function() {
return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/power-ports/"); return toggleConnection($(this), "dcim/power-ports/");
}); });
$(".interface-toggle").click(function() { $(".interface-toggle").click(function() {
return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/interface-connections/"); return toggleConnection($(this), "dcim/interface-connections/");
}); });
</script> </script>
<script src="{% static 'js/graphs.js' %}"></script> <script src="{% static 'js/graphs.js' %}"></script>

View File

@ -5,8 +5,8 @@
<h1>Add {{ component_name|title }}</h1> <h1>Add {{ component_name|title }}</h1>
<form action="." method="post" class="form form-horizontal"> <form action="." method="post" class="form form-horizontal">
{% csrf_token %} {% csrf_token %}
{% if request.POST.redirect_url %} {% if request.POST.return_url %}
<input type="hidden" name="redirect_url" value="{{ request.POST.redirect_url }}" /> <input type="hidden" name="return_url" value="{{ request.POST.return_url }}" />
{% endif %} {% endif %}
{% for field in form.hidden_fields %} {% for field in form.hidden_fields %}
{{ field }} {{ field }}
@ -51,7 +51,7 @@
<div class="form-group text-right"> <div class="form-group text-right">
<div class="col-md-12"> <div class="col-md-12">
<button type="submit" name="_create" class="btn btn-primary">Create</button> <button type="submit" name="_create" class="btn btn-primary">Create</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -34,7 +34,7 @@
<div class="col-md-9 col-md-offset-3"> <div class="col-md-9 col-md-offset-3">
<button type="submit" name="_create" class="btn btn-primary">Create</button> <button type="submit" name="_create" class="btn btn-primary">Create</button>
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button> <button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,7 +13,7 @@
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a> <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div> </div>
</form> </form>
<h4>CSV Format</h4> <h4>CSV Format</h4>

View File

@ -13,7 +13,7 @@
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a> <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div> </div>
</form> </form>
<h4>CSV Format</h4> <h4>CSV Format</h4>

View File

@ -24,7 +24,31 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block javascript %}
<script type="text/javascript">
$(document).ready(function() {
var model_list = $('#id_device_type_id');
$('#id_manufacturer_id').change(function() {
model_list.empty();
var selected_manufacturers = $(this).val();
if (selected_manufacturers) {
var api_url = netbox_api_path + 'dcim/device-types/?manufacturer_id=' + selected_manufacturers.join('&manufacturer_id=');
$.ajax({
url: api_url,
dataType: 'json',
success: function (response, status) {
$.each(response, function (index, device_type) {
var option = $("<option></option>").attr("value", device_type.id).text(device_type["model"] + " (" + device_type["instance_count"] + ")");
model_list.append(option);
});
}
});
}
});
});
</script>
{% endblock %}

View File

@ -37,7 +37,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-md-9 col-md-offset-3"> <div class="col-md-9 col-md-offset-3">
<button type="submit" name="_update" class="btn btn-primary">Save</button> <button type="submit" name="_update" class="btn btn-primary">Save</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -33,7 +33,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-md-9 col-md-offset-3"> <div class="col-md-9 col-md-offset-3">
<button type="submit" name="_update" class="btn btn-primary">Save</button> <button type="submit" name="_update" class="btn btn-primary">Save</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -19,7 +19,6 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -50,7 +50,7 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button> </button>
{% else %} {% else %}
<a href="{% url 'dcim:consoleport_delete' pk=cp.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <a href="{% url 'dcim:consoleport_delete' pk=cp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i>
</a> </a>
{% endif %} {% endif %}

View File

@ -49,7 +49,7 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button> </button>
{% else %} {% else %}
<a href="{% url 'dcim:consoleserverport_delete' pk=csp.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <a href="{% url 'dcim:consoleserverport_delete' pk=csp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i>
</a> </a>
{% endif %} {% endif %}

View File

@ -7,12 +7,12 @@
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% if perms.dcim.add_consoleport %}<li><a href="{% url 'dcim:device_bulk_add_consoleport' %}" class="formaction">Console Ports</a></li>{% endif %} {% if perms.dcim.add_consoleport %}<li><a href="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Ports</a></li>{% endif %}
{% if perms.dcim.add_consoleserverport %}<li><a href="{% url 'dcim:device_bulk_add_consoleserverport' %}" class="formaction">Console Server Ports</a></li>{% endif %} {% if perms.dcim.add_consoleserverport %}<li><a href="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Server Ports</a></li>{% endif %}
{% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:device_bulk_add_powerport' %}" class="formaction">Power Ports</a></li>{% endif %} {% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Ports</a></li>{% endif %}
{% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:device_bulk_add_poweroutlet' %}" class="formaction">Power Outlets</a></li>{% endif %} {% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Outlets</a></li>{% endif %}
{% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}" class="formaction">Interfaces</a></li>{% endif %} {% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
{% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}" class="formaction">Device Bays</a></li>{% endif %} {% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Device Bays</a></li>{% endif %}
</ul> </ul>
</div> </div>
{% endif %} {% endif %}

View File

@ -40,7 +40,7 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button> </button>
{% else %} {% else %}
<a href="{% url 'dcim:devicebay_delete' pk=devicebay.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <a href="{% url 'dcim:devicebay_delete' pk=devicebay.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete device bay"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete device bay"></i>
</a> </a>
{% endif %} {% endif %}

View File

@ -85,7 +85,7 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button> </button>
{% else %} {% else %}
<a href="{% url 'dcim:interface_delete' pk=iface.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete interface"> <a href="{% url 'dcim:interface_delete' pk=iface.pk %}" class="btn btn-danger btn-xs" title="Delete interface">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}

View File

@ -49,7 +49,7 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button> </button>
{% else %} {% else %}
<a href="{% url 'dcim:poweroutlet_delete' pk=po.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <a href="{% url 'dcim:poweroutlet_delete' pk=po.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete outlet"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete outlet"></i>
</a> </a>
{% endif %} {% endif %}

View File

@ -50,7 +50,7 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button> </button>
{% else %} {% else %}
<a href="{% url 'dcim:powerport_delete' pk=pp.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <a href="{% url 'dcim:powerport_delete' pk=pp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i>
</a> </a>
{% endif %} {% endif %}

View File

@ -19,7 +19,7 @@
{% render_table table 'table.html' %} {% render_table table 'table.html' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/filter_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -86,7 +86,7 @@
<div class="form-group"> <div class="form-group">
<button type="submit" name="_create" class="btn btn-primary">Connect</button> <button type="submit" name="_create" class="btn btn-primary">Connect</button>
<button type="submit" name="_addanother" class="btn btn-primary">Connect and Add Another</button> <button type="submit" name="_addanother" class="btn btn-primary">Connect and Add Another</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</div> </div>
</form> </form>

View File

@ -53,7 +53,7 @@
<div class="col-md-9 col-md-offset-3"> <div class="col-md-9 col-md-offset-3">
<button type="submit" name="_create" class="btn btn-primary">Create</button> <button type="submit" name="_create" class="btn btn-primary">Create</button>
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button> <button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -19,7 +19,7 @@
{% render_table table 'table.html' %} {% render_table table 'table.html' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/filter_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -40,7 +40,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-md-9 col-md-offset-3"> <div class="col-md-9 col-md-offset-3">
<button type="submit" name="_update" class="btn btn-primary">Connect</button> <button type="submit" name="_update" class="btn btn-primary">Connect</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -40,7 +40,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-md-9 col-md-offset-3"> <div class="col-md-9 col-md-offset-3">
<button type="submit" name="_update" class="btn btn-primary">Connect</button> <button type="submit" name="_update" class="btn btn-primary">Connect</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,7 +13,7 @@
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a> <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -24,7 +24,6 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -18,7 +18,7 @@
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackgroup_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackgroup_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/filter_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -13,7 +13,7 @@
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a> <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -23,7 +23,6 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,32 +0,0 @@
{% load form_helpers %}
{% if filter_form %}
<div class="panel panel-default">
<div class="panel-heading">
<span class="fa fa-filter" aria-hidden="true"></span>
<strong>Filter</strong>
</div>
<div class="panel-body">
<form action="." method="get" class="form">
{% for field in filter_form %}
<div class="form-group">
{% if field|widget_type == 'checkboxinput' %}
<label for="{{ field.id_for_label }}">{{ field }} {{ field.label }}</label>
{% else %}
{{ field.label_tag }}
{{ field }}
{% endif %}
</div>
{% endfor %}
<div class="text-right">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span> Apply
</button>
<a href="." class="btn btn-default">
<span class="fa fa-remove" aria-hidden="true"></span> Clear
</a>
</div>
</form>
</div>
</div>
{% endif %}

View File

@ -1,18 +1,39 @@
{% load form_helpers %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<span class="fa fa-search" aria-hidden="true"></span> <span class="fa fa-search" aria-hidden="true"></span>
<strong>Search</strong> <strong>Search</strong>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<form action="." method="get"> <form action="." method="get" class="form">
<div class="input-group"> {% for field in filter_form %}
<input type="text" name="q" class="form-control" placeholder="Search" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/> <div class="form-group">
<span class="input-group-btn"> {% if field.name == "q" %}
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Search" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
{% elif field|widget_type == 'checkboxinput' %}
<label for="{{ field.id_for_label }}">{{ field }} {{ field.label }}</label>
{% else %}
{{ field.label_tag }}
{{ field }}
{% endif %}
</div>
{% endfor %}
<div class="text-right">
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span> <span class="fa fa-search" aria-hidden="true"></span> Apply
</button> </button>
</span> <a href="." class="btn btn-default">
</div> <span class="fa fa-remove" aria-hidden="true"></span> Clear
</a>
</div>
</form> </form>
</div> </div>
</div> </div>

View File

@ -13,7 +13,7 @@
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a> <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -27,7 +27,6 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -59,7 +59,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-md-9 col-md-offset-3"> <div class="col-md-9 col-md-offset-3">
<button type="submit" name="_assign" class="btn btn-primary">Assign</button> <button type="submit" name="_assign" class="btn btn-primary">Assign</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,7 +13,7 @@
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a> <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% load helpers %} {% load helpers %}
{% block title %}IP Addresses{% endblock %} {% block title %}IP Addresses{% endblock %}
@ -25,7 +24,6 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -13,7 +13,7 @@
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a> <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -34,7 +34,6 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -33,7 +33,7 @@
{% endif %} {% endif %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/filter_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -13,7 +13,7 @@
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a> <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -25,7 +25,6 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -18,7 +18,7 @@
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:vlangroup_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_delete_url='ipam:vlangroup_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/filter_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -13,7 +13,7 @@
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a> <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -25,7 +25,6 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -59,7 +59,7 @@
{% else %} {% else %}
<button type="submit" name="_create" class="btn btn-primary">Create</button> <button type="submit" name="_create" class="btn btn-primary">Create</button>
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add Another</button> <button type="submit" name="_addanother" class="btn btn-primary">Create and Add Another</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -22,7 +22,7 @@
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -19,7 +19,6 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -13,7 +13,7 @@
{% render_form form %} {% render_form form %}
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
<a href="{% url obj_list_url %}" class="btn btn-default">Cancel</a> <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -24,7 +24,6 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -5,8 +5,8 @@
<h1>{% block title %}{% endblock %}</h1> <h1>{% block title %}{% endblock %}</h1>
<form action="." method="post" class="form form-horizontal"> <form action="." method="post" class="form form-horizontal">
{% csrf_token %} {% csrf_token %}
{% if request.POST.redirect_url %} {% if request.POST.return_url %}
<input type="hidden" name="redirect_url" value="{{ request.POST.redirect_url }}" /> <input type="hidden" name="return_url" value="{{ request.POST.return_url }}" />
{% endif %} {% endif %}
{% for field in form.hidden_fields %} {% for field in form.hidden_fields %}
{{ field }} {{ field }}
@ -44,7 +44,7 @@
<div class="form-group text-right"> <div class="form-group text-right">
<div class="col-md-12"> <div class="col-md-12">
<button type="submit" name="_apply" class="btn btn-primary">Apply</button> <button type="submit" name="_apply" class="btn btn-primary">Apply</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,7 +5,7 @@
{% block message %} {% block message %}
<p> <p>
Are you sure you want to delete these {{ obj_type_plural|default:"objects" }}{% if parent_obj %} from <a href="{{ parent_obj.get_absolute_url }}">{{ parent_obj }}</a>{% endif %}? Are you sure you want to delete these {{ selected_objects|length }} {{ obj_type_plural|default:"objects" }}{% if parent_obj %} from <a href="{{ parent_obj.get_absolute_url }}">{{ parent_obj }}</a>{% endif %}?
</p> </p>
<ul> <ul>
{% for obj in selected_objects %} {% for obj in selected_objects %}

View File

@ -23,7 +23,7 @@
</div> </div>
<div class="text-right"> <div class="text-right">
<button type="submit" name="_confirm" class="btn btn-{{ button_class|default:"danger" }}">Confirm</button> <button type="submit" name="_confirm" class="btn btn-{{ button_class|default:"danger" }}">Confirm</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -37,7 +37,7 @@
<button type="submit" name="_create" class="btn btn-primary">Create</button> <button type="submit" name="_create" class="btn btn-primary">Create</button>
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add Another</button> <button type="submit" name="_addanother" class="btn btn-primary">Create and Add Another</button>
{% endif %} {% endif %}
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a> <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div> </div>
</div> </div>
</form> </form>

View File

@ -1,29 +1,42 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load helpers %} {% load helpers %}
{% if table.model|user_can_change:request.user or table.model|user_can_delete:request.user %} {% if permissions.change or permissions.delete %}
<form method="post" class="form form-horizontal"> <form method="post" class="form form-horizontal">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="redirect_url" value="{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" /> <input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
<input type="hidden" name="pk_all" value="{% for obj in table.data.queryset %}{{ obj.pk|default:'' }}{% if not forloop.last %},{% endif %}{% endfor %}" />
{% if table.paginator.num_pages > 1 %} {% if table.paginator.num_pages > 1 %}
<div id="select_all_box" class="hidden alert alert-info"> <div id="select_all_box" class="hidden panel panel-default">
<div class="checkbox-inline"> <div class="panel-body">
<label for="select_all"> <div class="checkbox-inline">
<input type="checkbox" id="select_all" name="_all" /> <label for="select_all">
Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query <input type="checkbox" id="select_all" name="_all" />
</label> Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
</label>
</div>
<div class="pull-right">
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled="disabled">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit All
</button>
{% endif %}
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled="disabled">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete All
</button>
{% endif %}
</div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% render_table table table_template|default:'table.html' %} {% render_table table table_template|default:'table.html' %}
{% block extra_actions %}{% endblock %} {% block extra_actions %}{% endblock %}
{% if bulk_edit_url and table.model|user_can_change:request.user %} {% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}" class="btn btn-warning btn-sm"> <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
</button> </button>
{% endif %} {% endif %}
{% if bulk_delete_url and table.model|user_can_delete:request.user %} {% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}" class="btn btn-danger btn-sm"> <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
</button> </button>
{% endif %} {% endif %}

View File

@ -55,5 +55,6 @@ class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Tenant model = Tenant
q = forms.CharField(required=False, label='Search')
group = FilterChoiceField(queryset=TenantGroup.objects.annotate(filter_count=Count('tenants')), group = FilterChoiceField(queryset=TenantGroup.objects.annotate(filter_count=Count('tenants')),
to_field_name='slug', null_option=(0, 'None')) to_field_name='slug', null_option=(0, 'None'))

View File

@ -1,12 +1,14 @@
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from extras.models import CustomFieldModel, CustomFieldValue from extras.models import CustomFieldModel, CustomFieldValue
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format from utilities.utils import csv_format
@python_2_unicode_compatible
class TenantGroup(models.Model): class TenantGroup(models.Model):
""" """
An arbitrary collection of Tenants. An arbitrary collection of Tenants.
@ -17,13 +19,14 @@ class TenantGroup(models.Model):
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
def __unicode__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?group={}".format(reverse('tenancy:tenant_list'), self.slug) return "{}?group={}".format(reverse('tenancy:tenant_list'), self.slug)
@python_2_unicode_compatible
class Tenant(CreatedUpdatedModel, CustomFieldModel): class Tenant(CreatedUpdatedModel, CustomFieldModel):
""" """
A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
@ -39,7 +42,7 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
class Meta: class Meta:
ordering = ['group', 'name'] ordering = ['group', 'name']
def __unicode__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):

View File

@ -10,7 +10,7 @@ from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from models import Tenant, TenantGroup from .models import Tenant, TenantGroup
from . import filters, forms, tables from . import filters, forms, tables
@ -21,7 +21,6 @@ from . import filters, forms, tables
class TenantGroupListView(ObjectListView): class TenantGroupListView(ObjectListView):
queryset = TenantGroup.objects.annotate(tenant_count=Count('tenants')) queryset = TenantGroup.objects.annotate(tenant_count=Count('tenants'))
table = tables.TenantGroupTable table = tables.TenantGroupTable
edit_permissions = ['tenancy.change_tenantgroup', 'tenancy.delete_tenantgroup']
template_name = 'tenancy/tenantgroup_list.html' template_name = 'tenancy/tenantgroup_list.html'
@ -37,7 +36,7 @@ class TenantGroupEditView(PermissionRequiredMixin, ObjectEditView):
class TenantGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class TenantGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'tenancy.delete_tenantgroup' permission_required = 'tenancy.delete_tenantgroup'
cls = TenantGroup cls = TenantGroup
default_redirect_url = 'tenancy:tenantgroup_list' default_return_url = 'tenancy:tenantgroup_list'
# #
@ -49,7 +48,6 @@ class TenantListView(ObjectListView):
filter = filters.TenantFilter filter = filters.TenantFilter
filter_form = forms.TenantFilterForm filter_form = forms.TenantFilterForm
table = tables.TenantTable table = tables.TenantTable
edit_permissions = ['tenancy.change_tenant', 'tenancy.delete_tenant']
template_name = 'tenancy/tenant_list.html' template_name = 'tenancy/tenant_list.html'
@ -85,7 +83,7 @@ class TenantEditView(PermissionRequiredMixin, ObjectEditView):
form_class = forms.TenantForm form_class = forms.TenantForm
fields_initial = ['group'] fields_initial = ['group']
template_name = 'tenancy/tenant_edit.html' template_name = 'tenancy/tenant_edit.html'
obj_list_url = 'tenancy:tenant_list' default_return_url = 'tenancy:tenant_list'
class TenantDeleteView(PermissionRequiredMixin, ObjectDeleteView): class TenantDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@ -99,18 +97,20 @@ class TenantBulkImportView(PermissionRequiredMixin, BulkImportView):
form = forms.TenantImportForm form = forms.TenantImportForm
table = tables.TenantTable table = tables.TenantTable
template_name = 'tenancy/tenant_import.html' template_name = 'tenancy/tenant_import.html'
obj_list_url = 'tenancy:tenant_list' default_return_url = 'tenancy:tenant_list'
class TenantBulkEditView(PermissionRequiredMixin, BulkEditView): class TenantBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'tenancy.change_tenant' permission_required = 'tenancy.change_tenant'
cls = Tenant cls = Tenant
filter = filters.TenantFilter
form = forms.TenantBulkEditForm form = forms.TenantBulkEditForm
template_name = 'tenancy/tenant_bulk_edit.html' template_name = 'tenancy/tenant_bulk_edit.html'
default_redirect_url = 'tenancy:tenant_list' default_return_url = 'tenancy:tenant_list'
class TenantBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class TenantBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'tenancy.delete_tenant' permission_required = 'tenancy.delete_tenant'
cls = Tenant cls = Tenant
default_redirect_url = 'tenancy:tenant_list' filter = filters.TenantFilter
default_return_url = 'tenancy:tenant_list'

View File

@ -37,20 +37,38 @@ COLOR_CHOICES = (
('607d8b', 'Dark grey'), ('607d8b', 'Dark grey'),
('111111', 'Black'), ('111111', 'Black'),
) )
NUMERIC_EXPANSION_PATTERN = '\[(\d+-\d+)\]' NUMERIC_EXPANSION_PATTERN = '\[((?:\d+[?:,-])+\d+)\]'
IP4_EXPANSION_PATTERN = '\[([0-9]{1,3}-[0-9]{1,3})\]' IP4_EXPANSION_PATTERN = '\[((?:[0-9]{1,3}[?:,-])+[0-9]{1,3})\]'
IP6_EXPANSION_PATTERN = '\[([0-9a-f]{1,4}-[0-9a-f]{1,4})\]' IP6_EXPANSION_PATTERN = '\[((?:[0-9a-f]{1,4}[?:,-])+[0-9a-f]{1,4})\]'
def parse_numeric_range(string, base=10):
"""
Expand a numeric range (continuous or not) into a decimal or
hexadecimal list, as specified by the base parameter
'0-3,5' => [0, 1, 2, 3, 5]
'2,8-b,d,f' => [2, 8, 9, a, b, d, f]
"""
values = list()
for dash_range in string.split(','):
try:
begin, end = dash_range.split('-')
except ValueError:
begin, end = dash_range, dash_range
begin, end = int(begin.strip()), int(end.strip(), base=base) + 1
values.extend(range(begin, end))
return list(set(values))
def expand_numeric_pattern(string): def expand_numeric_pattern(string):
""" """
Expand a numeric pattern into a list of strings. Examples: Expand a numeric pattern into a list of strings. Examples:
'ge-0/0/[0-3]' => ['ge-0/0/0', 'ge-0/0/1', 'ge-0/0/2', 'ge-0/0/3'] 'ge-0/0/[0-3,5]' => ['ge-0/0/0', 'ge-0/0/1', 'ge-0/0/2', 'ge-0/0/3', 'ge-0/0/5']
'xe-0/[0-3]/[0-7]' => ['xe-0/0/0', 'xe-0/0/1', 'xe-0/0/2', ... 'xe-0/3/5', 'xe-0/3/6', 'xe-0/3/7'] 'xe-0/[0,2-3]/[0-7]' => ['xe-0/0/0', 'xe-0/0/1', 'xe-0/0/2', ... 'xe-0/3/5', 'xe-0/3/6', 'xe-0/3/7']
""" """
lead, pattern, remnant = re.split(NUMERIC_EXPANSION_PATTERN, string, maxsplit=1) lead, pattern, remnant = re.split(NUMERIC_EXPANSION_PATTERN, string, maxsplit=1)
x, y = pattern.split('-') parsed_range = parse_numeric_range(pattern)
for i in range(int(x), int(y) + 1): for i in parsed_range:
if re.search(NUMERIC_EXPANSION_PATTERN, remnant): if re.search(NUMERIC_EXPANSION_PATTERN, remnant):
for string in expand_numeric_pattern(remnant): for string in expand_numeric_pattern(remnant):
yield "{}{}{}".format(lead, i, string) yield "{}{}{}".format(lead, i, string)
@ -61,8 +79,8 @@ def expand_numeric_pattern(string):
def expand_ipaddress_pattern(string, family): def expand_ipaddress_pattern(string, family):
""" """
Expand an IP address pattern into a list of strings. Examples: Expand an IP address pattern into a list of strings. Examples:
'192.0.2.[1-254]/24' => ['192.0.2.1/24', '192.0.2.2/24', '192.0.2.3/24' ... '192.0.2.254/24'] '192.0.2.[1,2,100-250,254]/24' => ['192.0.2.1/24', '192.0.2.2/24', '192.0.2.100/24' ... '192.0.2.250/24', '192.0.2.254/24']
'2001:db8:0:[0-ff]::/64' => ['2001:db8:0:0::/64', '2001:db8:0:1::/64', ... '2001:db8:0:ff::/64'] '2001:db8:0:[0,fd-ff]::/64' => ['2001:db8:0:0::/64', '2001:db8:0:fd::/64', ... '2001:db8:0:ff::/64']
""" """
if family not in [4, 6]: if family not in [4, 6]:
raise Exception("Invalid IP address family: {}".format(family)) raise Exception("Invalid IP address family: {}".format(family))
@ -73,8 +91,8 @@ def expand_ipaddress_pattern(string, family):
regex = IP6_EXPANSION_PATTERN regex = IP6_EXPANSION_PATTERN
base = 16 base = 16
lead, pattern, remnant = re.split(regex, string, maxsplit=1) lead, pattern, remnant = re.split(regex, string, maxsplit=1)
x, y = pattern.split('-') parsed_range = parse_numeric_range(pattern, base)
for i in range(int(x, base), int(y, base) + 1): for i in parsed_range:
if re.search(regex, remnant): if re.search(regex, remnant):
for string in expand_ipaddress_pattern(remnant, family): for string in expand_ipaddress_pattern(remnant, family):
yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), string]) yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), string])
@ -248,7 +266,7 @@ class ExpandableNameField(forms.CharField):
super(ExpandableNameField, self).__init__(*args, **kwargs) super(ExpandableNameField, self).__init__(*args, **kwargs)
if not self.help_text: if not self.help_text:
self.help_text = 'Numeric ranges are supported for bulk creation.<br />'\ self.help_text = 'Numeric ranges are supported for bulk creation.<br />'\
'Example: <code>ge-0/0/[0-47]</code>' 'Example: <code>ge-0/0/[0-23,25,30]</code>'
def to_python(self, value): def to_python(self, value):
if re.search(NUMERIC_EXPANSION_PATTERN, value): if re.search(NUMERIC_EXPANSION_PATTERN, value):
@ -265,7 +283,7 @@ class ExpandableIPAddressField(forms.CharField):
super(ExpandableIPAddressField, self).__init__(*args, **kwargs) super(ExpandableIPAddressField, self).__init__(*args, **kwargs)
if not self.help_text: if not self.help_text:
self.help_text = 'Specify a numeric range to create multiple IPs.<br />'\ self.help_text = 'Specify a numeric range to create multiple IPs.<br />'\
'Example: <code>192.0.2.[1-254]/24</code>' 'Example: <code>192.0.2.[1,5,100-254]/24</code>'
def to_python(self, value): def to_python(self, value):
# Hackish address family detection but it's all we have to work with # Hackish address family detection but it's all we have to work with

View File

@ -17,10 +17,6 @@ class BaseTable(tables.Table):
'class': 'table table-hover', 'class': 'table table-hover',
} }
@property
def model(self):
return self._meta.model
class ToggleColumn(tables.CheckBoxColumn): class ToggleColumn(tables.CheckBoxColumn):

View File

@ -44,24 +44,6 @@ def startswith(value, arg):
return str(value).startswith(arg) return str(value).startswith(arg)
@register.filter()
def user_can_add(model, user):
perm_name = '{}:add_{}'.format(model._meta.app_label, model.__class__.__name__.lower())
return user.has_perm(perm_name)
@register.filter()
def user_can_change(model, user):
perm_name = '{}:change_{}'.format(model._meta.app_label, model.__class__.__name__.lower())
return user.has_perm(perm_name)
@register.filter()
def user_can_delete(model, user):
perm_name = '{}:delete_{}'.format(model._meta.app_label, model.__class__.__name__.lower())
return user.has_perm(perm_name)
# #
# Tags # Tags
# #

View File

@ -3,7 +3,7 @@ from django_tables2 import RequestConfig
from django.contrib import messages from django.contrib import messages
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import transaction, IntegrityError from django.db import transaction, IntegrityError
from django.db.models import ProtectedError from django.db.models import ProtectedError
@ -46,14 +46,12 @@ class ObjectListView(View):
filter: A django-filter FilterSet that is applied to the queryset filter: A django-filter FilterSet that is applied to the queryset
filter_form: The form used to render filter options filter_form: The form used to render filter options
table: The django-tables2 Table used to render the objects list table: The django-tables2 Table used to render the objects list
edit_permissions: Editing controls are displayed only if the user has these permissions
template_name: The name of the template template_name: The name of the template
""" """
queryset = None queryset = None
filter = None filter = None
filter_form = None filter_form = None
table = None table = None
edit_permissions = []
template_name = None template_name = None
def get(self, request): def get(self, request):
@ -95,14 +93,19 @@ class ObjectListView(View):
# Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list # Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list
self.queryset = self.alter_queryset(request) self.queryset = self.alter_queryset(request)
# Compile user model permissions for access from within the template
perm_base_name = '{}.{{}}_{}'.format(model._meta.app_label, model._meta.model_name)
permissions = {p: request.user.has_perm(perm_base_name.format(p)) for p in ['add', 'change', 'delete']}
# Construct the table based on the user's permissions # Construct the table based on the user's permissions
table = self.table(self.queryset) table = self.table(self.queryset)
if 'pk' in table.base_columns and any([request.user.has_perm(perm) for perm in self.edit_permissions]): if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
table.base_columns['pk'].visible = True table.base_columns['pk'].visible = True
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(table) RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(table)
context = { context = {
'table': table, 'table': table,
'permissions': permissions,
'filter_form': self.filter_form(request.GET, label_suffix='') if self.filter_form else None, 'filter_form': self.filter_form(request.GET, label_suffix='') if self.filter_form else None,
'export_templates': ExportTemplate.objects.filter(content_type=object_ct), 'export_templates': ExportTemplate.objects.filter(content_type=object_ct),
} }
@ -126,13 +129,13 @@ class ObjectEditView(View):
form_class: The form used to create or edit the object form_class: The form used to create or edit the object
fields_initial: A set of fields that will be prepopulated in the form from the request parameters fields_initial: A set of fields that will be prepopulated in the form from the request parameters
template_name: The name of the template template_name: The name of the template
obj_list_url: The name of the URL used to display a list of this object type default_return_url: The name of the URL used to display a list of this object type
""" """
model = None model = None
form_class = None form_class = None
fields_initial = [] fields_initial = []
template_name = 'utilities/obj_edit.html' template_name = 'utilities/obj_edit.html'
obj_list_url = None default_return_url = 'home'
def get_object(self, kwargs): def get_object(self, kwargs):
# Look up object by slug or PK. Return None if neither was provided. # Look up object by slug or PK. Return None if neither was provided.
@ -151,9 +154,7 @@ class ObjectEditView(View):
# Determine where to redirect the user after updating an object (or aborting an update). # Determine where to redirect the user after updating an object (or aborting an update).
if obj.pk and hasattr(obj, 'get_absolute_url'): if obj.pk and hasattr(obj, 'get_absolute_url'):
return obj.get_absolute_url() return obj.get_absolute_url()
if self.obj_list_url is not None: return reverse(self.default_return_url)
return reverse(self.obj_list_url)
return reverse('home')
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -166,7 +167,7 @@ class ObjectEditView(View):
'obj': obj, 'obj': obj,
'obj_type': self.model._meta.verbose_name, 'obj_type': self.model._meta.verbose_name,
'form': form, 'form': form,
'cancel_url': self.get_return_url(obj), 'return_url': self.get_return_url(obj),
}) })
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@ -203,7 +204,7 @@ class ObjectEditView(View):
'obj': obj, 'obj': obj,
'obj_type': self.model._meta.verbose_name, 'obj_type': self.model._meta.verbose_name,
'form': form, 'form': form,
'cancel_url': self.get_return_url(obj), 'return_url': self.get_return_url(obj),
}) })
@ -226,10 +227,10 @@ class ObjectDeleteView(View):
else: else:
return get_object_or_404(self.model, pk=kwargs['pk']) return get_object_or_404(self.model, pk=kwargs['pk'])
def get_cancel_url(self, obj): def get_return_url(self, obj):
if hasattr(obj, 'get_absolute_url'): if hasattr(obj, 'get_absolute_url'):
return obj.get_absolute_url() return obj.get_absolute_url()
return reverse('home') return reverse(self.default_return_url)
def get(self, request, **kwargs): def get(self, request, **kwargs):
@ -243,7 +244,7 @@ class ObjectDeleteView(View):
'obj': obj, 'obj': obj,
'form': form, 'form': form,
'obj_type': self.model._meta.verbose_name, 'obj_type': self.model._meta.verbose_name,
'cancel_url': request.GET.get('return_url') or self.get_cancel_url(obj), 'return_url': request.GET.get('return_url') or self.get_return_url(obj),
}) })
def post(self, request, **kwargs): def post(self, request, **kwargs):
@ -272,7 +273,7 @@ class ObjectDeleteView(View):
'obj': obj, 'obj': obj,
'form': form, 'form': form,
'obj_type': self.model._meta.verbose_name, 'obj_type': self.model._meta.verbose_name,
'cancel_url': request.GET.get('return_url') or self.get_cancel_url(obj), 'return_url': request.GET.get('return_url') or self.get_return_url(obj),
}) })
@ -283,12 +284,12 @@ class BulkAddView(View):
form: Form class form: Form class
model: The model of the objects being created model: The model of the objects being created
template_name: The name of the template template_name: The name of the template
redirect_url: Name of the URL to which the user is redirected after creating the objects default_return_url: Name of the URL to which the user is redirected after creating the objects
""" """
form = None form = None
model = None model = None
template_name = None template_name = None
redirect_url = None default_return_url = 'home'
def get(self, request): def get(self, request):
@ -297,7 +298,7 @@ class BulkAddView(View):
return render(request, self.template_name, { return render(request, self.template_name, {
'obj_type': self.model._meta.verbose_name, 'obj_type': self.model._meta.verbose_name,
'form': form, 'form': form,
'cancel_url': reverse(self.redirect_url), 'return_url': reverse(self.default_return_url),
}) })
def post(self, request): def post(self, request):
@ -328,12 +329,12 @@ class BulkAddView(View):
messages.success(request, u"Added {} {}.".format(len(new_objs), self.model._meta.verbose_name_plural)) messages.success(request, u"Added {} {}.".format(len(new_objs), self.model._meta.verbose_name_plural))
if '_addanother' in request.POST: if '_addanother' in request.POST:
return redirect(request.path) return redirect(request.path)
return redirect(self.redirect_url) return redirect(self.default_return_url)
return render(request, self.template_name, { return render(request, self.template_name, {
'form': form, 'form': form,
'obj_type': self.model._meta.verbose_name, 'obj_type': self.model._meta.verbose_name,
'cancel_url': reverse(self.redirect_url), 'return_url': reverse(self.default_return_url),
}) })
@ -344,18 +345,18 @@ class BulkImportView(View):
form: Form class form: Form class
table: The django-tables2 Table used to render the list of imported objects table: The django-tables2 Table used to render the list of imported objects
template_name: The name of the template template_name: The name of the template
obj_list_url: The name of the URL to use for the cancel button default_return_url: The name of the URL to use for the cancel button
""" """
form = None form = None
table = None table = None
template_name = None template_name = None
obj_list_url = None default_return_url = None
def get(self, request): def get(self, request):
return render(request, self.template_name, { return render(request, self.template_name, {
'form': self.form(), 'form': self.form(),
'obj_list_url': self.obj_list_url, 'return_url': self.default_return_url,
}) })
def post(self, request): def post(self, request):
@ -384,7 +385,7 @@ class BulkImportView(View):
return render(request, self.template_name, { return render(request, self.template_name, {
'form': form, 'form': form,
'obj_list_url': self.obj_list_url, 'return_url': self.default_return_url,
}) })
def save_obj(self, obj): def save_obj(self, obj):
@ -397,18 +398,21 @@ class BulkEditView(View):
cls: The model of the objects being edited cls: The model of the objects being edited
parent_cls: The model of the parent object (if any) parent_cls: The model of the parent object (if any)
filter: FilterSet to apply when deleting by QuerySet
form: The form class used to edit objects in bulk form: The form class used to edit objects in bulk
template_name: The name of the template template_name: The name of the template
default_redirect_url: Name of the URL to which the user is redirected after editing the objects default_return_url: Name of the URL to which the user is redirected after editing the objects (can be overriden by
POSTing return_url)
""" """
cls = None cls = None
parent_cls = None parent_cls = None
filter = None
form = None form = None
template_name = None template_name = None
default_redirect_url = None default_return_url = 'home'
def get(self): def get(self):
return redirect(self.default_redirect_url) return redirect(self.default_return_url)
def post(self, request, **kwargs): def post(self, request, **kwargs):
@ -419,19 +423,17 @@ class BulkEditView(View):
parent_obj = None parent_obj = None
# Determine URL to redirect users upon modification of objects # Determine URL to redirect users upon modification of objects
posted_redirect_url = request.POST.get('redirect_url') posted_return_url = request.POST.get('return_url')
if posted_redirect_url and is_safe_url(url=posted_redirect_url, host=request.get_host()): if posted_return_url and is_safe_url(url=posted_return_url, host=request.get_host()):
redirect_url = posted_redirect_url return_url = posted_return_url
elif parent_obj: elif parent_obj:
redirect_url = parent_obj.get_absolute_url() return_url = parent_obj.get_absolute_url()
elif self.default_redirect_url:
redirect_url = reverse(self.default_redirect_url)
else: else:
raise ImproperlyConfigured('No redirect URL has been provided.') return_url = reverse(self.default_return_url)
# Are we editing *all* objects in the queryset or just a selected subset? # Are we editing *all* objects in the queryset or just a selected subset?
if request.POST.get('_all'): if request.POST.get('_all') and self.filter is not None:
pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk] pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk'))]
else: else:
pk_list = [int(pk) for pk in request.POST.getlist('pk')] pk_list = [int(pk) for pk in request.POST.getlist('pk')]
@ -465,7 +467,7 @@ class BulkEditView(View):
msg = u'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural) msg = u'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural)
messages.success(self.request, msg) messages.success(self.request, msg)
UserAction.objects.log_bulk_edit(request.user, ContentType.objects.get_for_model(self.cls), msg) UserAction.objects.log_bulk_edit(request.user, ContentType.objects.get_for_model(self.cls), msg)
return redirect(redirect_url) return redirect(return_url)
else: else:
form = self.form(self.cls, initial={'pk': pk_list}) form = self.form(self.cls, initial={'pk': pk_list})
@ -473,12 +475,12 @@ class BulkEditView(View):
selected_objects = self.cls.objects.filter(pk__in=pk_list) selected_objects = self.cls.objects.filter(pk__in=pk_list)
if not selected_objects: if not selected_objects:
messages.warning(request, u"No {} were selected.".format(self.cls._meta.verbose_name_plural)) messages.warning(request, u"No {} were selected.".format(self.cls._meta.verbose_name_plural))
return redirect(redirect_url) return redirect(return_url)
return render(request, self.template_name, { return render(request, self.template_name, {
'form': form, 'form': form,
'selected_objects': selected_objects, 'selected_objects': selected_objects,
'cancel_url': redirect_url, 'return_url': return_url,
}) })
def update_custom_fields(self, pk_list, form, fields, nullified_fields): def update_custom_fields(self, pk_list, form, fields, nullified_fields):
@ -535,15 +537,18 @@ class BulkDeleteView(View):
cls: The model of the objects being deleted cls: The model of the objects being deleted
parent_cls: The model of the parent object (if any) parent_cls: The model of the parent object (if any)
filter: FilterSet to apply when deleting by QuerySet
form: The form class used to delete objects in bulk form: The form class used to delete objects in bulk
template_name: The name of the template template_name: The name of the template
default_redirect_url: Name of the URL to which the user is redirected after deleting the objects default_return_url: Name of the URL to which the user is redirected after deleting the objects (can be overriden by
POSTing return_url)
""" """
cls = None cls = None
parent_cls = None parent_cls = None
filter = None
form = None form = None
template_name = 'utilities/confirm_bulk_delete.html' template_name = 'utilities/confirm_bulk_delete.html'
default_redirect_url = None default_return_url = 'home'
def post(self, request, **kwargs): def post(self, request, **kwargs):
@ -554,19 +559,17 @@ class BulkDeleteView(View):
parent_obj = None parent_obj = None
# Determine URL to redirect users upon deletion of objects # Determine URL to redirect users upon deletion of objects
posted_redirect_url = request.POST.get('redirect_url') posted_return_url = request.POST.get('return_url')
if posted_redirect_url and is_safe_url(url=posted_redirect_url, host=request.get_host()): if posted_return_url and is_safe_url(url=posted_return_url, host=request.get_host()):
redirect_url = posted_redirect_url return_url = posted_return_url
elif parent_obj: elif parent_obj:
redirect_url = parent_obj.get_absolute_url() return_url = parent_obj.get_absolute_url()
elif self.default_redirect_url:
redirect_url = reverse(self.default_redirect_url)
else: else:
raise ImproperlyConfigured('No redirect URL has been provided.') return_url = reverse(self.default_return_url)
# Are we deleting *all* objects in the queryset or just a selected subset? # Are we deleting *all* objects in the queryset or just a selected subset?
if request.POST.get('_all'): if request.POST.get('_all') and self.filter is not None:
pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk] pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk'))]
else: else:
pk_list = [int(pk) for pk in request.POST.getlist('pk')] pk_list = [int(pk) for pk in request.POST.getlist('pk')]
@ -582,27 +585,27 @@ class BulkDeleteView(View):
deleted_count = queryset.delete()[1][self.cls._meta.label] deleted_count = queryset.delete()[1][self.cls._meta.label]
except ProtectedError as e: except ProtectedError as e:
handle_protectederror(list(queryset), request, e) handle_protectederror(list(queryset), request, e)
return redirect(redirect_url) return redirect(return_url)
msg = u'Deleted {} {}'.format(deleted_count, self.cls._meta.verbose_name_plural) msg = u'Deleted {} {}'.format(deleted_count, self.cls._meta.verbose_name_plural)
messages.success(request, msg) messages.success(request, msg)
UserAction.objects.log_bulk_delete(request.user, ContentType.objects.get_for_model(self.cls), msg) UserAction.objects.log_bulk_delete(request.user, ContentType.objects.get_for_model(self.cls), msg)
return redirect(redirect_url) return redirect(return_url)
else: else:
form = form_cls(initial={'pk': pk_list}) form = form_cls(initial={'pk': pk_list, 'return_url': return_url})
selected_objects = self.cls.objects.filter(pk__in=pk_list) selected_objects = self.cls.objects.filter(pk__in=pk_list)
if not selected_objects: if not selected_objects:
messages.warning(request, u"No {} were selected for deletion.".format(self.cls._meta.verbose_name_plural)) messages.warning(request, u"No {} were selected for deletion.".format(self.cls._meta.verbose_name_plural))
return redirect(redirect_url) return redirect(return_url)
return render(request, self.template_name, { return render(request, self.template_name, {
'form': form, 'form': form,
'parent_obj': parent_obj, 'parent_obj': parent_obj,
'obj_type_plural': self.cls._meta.verbose_name_plural, 'obj_type_plural': self.cls._meta.verbose_name_plural,
'selected_objects': selected_objects, 'selected_objects': selected_objects,
'cancel_url': redirect_url, 'return_url': return_url,
}) })
def get_form(self): def get_form(self):