From 99cd78cbbf73ae6542b2aaf886905e8bd82c86ac Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 4 Jan 2017 15:31:10 -0500 Subject: [PATCH 01/32] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 9ea64e6b7..0d80592e2 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.8.1' +VERSION = '1.8.2-dev' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: From 23c6451524b677adc820b0388586df659b16570b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 4 Jan 2017 16:56:28 -0500 Subject: [PATCH 02/32] Fixes #776: Prevent circuits from appearing twice while searching --- netbox/circuits/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index e2ec5f55b..117a106ea 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -99,4 +99,4 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): Q(terminations__xconnect_id__icontains=value) | Q(terminations__pp_info__icontains=value) | Q(comments__icontains=value) - ) + ).distinct() From 381eb664cf74d390be1c5e9f34ad25d207312150 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 4 Jan 2017 17:16:04 -0500 Subject: [PATCH 03/32] Added alternative installations section --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e4ba912a3..66c35250b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ NetBox is an IP address management (IPAM) and data center infrastructure managem NetBox runs as a web application atop the [Django](https://www.djangoproject.com/) Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox). -The complete documentation for Netbox can be found at [Read the Docs](http://netbox.readthedocs.io/en/latest/). +The complete documentation for NetBox can be found at [Read the Docs](http://netbox.readthedocs.io/en/stable/). Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https://groups.google.com/forum/#!forum/netbox-discuss), or join us on IRC in **#netbox** on **irc.freenode.net**! @@ -25,6 +25,9 @@ Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https # Installation -Please see [the documentation](http://netbox.readthedocs.io/en/latest/) for instructions on installing NetBox. +Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases) and run `upgrade.sh`. -To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases) and run `upgrade.sh`. +## Alternative Installations + +* [Docker container](http://netbox.readthedocs.io/en/stable/installation/docker/) +* [Heroku deployment](https://heroku.com/deploy?template=https://github.com/BILDQUADRAT/netbox/tree/heroku) (via [@mraerino](https://github.com/BILDQUADRAT/netbox/tree/heroku)) From 09fe328c3f22407a97c720e53c4b3c3faaa9f7a9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Jan 2017 15:31:41 -0500 Subject: [PATCH 04/32] Standardized template names --- netbox/templates/dcim/device.html | 20 +++++++++---------- netbox/templates/dcim/device_import.html | 2 +- .../templates/dcim/device_import_child.html | 2 +- netbox/templates/dcim/device_inventory.html | 2 +- .../templates/dcim/device_lldp_neighbors.html | 2 +- .../{_consoleport.html => consoleport.html} | 0 ...serverport.html => consoleserverport.html} | 0 ..._device_header.html => device_header.html} | 0 ..._header.html => device_import_header.html} | 0 .../inc/{_devicebay.html => devicebay.html} | 0 .../inc/{_interface.html => interface.html} | 0 .../inc/{_ipaddress.html => ipaddress.html} | 0 .../{_poweroutlet.html => poweroutlet.html} | 0 .../inc/{_powerport.html => powerport.html} | 0 ...ack_elevation.html => rack_elevation.html} | 0 .../dcim/inc/{_service.html => service.html} | 0 netbox/templates/dcim/rack.html | 4 ++-- 17 files changed, 16 insertions(+), 16 deletions(-) rename netbox/templates/dcim/inc/{_consoleport.html => consoleport.html} (100%) rename netbox/templates/dcim/inc/{_consoleserverport.html => consoleserverport.html} (100%) rename netbox/templates/dcim/inc/{_device_header.html => device_header.html} (100%) rename netbox/templates/dcim/inc/{_device_import_header.html => device_import_header.html} (100%) rename netbox/templates/dcim/inc/{_devicebay.html => devicebay.html} (100%) rename netbox/templates/dcim/inc/{_interface.html => interface.html} (100%) rename netbox/templates/dcim/inc/{_ipaddress.html => ipaddress.html} (100%) rename netbox/templates/dcim/inc/{_poweroutlet.html => poweroutlet.html} (100%) rename netbox/templates/dcim/inc/{_powerport.html => powerport.html} (100%) rename netbox/templates/dcim/inc/{_rack_elevation.html => rack_elevation.html} (100%) rename netbox/templates/dcim/inc/{_service.html => service.html} (100%) diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 5bfb70cd3..53273da47 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -6,7 +6,7 @@ {% block title %}{{ device }}{% endblock %} {% block content %} -{% include 'dcim/inc/_device_header.html' with active_tab='info' %} +{% include 'dcim/inc/device_header.html' with active_tab='info' %}
@@ -183,7 +183,7 @@ {% if ip_addresses %} {% for ip in ip_addresses %} - {% include 'dcim/inc/_ipaddress.html' %} + {% include 'dcim/inc/ipaddress.html' %} {% endfor %}
{% elif interfaces or mgmt_interfaces %} @@ -212,7 +212,7 @@ {% if services %} {% for service in services %} - {% include 'dcim/inc/_service.html' %} + {% include 'dcim/inc/service.html' %} {% endfor %}
{% else %} @@ -234,7 +234,7 @@
{% for iface in mgmt_interfaces %} - {% include 'dcim/inc/_interface.html' with icon='wrench' %} + {% include 'dcim/inc/interface.html' with icon='wrench' %} {% empty %} {% endfor %} {% for cp in console_ports %} - {% include 'dcim/inc/_consoleport.html' %} + {% include 'dcim/inc/consoleport.html' %} {% empty %} {% endfor %} {% for pp in power_ports %} - {% include 'dcim/inc/_powerport.html' %} + {% include 'dcim/inc/powerport.html' %} {% empty %} + + + + +
@@ -246,7 +246,7 @@
@@ -258,7 +258,7 @@
@@ -349,7 +349,7 @@ {% for devicebay in device_bays %} - {% include 'dcim/inc/_devicebay.html' with selectable=True %} + {% include 'dcim/inc/devicebay.html' with selectable=True %} {% empty %} @@ -401,7 +401,7 @@
No device bays defined
{% for iface in interfaces %} - {% include 'dcim/inc/_interface.html' with selectable=True %} + {% include 'dcim/inc/interface.html' with selectable=True %} {% empty %} @@ -458,7 +458,7 @@
No interfaces defined
{% for csp in cs_ports %} - {% include 'dcim/inc/_consoleserverport.html' with selectable=True %} + {% include 'dcim/inc/consoleserverport.html' with selectable=True %} {% empty %} @@ -510,7 +510,7 @@
No console server ports defined
{% for po in power_outlets %} - {% include 'dcim/inc/_poweroutlet.html' with selectable=True %} + {% include 'dcim/inc/poweroutlet.html' with selectable=True %} {% empty %} diff --git a/netbox/templates/dcim/device_import.html b/netbox/templates/dcim/device_import.html index a603ab4ef..c075abe34 100644 --- a/netbox/templates/dcim/device_import.html +++ b/netbox/templates/dcim/device_import.html @@ -5,7 +5,7 @@ {% block title %}Device Import{% endblock %} {% block content %} -{% include 'dcim/inc/_device_import_header.html' %} +{% include 'dcim/inc/device_import_header.html' %}
diff --git a/netbox/templates/dcim/device_import_child.html b/netbox/templates/dcim/device_import_child.html index eed987d46..cf5d96e79 100644 --- a/netbox/templates/dcim/device_import_child.html +++ b/netbox/templates/dcim/device_import_child.html @@ -5,7 +5,7 @@ {% block title %}Device Import{% endblock %} {% block content %} -{% include 'dcim/inc/_device_import_header.html' with active_tab='child_import' %} +{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
diff --git a/netbox/templates/dcim/device_inventory.html b/netbox/templates/dcim/device_inventory.html index b2c94c1dd..6e19d8fe8 100644 --- a/netbox/templates/dcim/device_inventory.html +++ b/netbox/templates/dcim/device_inventory.html @@ -3,7 +3,7 @@ {% block title %}{{ device }} - Inventory{% endblock %} {% block content %} -{% include 'dcim/inc/_device_header.html' with active_tab='inventory' %} +{% include 'dcim/inc/device_header.html' with active_tab='inventory' %}
diff --git a/netbox/templates/dcim/device_lldp_neighbors.html b/netbox/templates/dcim/device_lldp_neighbors.html index 415456a29..6a5446c3b 100644 --- a/netbox/templates/dcim/device_lldp_neighbors.html +++ b/netbox/templates/dcim/device_lldp_neighbors.html @@ -3,7 +3,7 @@ {% block title %}{{ device }} - LLDP Neighbors{% endblock %} {% block content %} -{% include 'dcim/inc/_device_header.html' with active_tab='lldp-neighbors' %} +{% include 'dcim/inc/device_header.html' with active_tab='lldp-neighbors' %}
LLDP Neighbors diff --git a/netbox/templates/dcim/inc/_consoleport.html b/netbox/templates/dcim/inc/consoleport.html similarity index 100% rename from netbox/templates/dcim/inc/_consoleport.html rename to netbox/templates/dcim/inc/consoleport.html diff --git a/netbox/templates/dcim/inc/_consoleserverport.html b/netbox/templates/dcim/inc/consoleserverport.html similarity index 100% rename from netbox/templates/dcim/inc/_consoleserverport.html rename to netbox/templates/dcim/inc/consoleserverport.html diff --git a/netbox/templates/dcim/inc/_device_header.html b/netbox/templates/dcim/inc/device_header.html similarity index 100% rename from netbox/templates/dcim/inc/_device_header.html rename to netbox/templates/dcim/inc/device_header.html diff --git a/netbox/templates/dcim/inc/_device_import_header.html b/netbox/templates/dcim/inc/device_import_header.html similarity index 100% rename from netbox/templates/dcim/inc/_device_import_header.html rename to netbox/templates/dcim/inc/device_import_header.html diff --git a/netbox/templates/dcim/inc/_devicebay.html b/netbox/templates/dcim/inc/devicebay.html similarity index 100% rename from netbox/templates/dcim/inc/_devicebay.html rename to netbox/templates/dcim/inc/devicebay.html diff --git a/netbox/templates/dcim/inc/_interface.html b/netbox/templates/dcim/inc/interface.html similarity index 100% rename from netbox/templates/dcim/inc/_interface.html rename to netbox/templates/dcim/inc/interface.html diff --git a/netbox/templates/dcim/inc/_ipaddress.html b/netbox/templates/dcim/inc/ipaddress.html similarity index 100% rename from netbox/templates/dcim/inc/_ipaddress.html rename to netbox/templates/dcim/inc/ipaddress.html diff --git a/netbox/templates/dcim/inc/_poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html similarity index 100% rename from netbox/templates/dcim/inc/_poweroutlet.html rename to netbox/templates/dcim/inc/poweroutlet.html diff --git a/netbox/templates/dcim/inc/_powerport.html b/netbox/templates/dcim/inc/powerport.html similarity index 100% rename from netbox/templates/dcim/inc/_powerport.html rename to netbox/templates/dcim/inc/powerport.html diff --git a/netbox/templates/dcim/inc/_rack_elevation.html b/netbox/templates/dcim/inc/rack_elevation.html similarity index 100% rename from netbox/templates/dcim/inc/_rack_elevation.html rename to netbox/templates/dcim/inc/rack_elevation.html diff --git a/netbox/templates/dcim/inc/_service.html b/netbox/templates/dcim/inc/service.html similarity index 100% rename from netbox/templates/dcim/inc/_service.html rename to netbox/templates/dcim/inc/service.html diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 2d0e065fd..d73a0b560 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -195,13 +195,13 @@

Front

- {% include 'dcim/inc/_rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 %} + {% include 'dcim/inc/rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 %}

Rear

- {% include 'dcim/inc/_rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 %} + {% include 'dcim/inc/rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 %}
From dbf9840b269d384b3202b99c08ebe6a191637c8b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Jan 2017 15:37:15 -0500 Subject: [PATCH 05/32] Corrected permissions for device component form rendering --- netbox/templates/dcim/inc/consoleport.html | 2 +- netbox/templates/dcim/inc/consoleserverport.html | 2 +- netbox/templates/dcim/inc/devicebay.html | 2 +- netbox/templates/dcim/inc/interface.html | 2 +- netbox/templates/dcim/inc/poweroutlet.html | 2 +- netbox/templates/dcim/inc/powerport.html | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/netbox/templates/dcim/inc/consoleport.html b/netbox/templates/dcim/inc/consoleport.html index 43b353bb1..e6c816780 100644 --- a/netbox/templates/dcim/inc/consoleport.html +++ b/netbox/templates/dcim/inc/consoleport.html @@ -1,5 +1,5 @@ - {% if selectable and perms.dcim.delete_consoleport %} + {% if selectable and perms.dcim.change_consoleport or perms.dcim.delete_consoleport %}
diff --git a/netbox/templates/dcim/inc/consoleserverport.html b/netbox/templates/dcim/inc/consoleserverport.html index b9c5e8e59..d3c923e43 100644 --- a/netbox/templates/dcim/inc/consoleserverport.html +++ b/netbox/templates/dcim/inc/consoleserverport.html @@ -1,5 +1,5 @@ - {% if selectable and perms.dcim.delete_consoleserverport %} + {% if selectable and perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %} diff --git a/netbox/templates/dcim/inc/devicebay.html b/netbox/templates/dcim/inc/devicebay.html index 8996cd225..eacb27440 100644 --- a/netbox/templates/dcim/inc/devicebay.html +++ b/netbox/templates/dcim/inc/devicebay.html @@ -1,5 +1,5 @@ - {% if selectable and perms.dcim.delete_devicebay %} + {% if selectable and perms.dcim.change_devicebay or perms.dcim.delete_devicebay %} diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index dc7ac4ecf..bfb44b75d 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -1,5 +1,5 @@ - {% if selectable and perms.dcim.delete_interface %} + {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %} diff --git a/netbox/templates/dcim/inc/poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html index 18619a37d..241ebc15c 100644 --- a/netbox/templates/dcim/inc/poweroutlet.html +++ b/netbox/templates/dcim/inc/poweroutlet.html @@ -1,5 +1,5 @@ - {% if selectable and perms.dcim.delete_poweroutlet %} + {% if selectable and perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %} diff --git a/netbox/templates/dcim/inc/powerport.html b/netbox/templates/dcim/inc/powerport.html index 7d519599d..aacb96839 100644 --- a/netbox/templates/dcim/inc/powerport.html +++ b/netbox/templates/dcim/inc/powerport.html @@ -1,5 +1,5 @@ - {% if selectable and perms.dcim.delete_powerport %} + {% if selectable and perms.dcim.change_powerport or perms.dcim.delete_powerport %} From ac72e90dcc000e8ae23c65e18b7452b96eeaa92b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Jan 2017 16:12:07 -0500 Subject: [PATCH 06/32] Fixes #778: Refactored order_interfaces() to fix InterfaceTemplate ordering within a table --- netbox/dcim/models.py | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index c95bca426..790128340 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -182,7 +182,7 @@ RPC_CLIENT_CHOICES = [ ] -def order_interfaces(queryset, sql_col, primary_ordering=tuple()): +def order_interfaces(queryset): """ Attempt to match interface names by their slot/position identifiers and order according. Matching is done using the following pattern: @@ -190,8 +190,8 @@ def order_interfaces(queryset, sql_col, primary_ordering=tuple()): {a}/{b}/{c}:{d} Interfaces are ordered first by field a, then b, then c, and finally d. Leading text (which typically indicates the - interface's type) is ignored. If any fields are not contained by an interface name, those fields are treated as - None. 'None' is ordered after all other values. For example: + interface's type) is then used to order any duplicate slot/position tuples. If any fields are not contained by an + interface name, those fields are treated as null. Null values are ordered after all other values. For example: et-0/0/0 et-0/0/1 @@ -210,12 +210,9 @@ def order_interfaces(queryset, sql_col, primary_ordering=tuple()): ... vlan1 vlan10 - - :param queryset: The base queryset to be ordered - :param sql_col: Table and name of the SQL column which contains the interface name (ex: ''dcim_interface.name') - :param primary_ordering: A tuple of fields which take ordering precedence before the interface name (optional) """ - ordering = primary_ordering + ('_id1', '_id2', '_id3', '_id4') + sql_col = '{}.name'.format(queryset.model._meta.db_table) + ordering = ('_id1', '_id2', '_id3', '_id4', 'name') return queryset.extra(select={ '_id1': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col), '_id2': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col), @@ -701,11 +698,17 @@ class PowerOutletTemplate(models.Model): return self.name -class InterfaceTemplateManager(models.Manager): +class InterfaceManager(models.Manager): def get_queryset(self): - qs = super(InterfaceTemplateManager, self).get_queryset() - return order_interfaces(qs, 'dcim_interfacetemplate.name', ('device_type',)) + qs = super(InterfaceManager, self).get_queryset() + return order_interfaces(qs) + + def virtual(self): + return self.get_queryset().filter(form_factor=IFACE_FF_VIRTUAL) + + def physical(self): + return self.get_queryset().exclude(form_factor=IFACE_FF_VIRTUAL) class InterfaceTemplate(models.Model): @@ -717,7 +720,7 @@ class InterfaceTemplate(models.Model): form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS) mgmt_only = models.BooleanField(default=False, verbose_name='Management only') - objects = InterfaceTemplateManager() + objects = InterfaceManager() class Meta: ordering = ['device_type', 'name'] @@ -1094,19 +1097,6 @@ class PowerOutlet(models.Model): return self.device.get_absolute_url() -class InterfaceManager(models.Manager): - - def get_queryset(self): - qs = super(InterfaceManager, self).get_queryset() - return order_interfaces(qs, 'dcim_interface.name', ('device',)) - - def virtual(self): - return self.get_queryset().filter(form_factor=IFACE_FF_VIRTUAL) - - def physical(self): - return self.get_queryset().exclude(form_factor=IFACE_FF_VIRTUAL) - - class Interface(models.Model): """ A physical data interface within a Device. An Interface can connect to exactly one other Interface via the creation From 73ae87aa5733ebc98749c80bff8e53f437e22157 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Jan 2017 17:06:30 -0500 Subject: [PATCH 07/32] Updated circuits documentation --- docs/data-model/circuits.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/data-model/circuits.md b/docs/data-model/circuits.md index 563e5df7c..226c62814 100644 --- a/docs/data-model/circuits.md +++ b/docs/data-model/circuits.md @@ -4,29 +4,30 @@ The circuits component of NetBox deals with the management of long-haul Internet A provider is any entity which provides some form of connectivity. This obviously includes carriers which offer Internet and private transit service. However, it might also include Internet exchange (IX) points and even organizations with whom you peer directly. -Each provider may be assigned an autonomous system number (ASN) for reference. Each provider can also be assigned account and contact information, as well as miscellaneous comments. +Each provider may be assigned an autonomous system number (ASN), an account number, and contact information. --- # Circuits -A circuit represents a single physical data link connecting two endpoints. Each circuit belongs to a provider and must be assigned circuit ID which is unique to that provider. Each circuit must also be assigned to a site, and may optionally be connected to a specific interface on a specific device within that site. - -NetBox also tracks miscellaneous circuit attributes (most of which are optional), including: - -* Date of installation -* Port speed -* Commit rate -* Cross-connect ID -* Patch panel information +A circuit represents a single physical data link connecting two endpoints. Each circuit belongs to a provider and must be assigned a circuit ID which is unique to that provider. ### Circuit Types -Circuits can be classified by type. For example: +Circuits are classified by type. For example: * Internet transit * Out-of-band connectivity * Peering * Private backhaul -Each circuit must be assigned exactly one circuit type. \ No newline at end of file +Circuit types are fully customizable. + +### Circuit Terminations + +A circuit may have one or two terminations, annotated as the "A" and "Z" sides of the circuit. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites. + +Each circuit termination can be tied to a site, or to a specific device and interface within that site. Each termination can be assigned a separate downstream and upstream speed independent from one another. Fields are also available to track cross-connect and patch panel details. + +!!! note + A circuit represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit. From 1486a8901ad732035a73dd5f4787ee6374892065 Mon Sep 17 00:00:00 2001 From: Josh Galvez Date: Thu, 5 Jan 2017 16:02:05 -0700 Subject: [PATCH 08/32] Update CONTRIBUTING.md Change Reddit to Google Groups Mailing List --- CONTRIBUTING.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 85119102b..0ae5beb57 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,10 +8,9 @@ If you encounter any issues installing or using NetBox, try one of the following Join the #netbox channel on [Freenode IRC](https://freenode.net/). You can connect to Freenode at irc.freenode.net using an IRC client, or you can use their [webchat client](https://webchat.freenode.net/). -### Reddit +### Mailing List -We have established [/r/netbox](https://www.reddit.com/r/netbox) on Reddit for NetBox issues and general discussion. -Reddit registration is free and does not require providing an email address (although it is encouraged). +We have established a Google Groups Mailing List for issues and general discussion. You can find us [here]( https://groups.google.com/forum/#!forum/netbox-discuss). ## Reporting Bugs @@ -24,7 +23,7 @@ click "add a reaction" in the top right corner of the issue and add a thumbs up comment describing how it's affecting your installation. This will allow us to prioritize bugs based on how many users are affected. -* If you haven't found an existing issue that describes your suspected bug, please inquire about it on IRC or Reddit. +* If you haven't found an existing issue that describes your suspected bug, please inquire about it on IRC or Google Groups. **Do not** file an issue until you have received confirmation that it is in fact a bug. Invalid issues are very distracting and slow the pace at which NetBox is developed. From c9e7c12463a529177a73df3d545756663218ac93 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 6 Jan 2017 12:59:49 -0500 Subject: [PATCH 09/32] Closes #284: Added interface_ordering field to DeviceType --- netbox/dcim/api/serializers.py | 9 +- netbox/dcim/forms.py | 12 ++- .../0025_devicetype_add_interface_ordering.py | 20 +++++ netbox/dcim/models.py | 87 +++++++++---------- netbox/dcim/tests/test_apis.py | 1 + netbox/dcim/views.py | 34 +++++--- netbox/templates/dcim/devicetype.html | 4 + netbox/templates/dcim/devicetype_edit.html | 6 ++ 8 files changed, 105 insertions(+), 68 deletions(-) create mode 100644 netbox/dcim/migrations/0025_devicetype_add_interface_ordering.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 3b1ab720c..698e7d2d6 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -138,7 +138,8 @@ class DeviceTypeSerializer(CustomFieldSerializer, serializers.ModelSerializer): class Meta: model = DeviceType fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', - 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields'] + 'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', + 'comments', 'custom_fields'] def get_subdevice_role(self, obj): return { @@ -198,9 +199,9 @@ class DeviceTypeDetailSerializer(DeviceTypeSerializer): class Meta(DeviceTypeSerializer.Meta): fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', - 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields', - 'console_port_templates', 'cs_port_templates', 'power_port_templates', 'power_outlet_templates', - 'interface_templates'] + 'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', + 'comments', 'custom_fields', 'console_port_templates', 'cs_port_templates', 'power_port_templates', + 'power_outlet_templates', 'interface_templates'] # diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index d82f28090..cb35ff881 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -17,9 +17,9 @@ from formfields import MACAddressFormField from .models import ( DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, - Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, - Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, - Rack, RackGroup, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD + Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, + Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, + RACK_WIDTH_CHOICES, Rack, RackGroup, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD ) @@ -263,13 +263,17 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm): class Meta: model = DeviceType fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', - 'is_pdu', 'is_network_device', 'subdevice_role', 'comments'] + 'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments'] + labels = { + 'interface_ordering': 'Order interfaces by', + } class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput) manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False) u_height = forms.IntegerField(min_value=1, required=False) + interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False) class Meta: nullable_fields = [] diff --git a/netbox/dcim/migrations/0025_devicetype_add_interface_ordering.py b/netbox/dcim/migrations/0025_devicetype_add_interface_ordering.py new file mode 100644 index 000000000..d1263cb89 --- /dev/null +++ b/netbox/dcim/migrations/0025_devicetype_add_interface_ordering.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2017-01-06 16:56 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0024_site_add_contact_fields'), + ] + + operations = [ + migrations.AddField( + model_name='devicetype', + name='interface_ordering', + field=models.PositiveSmallIntegerField(choices=[[1, b'Slot/position'], [2, b'Name (alphabetically)']], default=1), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 790128340..030de3436 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -56,6 +56,13 @@ SUBDEVICE_ROLE_CHOICES = ( (SUBDEVICE_ROLE_CHILD, 'Child'), ) +IFACE_ORDERING_POSITION = 1 +IFACE_ORDERING_NAME = 2 +IFACE_ORDERING_CHOICES = [ + [IFACE_ORDERING_POSITION, 'Slot/position'], + [IFACE_ORDERING_NAME, 'Name (alphabetically)'] +] + # Virtual IFACE_FF_VIRTUAL = 0 # Ethernet @@ -182,45 +189,6 @@ RPC_CLIENT_CHOICES = [ ] -def order_interfaces(queryset): - """ - Attempt to match interface names by their slot/position identifiers and order according. Matching is done using the - following pattern: - - {a}/{b}/{c}:{d} - - Interfaces are ordered first by field a, then b, then c, and finally d. Leading text (which typically indicates the - interface's type) is then used to order any duplicate slot/position tuples. If any fields are not contained by an - interface name, those fields are treated as null. Null values are ordered after all other values. For example: - - et-0/0/0 - et-0/0/1 - et-0/1/0 - xe-0/1/1:0 - xe-0/1/1:1 - xe-0/1/1:2 - xe-0/1/1:3 - et-0/1/2 - ... - et-0/1/9 - et-0/1/10 - et-0/1/11 - et-1/0/0 - et-1/0/1 - ... - vlan1 - vlan10 - """ - sql_col = '{}.name'.format(queryset.model._meta.db_table) - ordering = ('_id1', '_id2', '_id3', '_id4', 'name') - return queryset.extra(select={ - '_id1': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col), - '_id2': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col), - '_id3': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?$') AS integer)".format(sql_col), - '_id4': "CAST(SUBSTRING({} FROM ':([0-9]+)$') AS integer)".format(sql_col), - }).order_by(*ordering) - - # # Sites # @@ -548,6 +516,8 @@ class DeviceType(models.Model, CustomFieldModel): u_height = models.PositiveSmallIntegerField(verbose_name='Height (U)', default=1) is_full_depth = models.BooleanField(default=True, verbose_name="Is full depth", help_text="Device consumes both front and rear rack faces") + interface_ordering = models.PositiveSmallIntegerField(choices=IFACE_ORDERING_CHOICES, + default=IFACE_ORDERING_POSITION) is_console_server = models.BooleanField(default=False, verbose_name='Is a console server', help_text="This type of device has console server ports") is_pdu = models.BooleanField(default=False, verbose_name='Is a PDU', @@ -700,15 +670,40 @@ class PowerOutletTemplate(models.Model): class InterfaceManager(models.Manager): - def get_queryset(self): - qs = super(InterfaceManager, self).get_queryset() - return order_interfaces(qs) + def order_naturally(self, method=IFACE_ORDERING_POSITION): + """ + Naturally order interfaces by their name and numeric position. The sort method must be one of the defined + IFACE_ORDERING_CHOICES (typically indicated by a parent Device's DeviceType). - def virtual(self): - return self.get_queryset().filter(form_factor=IFACE_FF_VIRTUAL) + To order interfaces naturally, the `name` field is split into five distinct components: leading text (name), + slot, subslot, position, and channel: - def physical(self): - return self.get_queryset().exclude(form_factor=IFACE_FF_VIRTUAL) + {name}{slot}/{subslot}/{position}:{channel} + + Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would + be parsed as follows: + + name = 'GigabitEthernet' + slot = None + subslot = 0 + position = 1 + channel = None + + The chosen sorting method will determine which fields are ordered first in the query. + """ + queryset = self.get_queryset() + sql_col = '{}.name'.format(queryset.model._meta.db_table) + ordering = { + IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_name'), + IFACE_ORDERING_NAME: ('_name', '_slot', '_subslot', '_position', '_channel'), + }[method] + return queryset.extra(select={ + '_name': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col), + '_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col), + '_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col), + '_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?$') AS integer)".format(sql_col), + '_channel': "CAST(SUBSTRING({} FROM ':([0-9]+)$') AS integer)".format(sql_col), + }).order_by(*ordering) class InterfaceTemplate(models.Model): diff --git a/netbox/dcim/tests/test_apis.py b/netbox/dcim/tests/test_apis.py index 1305d7e37..3cef01701 100644 --- a/netbox/dcim/tests/test_apis.py +++ b/netbox/dcim/tests/test_apis.py @@ -232,6 +232,7 @@ class DeviceTypeTest(APITestCase): 'part_number', 'u_height', 'is_full_depth', + 'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 58c38bfb0..3ac48a82f 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -358,10 +358,14 @@ def devicetype(request, pk): poweroutlet_table = tables.PowerOutletTemplateTable( natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) ) - mgmt_interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype, - mgmt_only=True)) - interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype, - mgmt_only=False)) + mgmt_interface_table = tables.InterfaceTemplateTable( + InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype, + mgmt_only=True) + ) + interface_table = tables.InterfaceTemplateTable( + InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype, + mgmt_only=False) + ) devicebay_table = tables.DeviceBayTemplateTable( natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) ) @@ -597,16 +601,18 @@ def device(request, pk): power_outlets = natsorted( PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name') ) - interfaces = Interface.objects.filter(device=device, mgmt_only=False).select_related( - 'connected_as_a__interface_b__device', - 'connected_as_b__interface_a__device', - 'circuit_termination__circuit', - ) - mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True).select_related( - 'connected_as_a__interface_b__device', - 'connected_as_b__interface_a__device', - 'circuit_termination__circuit', - ) + interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ + .filter(device=device, mgmt_only=False).select_related( + 'connected_as_a__interface_b__device', + 'connected_as_b__interface_a__device', + 'circuit_termination__circuit', + ) + mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ + .filter(device=device, mgmt_only=True).select_related( + 'connected_as_a__interface_b__device', + 'connected_as_b__interface_a__device', + 'circuit_termination__circuit', + ) device_bays = natsorted( DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), key=attrgetter('name') diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 9bc16d146..a9a9fa130 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -72,6 +72,10 @@ {% endif %} + + + + diff --git a/netbox/templates/dcim/devicetype_edit.html b/netbox/templates/dcim/devicetype_edit.html index 929da06b8..d2a107607 100644 --- a/netbox/templates/dcim/devicetype_edit.html +++ b/netbox/templates/dcim/devicetype_edit.html @@ -11,6 +11,12 @@ {% render_field form.part_number %} {% render_field form.u_height %} {% render_field form.is_full_depth %} + {% render_field form.interface_ordering %} + + +
+
Function
+
{% render_field form.is_console_server %} {% render_field form.is_pdu %} {% render_field form.is_network_device %} From 424c2a59d6bd726cbd9437b7483aeeba602a183a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 6 Jan 2017 16:50:57 -0500 Subject: [PATCH 10/32] Table rendering optimizations --- netbox/dcim/views.py | 8 ++++---- netbox/ipam/views.py | 22 +++++++++++++--------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 3ac48a82f..fa433259a 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -359,12 +359,12 @@ def devicetype(request, pk): natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) ) mgmt_interface_table = tables.InterfaceTemplateTable( - InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype, - mgmt_only=True) + list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype, + mgmt_only=True)) ) interface_table = tables.InterfaceTemplateTable( - InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype, - mgmt_only=False) + list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype, + mgmt_only=False)) ) devicebay_table = tables.DeviceBayTemplateTable( natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index c7fc4ea31..3e62142ae 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -102,8 +102,10 @@ class VRFListView(ObjectListView): def vrf(request, pk): vrf = get_object_or_404(VRF.objects.all(), pk=pk) - prefixes = Prefix.objects.filter(vrf=vrf) - prefix_table = tables.PrefixBriefTable(prefixes) + prefix_table = tables.PrefixBriefTable( + list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')) + ) + prefix_table.exclude = ('vrf',) return render(request, 'ipam/vrf.html', { 'vrf': vrf, @@ -401,7 +403,7 @@ def prefix(request, pk): # Duplicate prefixes table duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\ .select_related('site', 'role') - duplicate_prefix_table = tables.PrefixBriefTable(duplicate_prefixes) + duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes)) # Child prefixes table if prefix.vrf: @@ -504,18 +506,20 @@ def ipaddress(request, pk): ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk) # Parent prefixes table - parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip)) - parent_prefixes_table = tables.PrefixBriefTable(parent_prefixes) + parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip))\ + .select_related('site', 'role') + parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes)) + parent_prefixes_table.exclude = ('vrf',) # Duplicate IPs table duplicate_ips = IPAddress.objects.filter(vrf=ipaddress.vrf, address=str(ipaddress.address))\ .exclude(pk=ipaddress.pk).select_related('interface__device', 'nat_inside') - duplicate_ips_table = tables.IPAddressBriefTable(duplicate_ips) + duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips)) # Related IP table related_ips = IPAddress.objects.select_related('interface__device').exclude(address=str(ipaddress.address))\ .filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)) - related_ips_table = tables.IPAddressBriefTable(related_ips) + related_ips_table = tables.IPAddressBriefTable(list(related_ips)) return render(request, 'ipam/ipaddress.html', { 'ipaddress': ipaddress, @@ -695,8 +699,8 @@ class VLANListView(ObjectListView): def vlan(request, pk): vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk) - prefixes = Prefix.objects.filter(vlan=vlan) - prefix_table = tables.PrefixBriefTable(prefixes) + prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role') + prefix_table = tables.PrefixBriefTable(list(prefixes)) return render(request, 'ipam/vlan.html', { 'vlan': vlan, From 97c0f23c676de7e726e938bf0b61087834cf9fd9 Mon Sep 17 00:00:00 2001 From: "Raymond P. Burkholder" Date: Fri, 13 Jan 2017 08:49:43 -0400 Subject: [PATCH 11/32] Add description field to TenantSerializer This might be just an oversight. Other data models do include the description in their serialisers. The API produces the description field with this change. --- netbox/tenancy/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index bde6f3345..6d22561ef 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -30,7 +30,7 @@ class TenantSerializer(CustomFieldSerializer, serializers.ModelSerializer): class Meta: model = Tenant - fields = ['id', 'name', 'slug', 'group', 'comments', 'custom_fields'] + fields = ['id', 'name', 'slug', 'group', 'description', 'comments', 'custom_fields'] class TenantNestedSerializer(TenantSerializer): From 466c505bb88fda2e12471343e16fe0250817fe15 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 13 Jan 2017 09:30:59 -0500 Subject: [PATCH 12/32] Corrected PEP8 errors --- netbox/dcim/views.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index fa433259a..548627cd5 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -601,18 +601,16 @@ def device(request, pk): power_outlets = natsorted( PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name') ) - interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ - .filter(device=device, mgmt_only=False).select_related( - 'connected_as_a__interface_b__device', - 'connected_as_b__interface_a__device', - 'circuit_termination__circuit', - ) - mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ - .filter(device=device, mgmt_only=True).select_related( - 'connected_as_a__interface_b__device', - 'connected_as_b__interface_a__device', - 'circuit_termination__circuit', - ) + interfaces = Interface.objects.filter(device=device, mgmt_only=False).select_related( + 'connected_as_a__interface_b__device', + 'connected_as_b__interface_a__device', + 'circuit_termination__circuit', + ).order_naturally(device.device_type.interface_ordering) + mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True).select_related( + 'connected_as_a__interface_b__device', + 'connected_as_b__interface_a__device', + 'circuit_termination__circuit', + ).order_naturally(device.device_type.interface_ordering) device_bays = natsorted( DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), key=attrgetter('name') From cfaf8b9157db535a5289a656188628842c781ddb Mon Sep 17 00:00:00 2001 From: Zach Moody Date: Mon, 16 Jan 2017 16:28:04 -0600 Subject: [PATCH 13/32] added duplicates() method to IPAddress and Prefix model managers. refactored condition on IPAddress and Prefix clean method to use new manager method. --- netbox/ipam/models.py | 48 +++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index fa42d68f1..62faacb18 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -262,6 +262,12 @@ class PrefixQuerySet(NullsFirstQuerySet): return queryset return filter(lambda p: p.depth <= limit, queryset) + def duplicates(self, prefix): + return self.filter( + vrf=prefix.vrf, + prefix=str(prefix) + ).exclude(pk=prefix.pk) + class Prefix(CreatedUpdatedModel, CustomFieldModel): """ @@ -299,7 +305,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): return reverse('ipam:prefix', args=[self.pk]) def clean(self): - # Disallow host masks if self.prefix: if self.prefix.version == 4 and self.prefix.prefixlen == 32: @@ -311,6 +316,16 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): 'prefix': "Cannot create host addresses (/128) as prefixes. Create an IPv6 address instead." }) + if ((not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique)): + dupes = Prefix.objects.duplicates(self) + if dupes: + raise ValidationError({ + 'prefix': "Duplicate prefix found in {}: {}".format( + "VRF {}".format(self.vrf) if self.vrf else "global table", + dupes.first(), + ) + }) + def save(self, *args, **kwargs): if self.prefix: # Clear host bits from prefix @@ -361,6 +376,12 @@ class IPAddressManager(models.Manager): qs = super(IPAddressManager, self).get_queryset() return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host') + def duplicates(self, ip_obj): + return self.filter( + vrf=ip_obj.vrf, + address__net_host=str(ip_obj.address.ip) + ).exclude(pk=ip_obj.pk) + class IPAddress(CreatedUpdatedModel, CustomFieldModel): """ @@ -401,21 +422,22 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): return reverse('ipam:ipaddress', args=[self.pk]) def clean(self): + if ((not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique)): + dupes = IPAddress.objects.duplicates(self) + if dupes: + raise ValidationError({ + 'address': "Duplicate IP address found in global table: {}".format(dupes.first()) + }) # Enforce unique IP space if applicable - if self.vrf and self.vrf.enforce_unique: - duplicate_ips = IPAddress.objects.filter(vrf=self.vrf, address__net_host=str(self.address.ip))\ - .exclude(pk=self.pk) - if duplicate_ips: + if ((not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique)): + dupes = IPAddress.objects.duplicates(self) + if dupes: raise ValidationError({ - 'address': "Duplicate IP address found in VRF {}: {}".format(self.vrf, duplicate_ips.first()) - }) - elif not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE: - duplicate_ips = IPAddress.objects.filter(vrf=None, address__net_host=str(self.address.ip))\ - .exclude(pk=self.pk) - if duplicate_ips: - raise ValidationError({ - 'address': "Duplicate IP address found in global table: {}".format(duplicate_ips.first()) + 'address': "Duplicate IP address found in {}: {}".format( + "VRF {}".format(self.vrf) if self.vrf else "global table", + dupes.first(), + ) }) def save(self, *args, **kwargs): From eedec192ba0a21c09f1108b61cd590a0bb679820 Mon Sep 17 00:00:00 2001 From: Zach Moody Date: Mon, 16 Jan 2017 16:28:25 -0600 Subject: [PATCH 14/32] Added model tests for duplicate prefix and IPs. --- netbox/ipam/tests/__init__.py | 0 netbox/ipam/tests/test_models.py | 72 ++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 netbox/ipam/tests/__init__.py create mode 100644 netbox/ipam/tests/test_models.py diff --git a/netbox/ipam/tests/__init__.py b/netbox/ipam/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py new file mode 100644 index 000000000..49ca3b09b --- /dev/null +++ b/netbox/ipam/tests/test_models.py @@ -0,0 +1,72 @@ +import netaddr + +from django.test import TestCase, override_settings + +from ipam.models import IPAddress, Prefix, VRF +from django.core.exceptions import ValidationError + + +class TestPrefix(TestCase): + + fixtures = [ + 'dcim', + 'ipam' + ] + + def test_create(self): + prefix = Prefix.objects.create( + prefix=netaddr.IPNetwork('10.1.1.0/24'), + status=1 + ) + self.assertIsNone(prefix.clean()) + + @override_settings(ENFORCE_GLOBAL_UNIQUE=True) + def test_duplicate_global(self): + prefix = Prefix.objects.create( + prefix=netaddr.IPNetwork('10.1.1.0/24'), + status=1 + ) + self.assertRaises(ValidationError, prefix.clean) + + @override_settings(ENFORCE_GLOBAL_UNIQUE=True) + def test_duplicate_vrf(self): + pfx_kwargs = { + "prefix": netaddr.IPNetwork('10.1.1.0/24'), + "status": 1, + "vrf": VRF.objects.create(name='Test', rd='1:1'), + } + Prefix.objects.create(**pfx_kwargs) + dup_prefix = Prefix.objects.create(**pfx_kwargs) + self.assertRaises(ValidationError, dup_prefix.clean) + + +class TestIPAddress(TestCase): + + fixtures = [ + 'dcim', + 'ipam' + ] + + def test_create(self): + address = IPAddress.objects.create( + address=netaddr.IPNetwork('10.0.254.1/24'), + ) + self.assertIsNone(address.clean()) + + @override_settings(ENFORCE_GLOBAL_UNIQUE=True) + def test_duplicate_global(self): + address = IPAddress.objects.create( + address=netaddr.IPNetwork('10.0.254.1/24'), + ) + self.assertRaises(ValidationError, address.clean) + + @override_settings(ENFORCE_GLOBAL_UNIQUE=True) + def test_duplicate_vrf(self): + pfx_kwargs = { + "address": netaddr.IPNetwork('10.0.254.1/24'), + "status": 1, + "vrf": VRF.objects.create(name='Test', rd='1:1'), + } + IPAddress.objects.create(**pfx_kwargs) + dup_address = IPAddress.objects.create(**pfx_kwargs) + self.assertRaises(ValidationError, dup_address.clean) From 485a21f13ee73c25ccd99a029a4f3616eee35bc2 Mon Sep 17 00:00:00 2001 From: Zach Moody Date: Mon, 16 Jan 2017 16:52:03 -0600 Subject: [PATCH 15/32] cleaned up IPAddress clean() to be more like Prefix's --- netbox/ipam/models.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 62faacb18..8e45277ab 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -422,19 +422,13 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): return reverse('ipam:ipaddress', args=[self.pk]) def clean(self): - if ((not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique)): - dupes = IPAddress.objects.duplicates(self) - if dupes: - raise ValidationError({ - 'address': "Duplicate IP address found in global table: {}".format(dupes.first()) - }) # Enforce unique IP space if applicable if ((not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique)): dupes = IPAddress.objects.duplicates(self) if dupes: raise ValidationError({ - 'address': "Duplicate IP address found in {}: {}".format( + 'address': "Duplicate IP Address found in {}: {}".format( "VRF {}".format(self.vrf) if self.vrf else "global table", dupes.first(), ) From edf29e7b9b38821650dbba6a762b188a44948558 Mon Sep 17 00:00:00 2001 From: Zach Moody Date: Mon, 16 Jan 2017 18:14:34 -0600 Subject: [PATCH 16/32] moved duplicates() method to model instead of manager. --- netbox/ipam/models.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 8e45277ab..fc404dbda 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -262,12 +262,6 @@ class PrefixQuerySet(NullsFirstQuerySet): return queryset return filter(lambda p: p.depth <= limit, queryset) - def duplicates(self, prefix): - return self.filter( - vrf=prefix.vrf, - prefix=str(prefix) - ).exclude(pk=prefix.pk) - class Prefix(CreatedUpdatedModel, CustomFieldModel): """ @@ -304,6 +298,12 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): def get_absolute_url(self): return reverse('ipam:prefix', args=[self.pk]) + def duplicates(self): + return Prefix.objects.filter( + vrf=self.vrf, + prefix=str(self.prefix) + ).exclude(pk=self.pk) + def clean(self): # Disallow host masks if self.prefix: @@ -317,7 +317,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): }) if ((not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique)): - dupes = Prefix.objects.duplicates(self) + dupes = self.duplicates() if dupes: raise ValidationError({ 'prefix': "Duplicate prefix found in {}: {}".format( @@ -376,12 +376,6 @@ class IPAddressManager(models.Manager): qs = super(IPAddressManager, self).get_queryset() return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host') - def duplicates(self, ip_obj): - return self.filter( - vrf=ip_obj.vrf, - address__net_host=str(ip_obj.address.ip) - ).exclude(pk=ip_obj.pk) - class IPAddress(CreatedUpdatedModel, CustomFieldModel): """ @@ -421,11 +415,17 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): def get_absolute_url(self): return reverse('ipam:ipaddress', args=[self.pk]) + def duplicates(self): + return IPAddress.objects.filter( + vrf=self.vrf, + address__net_host=str(self.address.ip) + ).exclude(pk=self.pk) + def clean(self): # Enforce unique IP space if applicable if ((not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique)): - dupes = IPAddress.objects.duplicates(self) + dupes = self.duplicates() if dupes: raise ValidationError({ 'address': "Duplicate IP Address found in {}: {}".format( From ab706d24408929fa8144e83d1e320572c4cd8966 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 17 Jan 2017 12:32:54 -0500 Subject: [PATCH 17/32] Follow-up to #804 --- netbox/ipam/models.py | 34 ++++++------ netbox/ipam/tests/test_models.py | 88 ++++++++++++++------------------ 2 files changed, 53 insertions(+), 69 deletions(-) diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index fc404dbda..78bbf7caa 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -298,13 +298,11 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): def get_absolute_url(self): return reverse('ipam:prefix', args=[self.pk]) - def duplicates(self): - return Prefix.objects.filter( - vrf=self.vrf, - prefix=str(self.prefix) - ).exclude(pk=self.pk) + def get_duplicates(self): + return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk) def clean(self): + # Disallow host masks if self.prefix: if self.prefix.version == 4 and self.prefix.prefixlen == 32: @@ -316,13 +314,14 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): 'prefix': "Cannot create host addresses (/128) as prefixes. Create an IPv6 address instead." }) - if ((not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique)): - dupes = self.duplicates() - if dupes: + # Enforce unique IP space if applicable + if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique): + duplicate_prefixes = self.get_duplicates() + if duplicate_prefixes: raise ValidationError({ 'prefix': "Duplicate prefix found in {}: {}".format( "VRF {}".format(self.vrf) if self.vrf else "global table", - dupes.first(), + duplicate_prefixes.first(), ) }) @@ -415,22 +414,19 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): def get_absolute_url(self): return reverse('ipam:ipaddress', args=[self.pk]) - def duplicates(self): - return IPAddress.objects.filter( - vrf=self.vrf, - address__net_host=str(self.address.ip) - ).exclude(pk=self.pk) + def get_duplicates(self): + return IPAddress.objects.filter(vrf=self.vrf, address__net_host=str(self.address.ip)).exclude(pk=self.pk) def clean(self): # Enforce unique IP space if applicable - if ((not self.vrf and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique)): - dupes = self.duplicates() - if dupes: + if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique): + duplicate_ips = self.get_duplicates() + if duplicate_ips: raise ValidationError({ - 'address': "Duplicate IP Address found in {}: {}".format( + 'address': "Duplicate IP address found in {}: {}".format( "VRF {}".format(self.vrf) if self.vrf else "global table", - dupes.first(), + duplicate_ips.first(), ) }) diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index 49ca3b09b..3385c643f 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -8,65 +8,53 @@ from django.core.exceptions import ValidationError class TestPrefix(TestCase): - fixtures = [ - 'dcim', - 'ipam' - ] - - def test_create(self): - prefix = Prefix.objects.create( - prefix=netaddr.IPNetwork('10.1.1.0/24'), - status=1 - ) - self.assertIsNone(prefix.clean()) - - @override_settings(ENFORCE_GLOBAL_UNIQUE=True) + @override_settings(ENFORCE_GLOBAL_UNIQUE=False) def test_duplicate_global(self): - prefix = Prefix.objects.create( - prefix=netaddr.IPNetwork('10.1.1.0/24'), - status=1 - ) - self.assertRaises(ValidationError, prefix.clean) + Prefix.objects.create(prefix=netaddr.IPNetwork('192.0.2.0/24')) + duplicate_prefix = Prefix(prefix=netaddr.IPNetwork('192.0.2.0/24')) + self.assertIsNone(duplicate_prefix.clean()) @override_settings(ENFORCE_GLOBAL_UNIQUE=True) + def test_duplicate_global_unique(self): + Prefix.objects.create(prefix=netaddr.IPNetwork('192.0.2.0/24')) + duplicate_prefix = Prefix(prefix=netaddr.IPNetwork('192.0.2.0/24')) + self.assertRaises(ValidationError, duplicate_prefix.clean) + def test_duplicate_vrf(self): - pfx_kwargs = { - "prefix": netaddr.IPNetwork('10.1.1.0/24'), - "status": 1, - "vrf": VRF.objects.create(name='Test', rd='1:1'), - } - Prefix.objects.create(**pfx_kwargs) - dup_prefix = Prefix.objects.create(**pfx_kwargs) - self.assertRaises(ValidationError, dup_prefix.clean) + vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=False) + Prefix.objects.create(vrf=vrf, prefix=netaddr.IPNetwork('192.0.2.0/24')) + duplicate_prefix = Prefix(vrf=vrf, prefix=netaddr.IPNetwork('192.0.2.0/24')) + self.assertIsNone(duplicate_prefix.clean()) + + def test_duplicate_vrf_unique(self): + vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=True) + Prefix.objects.create(vrf=vrf, prefix=netaddr.IPNetwork('192.0.2.0/24')) + duplicate_prefix = Prefix(vrf=vrf, prefix=netaddr.IPNetwork('192.0.2.0/24')) + self.assertRaises(ValidationError, duplicate_prefix.clean) class TestIPAddress(TestCase): - fixtures = [ - 'dcim', - 'ipam' - ] - - def test_create(self): - address = IPAddress.objects.create( - address=netaddr.IPNetwork('10.0.254.1/24'), - ) - self.assertIsNone(address.clean()) - - @override_settings(ENFORCE_GLOBAL_UNIQUE=True) + @override_settings(ENFORCE_GLOBAL_UNIQUE=False) def test_duplicate_global(self): - address = IPAddress.objects.create( - address=netaddr.IPNetwork('10.0.254.1/24'), - ) - self.assertRaises(ValidationError, address.clean) + IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24')) + duplicate_ip = IPAddress(address=netaddr.IPNetwork('192.0.2.1/24')) + self.assertIsNone(duplicate_ip.clean()) @override_settings(ENFORCE_GLOBAL_UNIQUE=True) + def test_duplicate_global_unique(self): + IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24')) + duplicate_ip = IPAddress(address=netaddr.IPNetwork('192.0.2.1/24')) + self.assertRaises(ValidationError, duplicate_ip.clean) + def test_duplicate_vrf(self): - pfx_kwargs = { - "address": netaddr.IPNetwork('10.0.254.1/24'), - "status": 1, - "vrf": VRF.objects.create(name='Test', rd='1:1'), - } - IPAddress.objects.create(**pfx_kwargs) - dup_address = IPAddress.objects.create(**pfx_kwargs) - self.assertRaises(ValidationError, dup_address.clean) + vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=False) + IPAddress.objects.create(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24')) + duplicate_ip = IPAddress(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24')) + self.assertIsNone(duplicate_ip.clean()) + + def test_duplicate_vrf_unique(self): + vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=True) + IPAddress.objects.create(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24')) + duplicate_ip = IPAddress(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24')) + self.assertRaises(ValidationError, duplicate_ip.clean) From 0ad267082206173d9a4c0293019614b4efaa37a5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 17 Jan 2017 14:46:29 -0500 Subject: [PATCH 18/32] Closes #805: Linkify site column in device table --- netbox/dcim/tables.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 94d359ac0..442e2f8fb 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -311,7 +311,8 @@ class DeviceTable(BaseTable): status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='') name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site') + site = tables.LinkColumn('dcim:site', accessor=Accessor('rack.site'), args=[Accessor('rack.site.slug')], + verbose_name='Site') rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack') device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role') device_type = tables.LinkColumn('dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type', @@ -327,7 +328,8 @@ class DeviceTable(BaseTable): class DeviceImportTable(BaseTable): name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site') + site = tables.LinkColumn('dcim:site', accessor=Accessor('rack.site'), args=[Accessor('rack.site.slug')], + verbose_name='Site') rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack') position = tables.Column(verbose_name='Position') device_role = tables.Column(verbose_name='Role') From 03859d72877b77e39afd3998d255b0ad8a177404 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 17 Jan 2017 14:52:39 -0500 Subject: [PATCH 19/32] Closes #803: Clarify that no child objects are deleted when deleting a prefix --- netbox/ipam/views.py | 1 + netbox/templates/ipam/prefix_delete.html | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 netbox/templates/ipam/prefix_delete.html diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 3e62142ae..3f991a7bc 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -444,6 +444,7 @@ class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_prefix' model = Prefix redirect_url = 'ipam:prefix_list' + template_name = 'ipam/prefix_delete.html' class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView): diff --git a/netbox/templates/ipam/prefix_delete.html b/netbox/templates/ipam/prefix_delete.html new file mode 100644 index 000000000..5ea39dc4c --- /dev/null +++ b/netbox/templates/ipam/prefix_delete.html @@ -0,0 +1,5 @@ +{% extends 'utilities/obj_delete.html' %} + +{% block message_extra %} +

Note: This will not delete any child prefixes or IP addresses.

+{% endblock %} From 07997b24ca06c518305caf6bb92027686d9e755d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 17 Jan 2017 15:01:30 -0500 Subject: [PATCH 20/32] Fixes #785: Trigger validation error when importing a prefix assigned to a nonexistent VLAN --- netbox/ipam/forms.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 7cb04cc60..9c016782e 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -215,6 +215,8 @@ class PrefixFromCSVForm(forms.ModelForm): elif vlan_vid and site: try: self.instance.vlan = VLAN.objects.get(site=site, vid=vlan_vid) + except VLAN.DoesNotExist: + self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site)) except VLAN.MultipleObjectsReturned: self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid)) elif vlan_vid: From b3f20aa233e877e4d6f0a07607c047fdce8a4bc7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 17 Jan 2017 15:18:03 -0500 Subject: [PATCH 21/32] Closes #783: Add a description field to the Circuit model --- netbox/circuits/api/serializers.py | 3 ++- netbox/circuits/filters.py | 1 + netbox/circuits/forms.py | 7 ++++--- .../0007_circuit_add_description.py | 20 +++++++++++++++++++ netbox/circuits/models.py | 2 ++ netbox/circuits/tables.py | 5 ++--- netbox/templates/circuits/circuit.html | 10 ++++++++++ netbox/templates/circuits/circuit_edit.html | 1 + netbox/templates/circuits/circuit_import.html | 7 ++++++- 9 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 netbox/circuits/migrations/0007_circuit_add_description.py diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 1b894afe0..947aa9860 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -62,7 +62,8 @@ class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer): class Meta: model = Circuit - fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'comments', 'terminations', 'custom_fields'] + fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', + 'terminations', 'custom_fields'] class CircuitNestedSerializer(CircuitSerializer): diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 117a106ea..fa57a74dc 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -98,5 +98,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): Q(cid__icontains=value) | Q(terminations__xconnect_id__icontains=value) | Q(terminations__pp_info__icontains=value) | + Q(description__icontains=value) | Q(comments__icontains=value) ).distinct() diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index dc59bc0c7..a1777bb16 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -86,7 +86,7 @@ class CircuitForm(BootstrapMixin, CustomFieldForm): class Meta: model = Circuit - fields = ['cid', 'type', 'provider', 'tenant', 'install_date', 'commit_rate', 'comments'] + fields = ['cid', 'type', 'provider', 'tenant', 'install_date', 'commit_rate', 'description', 'comments'] help_texts = { 'cid': "Unique circuit ID", 'install_date': "Format: YYYY-MM-DD", @@ -104,7 +104,7 @@ class CircuitFromCSVForm(forms.ModelForm): class Meta: model = Circuit - fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate'] + fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description'] class CircuitImportForm(BootstrapMixin, BulkImportForm): @@ -117,10 +117,11 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False) tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)') + description = forms.CharField(max_length=100, required=False) comments = CommentField(widget=SmallTextarea) class Meta: - nullable_fields = ['tenant', 'commit_rate', 'comments'] + nullable_fields = ['tenant', 'commit_rate', 'description', 'comments'] class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): diff --git a/netbox/circuits/migrations/0007_circuit_add_description.py b/netbox/circuits/migrations/0007_circuit_add_description.py new file mode 100644 index 000000000..023e5890a --- /dev/null +++ b/netbox/circuits/migrations/0007_circuit_add_description.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2017-01-17 20:08 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0006_terminations'), + ] + + operations = [ + migrations.AddField( + model_name='circuit', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index d43778657..e0f42a78f 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -97,6 +97,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT) install_date = models.DateField(blank=True, null=True, verbose_name='Date installed') commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)') + description = models.CharField(max_length=100, blank=True) comments = models.TextField(blank=True) custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') @@ -118,6 +119,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): self.tenant.name if self.tenant else None, self.install_date.isoformat() if self.install_date else None, self.commit_rate, + self.description, ]) def _get_termination(self, side): diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index 34236d843..ab877a8ce 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -60,9 +60,8 @@ class CircuitTable(BaseTable): args=[Accessor('termination_a.site.slug')]) z_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_z.site'), orderable=False, args=[Accessor('termination_z.site.slug')]) - commit_rate = tables.Column(accessor=Accessor('commit_rate_human'), order_by=Accessor('commit_rate'), - verbose_name='Commit Rate') + description = tables.Column(verbose_name='Description') class Meta(BaseTable.Meta): model = Circuit - fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'commit_rate') + fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description') diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index 5591c64b9..ab54b45a5 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -92,6 +92,16 @@ {% endif %}
+ + + +
No power outlets defined
Interface Ordering{{ devicetype.get_interface_ordering_display }}
Instances {{ devicetype.instances.count }}
Description + {% if circuit.description %} + {{ circuit.description }} + {% else %} + N/A + {% endif %} +
{% with circuit.get_custom_fields as custom_fields %} diff --git a/netbox/templates/circuits/circuit_edit.html b/netbox/templates/circuits/circuit_edit.html index 67d18d1ae..6b5e4497d 100644 --- a/netbox/templates/circuits/circuit_edit.html +++ b/netbox/templates/circuits/circuit_edit.html @@ -11,6 +11,7 @@ {% render_field form.tenant %} {% render_field form.install_date %} {% render_field form.commit_rate %} + {% render_field form.description %} {% if form.custom_fields %} diff --git a/netbox/templates/circuits/circuit_import.html b/netbox/templates/circuits/circuit_import.html index fec364b60..fec7ff65e 100644 --- a/netbox/templates/circuits/circuit_import.html +++ b/netbox/templates/circuits/circuit_import.html @@ -58,10 +58,15 @@
Commited rate in Kbps (optional) 2000
DescriptionShort description (optional)Primary for voice

Example

-
IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000
+
IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000,Primary for voice
{% endblock %} From c2642815303b3aae8104ded34def022820c3e389 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 17 Jan 2017 15:33:55 -0500 Subject: [PATCH 22/32] Add an empty label (global) to IPAddressBulkAddForm VRF field --- netbox/ipam/forms.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 9c016782e..2f6f0af84 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -336,7 +336,7 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm): class IPAddressBulkAddForm(BootstrapMixin, forms.Form): address = ExpandableIPAddressField() - vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF') + vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF', empty_label='Global') tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) status = forms.ChoiceField(choices=IPADDRESS_STATUS_CHOICES) description = forms.CharField(max_length=100, required=False) @@ -346,9 +346,11 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form): site = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False, widget=forms.Select(attrs={'filter-for': 'rack'})) rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False, - widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}', display_field='display_name', attrs={'filter-for': 'device'})) + widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}', + display_field='display_name', attrs={'filter-for': 'device'})) device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False, - widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}', display_field='display_name', attrs={'filter-for': 'interface'})) + widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}', + display_field='display_name', attrs={'filter-for': 'interface'})) livesearch = forms.CharField(required=False, label='Device', widget=Livesearch( query_key='q', query_url='dcim-api:device_list', field_to_update='device') ) From 7f3b358571427a74fe9c21fa958db10588ecd7e3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 17 Jan 2017 15:46:43 -0500 Subject: [PATCH 23/32] Fixes #807: Redirect user back to form when adding IP addresses in bulk and "create and add another" is clicked --- netbox/utilities/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 3422b2d9d..125bb6584 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -8,7 +8,7 @@ from django.core.urlresolvers import reverse from django.db import transaction, IntegrityError from django.db.models import ProtectedError from django.forms import CharField, ModelMultipleChoiceField, MultipleHiddenInput, TypedChoiceField -from django.http import HttpResponse, HttpResponseRedirect +from django.http import HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.template import TemplateSyntaxError from django.utils.http import is_safe_url @@ -326,6 +326,8 @@ class BulkAddView(View): if not form.errors: messages.success(request, u"Added {} {}.".format(len(new_objs), self.model._meta.verbose_name_plural)) + if '_addanother' in request.POST: + return redirect(request.path) return redirect(self.redirect_url) return render(request, self.template_name, { From f8a4f1b24f2487d46a347c2ab253de0158235557 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 17 Jan 2017 16:06:19 -0500 Subject: [PATCH 24/32] Closes #797: Add description column to VLANs table --- netbox/ipam/tables.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 2c04b97c3..f4ceffd60 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -136,7 +136,7 @@ class VRFTable(BaseTable): name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name') rd = tables.Column(verbose_name='RD') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - description = tables.Column(orderable=False, verbose_name='Description') + description = tables.Column(verbose_name='Description') class Meta(BaseTable.Meta): model = VRF @@ -182,7 +182,7 @@ class AggregateTable(BaseTable): child_count = tables.Column(verbose_name='Prefixes') get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization') date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added') - description = tables.Column(orderable=False, verbose_name='Description') + description = tables.Column(verbose_name='Description') class Meta(BaseTable.Meta): model = Aggregate @@ -219,7 +219,7 @@ class PrefixTable(BaseTable): site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN') role = tables.TemplateColumn(PREFIX_ROLE_LINK, verbose_name='Role') - description = tables.Column(orderable=False, verbose_name='Description') + description = tables.Column(verbose_name='Description') class Meta(BaseTable.Meta): model = Prefix @@ -255,7 +255,7 @@ class IPAddressTable(BaseTable): device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False, verbose_name='Device') interface = tables.Column(orderable=False, verbose_name='Interface') - description = tables.Column(orderable=False, verbose_name='Description') + description = tables.Column(verbose_name='Description') class Meta(BaseTable.Meta): model = IPAddress @@ -310,7 +310,8 @@ class VLANTable(BaseTable): tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') role = tables.TemplateColumn(VLAN_ROLE_LINK, verbose_name='Role') + description = tables.Column(verbose_name='Description') class Meta(BaseTable.Meta): model = VLAN - fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role') + fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description') From 769537fe98cba2ac1c09a47b5a940a2a134d2b6f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 18 Jan 2017 09:55:57 -0500 Subject: [PATCH 25/32] Fixes #810: Suppress unique IP validation on invalid IP addresses and prefixes --- netbox/ipam/models.py | 45 +++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 78bbf7caa..37501735e 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -303,8 +303,9 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): def clean(self): - # Disallow host masks if self.prefix: + + # Disallow host masks if self.prefix.version == 4 and self.prefix.prefixlen == 32: raise ValidationError({ 'prefix': "Cannot create host addresses (/32) as prefixes. Create an IPv4 address instead." @@ -314,16 +315,16 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): 'prefix': "Cannot create host addresses (/128) as prefixes. Create an IPv6 address instead." }) - # Enforce unique IP space if applicable - if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique): - duplicate_prefixes = self.get_duplicates() - if duplicate_prefixes: - raise ValidationError({ - 'prefix': "Duplicate prefix found in {}: {}".format( - "VRF {}".format(self.vrf) if self.vrf else "global table", - duplicate_prefixes.first(), - ) - }) + # Enforce unique IP space (if applicable) + if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique): + duplicate_prefixes = self.get_duplicates() + if duplicate_prefixes: + raise ValidationError({ + 'prefix': "Duplicate prefix found in {}: {}".format( + "VRF {}".format(self.vrf) if self.vrf else "global table", + duplicate_prefixes.first(), + ) + }) def save(self, *args, **kwargs): if self.prefix: @@ -419,16 +420,18 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): def clean(self): - # Enforce unique IP space if applicable - if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique): - duplicate_ips = self.get_duplicates() - if duplicate_ips: - raise ValidationError({ - 'address': "Duplicate IP address found in {}: {}".format( - "VRF {}".format(self.vrf) if self.vrf else "global table", - duplicate_ips.first(), - ) - }) + if self.address: + + # Enforce unique IP space (if applicable) + if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique): + duplicate_ips = self.get_duplicates() + if duplicate_ips: + raise ValidationError({ + 'address': "Duplicate IP address found in {}: {}".format( + "VRF {}".format(self.vrf) if self.vrf else "global table", + duplicate_ips.first(), + ) + }) def save(self, *args, **kwargs): if self.address: From fc7f88d2a2d9972e99fe56112f83e325bba0b550 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 18 Jan 2017 11:55:48 -0500 Subject: [PATCH 26/32] Regression fix: order_naturally() must come first in the queryset definition --- netbox/dcim/views.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 548627cd5..fa433259a 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -601,16 +601,18 @@ def device(request, pk): power_outlets = natsorted( PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name') ) - interfaces = Interface.objects.filter(device=device, mgmt_only=False).select_related( - 'connected_as_a__interface_b__device', - 'connected_as_b__interface_a__device', - 'circuit_termination__circuit', - ).order_naturally(device.device_type.interface_ordering) - mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True).select_related( - 'connected_as_a__interface_b__device', - 'connected_as_b__interface_a__device', - 'circuit_termination__circuit', - ).order_naturally(device.device_type.interface_ordering) + interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ + .filter(device=device, mgmt_only=False).select_related( + 'connected_as_a__interface_b__device', + 'connected_as_b__interface_a__device', + 'circuit_termination__circuit', + ) + mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ + .filter(device=device, mgmt_only=True).select_related( + 'connected_as_a__interface_b__device', + 'connected_as_b__interface_a__device', + 'circuit_termination__circuit', + ) device_bays = natsorted( DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), key=attrgetter('name') From 9ff59ab686ee3199be9a001bb8e37e5973a3748d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 18 Jan 2017 12:25:07 -0500 Subject: [PATCH 27/32] Closes #760: Redirect user back to device view after deleting an assigned IP address --- netbox/templates/dcim/inc/ipaddress.html | 2 +- netbox/utilities/forms.py | 5 +++++ netbox/utilities/views.py | 14 ++++++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/netbox/templates/dcim/inc/ipaddress.html b/netbox/templates/dcim/inc/ipaddress.html index 7bdc8bc1e..72920986e 100644 --- a/netbox/templates/dcim/inc/ipaddress.html +++ b/netbox/templates/dcim/inc/ipaddress.html @@ -13,7 +13,7 @@ {% if perms.ipam.delete_ipaddress %} - + {% endif %} diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 9351c6044..f957f8e88 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -386,7 +386,12 @@ class BootstrapMixin(forms.BaseForm): class ConfirmationForm(BootstrapMixin, forms.Form): + """ + A generic confirmation form. The form is not valid unless the confirm field is checked. An optional return_url can + be specified to direct the user to a specific URL after the action has been taken. + """ confirm = forms.BooleanField(required=True) + return_url = forms.CharField(required=False, widget=forms.HiddenInput()) class BulkEditForm(forms.Form): diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 125bb6584..bae8728bf 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -239,13 +239,16 @@ class ObjectDeleteView(View): def get(self, request, **kwargs): obj = self.get_object(kwargs) - form = ConfirmationForm() + initial_data = { + 'return_url': request.GET.get('return_url'), + } + form = ConfirmationForm(initial=initial_data) return render(request, self.template_name, { 'obj': obj, 'form': form, 'obj_type': self.model._meta.verbose_name, - 'cancel_url': self.get_cancel_url(obj), + 'cancel_url': request.GET.get('return_url') or self.get_cancel_url(obj), }) def post(self, request, **kwargs): @@ -261,7 +264,10 @@ class ObjectDeleteView(View): msg = u'Deleted {} {}'.format(self.model._meta.verbose_name, obj) messages.success(request, msg) UserAction.objects.log_delete(request.user, obj, msg) - if self.redirect_url: + return_url = form.cleaned_data['return_url'] + if return_url and is_safe_url(url=return_url, host=request.get_host()): + return redirect(return_url) + elif self.redirect_url: return redirect(self.redirect_url) elif hasattr(obj, 'get_parent_url'): return redirect(obj.get_parent_url()) @@ -272,7 +278,7 @@ class ObjectDeleteView(View): 'obj': obj, 'form': form, 'obj_type': self.model._meta.verbose_name, - 'cancel_url': self.get_cancel_url(obj), + 'cancel_url': request.GET.get('return_url') or self.get_cancel_url(obj), }) From 3eb969de0caa8ef71ff8abeeef62c76cd09ffee2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 18 Jan 2017 13:30:19 -0500 Subject: [PATCH 28/32] Standardized the use of return_url for ObjectDeleteView --- netbox/circuits/views.py | 4 ++-- netbox/dcim/views.py | 8 ++++---- netbox/ipam/views.py | 10 +++++----- netbox/secrets/views.py | 2 +- .../circuits/inc/circuit_termination.html | 2 +- netbox/templates/dcim/device_inventory.html | 14 +++++++------- netbox/templates/dcim/inc/consoleport.html | 2 +- netbox/templates/dcim/inc/consoleserverport.html | 2 +- netbox/templates/dcim/inc/devicebay.html | 2 +- netbox/templates/dcim/inc/interface.html | 2 +- netbox/templates/dcim/inc/poweroutlet.html | 2 +- netbox/templates/dcim/inc/powerport.html | 2 +- netbox/templates/dcim/inc/service.html | 2 +- netbox/tenancy/views.py | 2 +- netbox/utilities/views.py | 15 ++++++--------- 15 files changed, 34 insertions(+), 37 deletions(-) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 9feb19ef6..4217ca673 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -52,7 +52,7 @@ class ProviderEditView(PermissionRequiredMixin, ObjectEditView): class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'circuits.delete_provider' model = Provider - redirect_url = 'circuits:provider_list' + default_return_url = 'circuits:provider_list' class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView): @@ -140,7 +140,7 @@ class CircuitEditView(PermissionRequiredMixin, ObjectEditView): class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'circuits.delete_circuit' model = Circuit - redirect_url = 'circuits:circuit_list' + default_return_url = 'circuits:circuit_list' class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView): diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index fa433259a..aac26821e 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -163,7 +163,7 @@ class SiteEditView(PermissionRequiredMixin, ObjectEditView): class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_site' model = Site - redirect_url = 'dcim:site_list' + default_return_url = 'dcim:site_list' class SiteBulkImportView(PermissionRequiredMixin, BulkImportView): @@ -278,7 +278,7 @@ class RackEditView(PermissionRequiredMixin, ObjectEditView): class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_rack' model = Rack - redirect_url = 'dcim:rack_list' + default_return_url = 'dcim:rack_list' class RackBulkImportView(PermissionRequiredMixin, BulkImportView): @@ -401,7 +401,7 @@ class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView): class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_devicetype' model = DeviceType - redirect_url = 'dcim:devicetype_list' + default_return_url = 'dcim:devicetype_list' class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView): @@ -671,7 +671,7 @@ class DeviceEditView(PermissionRequiredMixin, ObjectEditView): class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_device' model = Device - redirect_url = 'dcim:device_list' + default_return_url = 'dcim:device_list' class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView): diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 3f991a7bc..e19e6c486 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -124,7 +124,7 @@ class VRFEditView(PermissionRequiredMixin, ObjectEditView): class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_vrf' model = VRF - redirect_url = 'ipam:vrf_list' + default_return_url = 'ipam:vrf_list' class VRFBulkImportView(PermissionRequiredMixin, BulkImportView): @@ -313,7 +313,7 @@ class AggregateEditView(PermissionRequiredMixin, ObjectEditView): class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_aggregate' model = Aggregate - redirect_url = 'ipam:aggregate_list' + default_return_url = 'ipam:aggregate_list' class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView): @@ -443,7 +443,7 @@ class PrefixEditView(PermissionRequiredMixin, ObjectEditView): class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_prefix' model = Prefix - redirect_url = 'ipam:prefix_list' + default_return_url = 'ipam:prefix_list' template_name = 'ipam/prefix_delete.html' @@ -609,7 +609,7 @@ class IPAddressEditView(PermissionRequiredMixin, ObjectEditView): class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_ipaddress' model = IPAddress - redirect_url = 'ipam:ipaddress_list' + default_return_url = 'ipam:ipaddress_list' class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView): @@ -720,7 +720,7 @@ class VLANEditView(PermissionRequiredMixin, ObjectEditView): class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_vlan' model = VLAN - redirect_url = 'ipam:vlan_list' + default_return_url = 'ipam:vlan_list' class VLANBulkImportView(PermissionRequiredMixin, BulkImportView): diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 7880adfb2..6db711a72 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -151,7 +151,7 @@ def secret_edit(request, pk): class SecretDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'secrets.delete_secret' model = Secret - redirect_url = 'secrets:secret_list' + default_return_url = 'secrets:secret_list' @permission_required('secrets.add_secret') diff --git a/netbox/templates/circuits/inc/circuit_termination.html b/netbox/templates/circuits/inc/circuit_termination.html index 7c641975c..ba0f8b5fe 100644 --- a/netbox/templates/circuits/inc/circuit_termination.html +++ b/netbox/templates/circuits/inc/circuit_termination.html @@ -15,7 +15,7 @@ {% endif %} {% if termination and perms.circuits.delete_circuittermination %} - + Delete {% endif %} diff --git a/netbox/templates/dcim/device_inventory.html b/netbox/templates/dcim/device_inventory.html index 6e19d8fe8..a4fbe7170 100644 --- a/netbox/templates/dcim/device_inventory.html +++ b/netbox/templates/dcim/device_inventory.html @@ -67,7 +67,7 @@ {% endif %} {% if perms.dcim.delete_module %} - + {% endif %} @@ -80,10 +80,10 @@ {{ m2.serial }} {% if perms.dcim.change_module %} - + {% endif %} {% if perms.dcim.delete_module %} - + {% endif %} @@ -96,10 +96,10 @@ {{ m3.serial }} {% if perms.dcim.change_module %} - + {% endif %} {% if perms.dcim.delete_module %} - + {% endif %} @@ -112,10 +112,10 @@ {{ m4.serial }} {% if perms.dcim.change_module %} - + {% endif %} {% if perms.dcim.delete_module %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/consoleport.html b/netbox/templates/dcim/inc/consoleport.html index e6c816780..ebe96a660 100644 --- a/netbox/templates/dcim/inc/consoleport.html +++ b/netbox/templates/dcim/inc/consoleport.html @@ -50,7 +50,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/consoleserverport.html b/netbox/templates/dcim/inc/consoleserverport.html index d3c923e43..848bb8f9d 100644 --- a/netbox/templates/dcim/inc/consoleserverport.html +++ b/netbox/templates/dcim/inc/consoleserverport.html @@ -49,7 +49,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/devicebay.html b/netbox/templates/dcim/inc/devicebay.html index eacb27440..bc0934283 100644 --- a/netbox/templates/dcim/inc/devicebay.html +++ b/netbox/templates/dcim/inc/devicebay.html @@ -40,7 +40,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index bfb44b75d..d249b8b6e 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -85,7 +85,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html index 241ebc15c..929c9c903 100644 --- a/netbox/templates/dcim/inc/poweroutlet.html +++ b/netbox/templates/dcim/inc/poweroutlet.html @@ -49,7 +49,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/powerport.html b/netbox/templates/dcim/inc/powerport.html index aacb96839..c06d38b5a 100644 --- a/netbox/templates/dcim/inc/powerport.html +++ b/netbox/templates/dcim/inc/powerport.html @@ -50,7 +50,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/service.html b/netbox/templates/dcim/inc/service.html index 28cd64094..1e42a1811 100644 --- a/netbox/templates/dcim/inc/service.html +++ b/netbox/templates/dcim/inc/service.html @@ -18,7 +18,7 @@ {% endif %} {% if perms.ipam.delete_service %} - + {% endif %} diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 821f6d7bf..768fa390d 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -89,7 +89,7 @@ class TenantEditView(PermissionRequiredMixin, ObjectEditView): class TenantDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'tenancy.delete_tenant' model = Tenant - redirect_url = 'tenancy:tenant_list' + default_return_url = 'tenancy:tenant_list' class TenantBulkImportView(PermissionRequiredMixin, BulkImportView): diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index bae8728bf..18b536d94 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -216,11 +216,11 @@ class ObjectDeleteView(View): model: The model of the object being edited template_name: The name of the template - redirect_url: Name of the URL to which the user is redirected after deleting the object + default_return_url: Name of the URL to which the user is redirected after deleting the object """ model = None template_name = 'utilities/obj_delete.html' - redirect_url = None + default_return_url = 'home' def get_object(self, kwargs): # Look up object by slug if one has been provided. Otherwise, use PK. @@ -232,8 +232,6 @@ class ObjectDeleteView(View): def get_cancel_url(self, obj): if hasattr(obj, 'get_absolute_url'): return obj.get_absolute_url() - if hasattr(obj, 'get_parent_url'): - return obj.get_parent_url() return reverse('home') def get(self, request, **kwargs): @@ -256,23 +254,22 @@ class ObjectDeleteView(View): obj = self.get_object(kwargs) form = ConfirmationForm(request.POST) if form.is_valid(): + try: obj.delete() except ProtectedError as e: handle_protectederror(obj, request, e) return redirect(obj.get_absolute_url()) + msg = u'Deleted {} {}'.format(self.model._meta.verbose_name, obj) messages.success(request, msg) UserAction.objects.log_delete(request.user, obj, msg) + return_url = form.cleaned_data['return_url'] if return_url and is_safe_url(url=return_url, host=request.get_host()): return redirect(return_url) - elif self.redirect_url: - return redirect(self.redirect_url) - elif hasattr(obj, 'get_parent_url'): - return redirect(obj.get_parent_url()) else: - return redirect('home') + return redirect(self.default_return_url) return render(request, self.template_name, { 'obj': obj, From cdccc3a47f7eedd285abc085b44b464a6f554e86 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 18 Jan 2017 14:07:46 -0500 Subject: [PATCH 29/32] Ditched get_parent_url() model method in favor of overrideable get_return_url() view method --- netbox/circuits/models.py | 3 --- netbox/circuits/views.py | 6 ++++-- netbox/dcim/models.py | 21 --------------------- netbox/dcim/views.py | 6 ++++-- netbox/ipam/models.py | 3 --- netbox/ipam/views.py | 3 +++ netbox/utilities/views.py | 14 +++++++------- 7 files changed, 18 insertions(+), 38 deletions(-) diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index e0f42a78f..7f6cc4f21 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -159,9 +159,6 @@ class CircuitTermination(models.Model): def __unicode__(self): return u'{} (Side {})'.format(self.circuit, self.get_term_side_display()) - def get_parent_url(self): - return self.circuit.get_absolute_url() - def get_peer_termination(self): peer_side = 'Z' if self.term_side == 'A' else 'A' try: diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 4217ca673..a43bc175f 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -223,10 +223,12 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView): def alter_obj(self, obj, args, kwargs): if 'circuit' in kwargs: - circuit = get_object_or_404(Circuit, pk=kwargs['circuit']) - obj.circuit = circuit + obj.circuit = get_object_or_404(Circuit, pk=kwargs['circuit']) return obj + def get_return_url(self, obj): + return obj.circuit.get_absolute_url() + class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'circuits.delete_circuittermination' diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 030de3436..6274f3a53 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -985,9 +985,6 @@ class ConsolePort(models.Model): def __unicode__(self): return self.name - def get_parent_url(self): - return self.device.get_absolute_url() - # Used for connections export def to_csv(self): return csv_format([ @@ -1029,9 +1026,6 @@ class ConsoleServerPort(models.Model): def __unicode__(self): return self.name - def get_parent_url(self): - return self.device.get_absolute_url() - class PowerPort(models.Model): """ @@ -1050,9 +1044,6 @@ class PowerPort(models.Model): def __unicode__(self): return self.name - def get_parent_url(self): - return self.device.get_absolute_url() - # Used for connections export def csv_format(self): return ','.join([ @@ -1088,9 +1079,6 @@ class PowerOutlet(models.Model): def __unicode__(self): return self.name - def get_parent_url(self): - return self.device.get_absolute_url() - class Interface(models.Model): """ @@ -1114,9 +1102,6 @@ class Interface(models.Model): def __unicode__(self): return self.name - def get_parent_url(self): - return self.device.get_absolute_url() - def clean(self): if self.form_factor == IFACE_FF_VIRTUAL and self.is_connected: @@ -1207,9 +1192,6 @@ class DeviceBay(models.Model): def __unicode__(self): return u'{} - {}'.format(self.device.name, self.name) - def get_parent_url(self): - return self.device.get_absolute_url() - def clean(self): # Validate that the parent Device can have DeviceBays @@ -1243,6 +1225,3 @@ class Module(models.Model): def __unicode__(self): return self.name - - def get_parent_url(self): - return reverse('dcim:device_inventory', args=[self.device.pk]) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index aac26821e..cedc8d4c4 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1506,10 +1506,12 @@ class ModuleEditView(PermissionRequiredMixin, ObjectEditView): def alter_obj(self, obj, args, kwargs): if 'device' in kwargs: - device = get_object_or_404(Device, pk=kwargs['device']) - obj.device = device + obj.device = get_object_or_404(Device, pk=kwargs['device']) return obj + def get_return_url(self, obj): + return obj.device.get_absolute_url() + class ModuleDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_module' diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 37501735e..c8afc6402 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -578,6 +578,3 @@ class Service(CreatedUpdatedModel): def __unicode__(self): return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display()) - - def get_parent_url(self): - return self.device.get_absolute_url() diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index e19e6c486..7dcf03fd9 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -760,6 +760,9 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView): obj.device = get_object_or_404(Device, pk=kwargs['device']) return obj + def get_return_url(self, obj): + return obj.device.get_absolute_url() + class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_service' diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 18b536d94..a158d701f 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -150,13 +150,13 @@ class ObjectEditView(View): # given some parameter from the request URI. return obj - def get_redirect_url(self, obj): + def get_return_url(self, obj): # Determine where to redirect the user after updating an object (or aborting an update). if obj.pk and self.use_obj_view and hasattr(obj, 'get_absolute_url'): return obj.get_absolute_url() - if obj and self.use_obj_view and hasattr(obj, 'get_parent_url'): - return obj.get_parent_url() - return reverse(self.obj_list_url) + if self.obj_list_url is not None: + return reverse(self.obj_list_url) + return reverse('home') def get(self, request, *args, **kwargs): @@ -169,7 +169,7 @@ class ObjectEditView(View): 'obj': obj, 'obj_type': self.model._meta.verbose_name, 'form': form, - 'cancel_url': self.get_redirect_url(obj), + 'cancel_url': self.get_return_url(obj), }) def post(self, request, *args, **kwargs): @@ -200,13 +200,13 @@ class ObjectEditView(View): if '_addanother' in request.POST: return redirect(request.path) - return redirect(self.get_redirect_url(obj)) + return redirect(self.get_return_url(obj)) return render(request, self.template_name, { 'obj': obj, 'obj_type': self.model._meta.verbose_name, 'form': form, - 'cancel_url': self.get_redirect_url(obj), + 'cancel_url': self.get_return_url(obj), }) From 28a9307f9f09b70e64652b1131682b1e55a971ba Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 18 Jan 2017 14:34:17 -0500 Subject: [PATCH 30/32] Deprecated use_obj_view in favor of get_return_url() --- netbox/circuits/views.py | 6 ++++-- netbox/dcim/views.py | 25 +++++++++++++++---------- netbox/ipam/views.py | 15 +++++++++------ netbox/secrets/views.py | 5 +++-- netbox/tenancy/views.py | 6 ++++-- netbox/utilities/views.py | 5 +---- 6 files changed, 36 insertions(+), 26 deletions(-) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index a43bc175f..3eb269327 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -1,6 +1,7 @@ from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import PermissionRequiredMixin +from django.core.urlresolvers import reverse from django.db import transaction from django.db.models import Count from django.shortcuts import get_object_or_404, redirect, render @@ -92,8 +93,9 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'circuits.change_circuittype' model = CircuitType form_class = forms.CircuitTypeForm - obj_list_url = 'circuits:circuittype_list' - use_obj_view = False + + def get_return_url(self, obj): + return reverse('circuits:circuittype_list') class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index cedc8d4c4..611bacec1 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -199,8 +199,9 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'dcim.change_rackgroup' model = RackGroup form_class = forms.RackGroupForm - obj_list_url = 'dcim:rackgroup_list' - use_obj_view = False + + def get_return_url(self, obj): + return reverse('dcim:rackgroup_list') class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): @@ -224,8 +225,9 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'dcim.change_rackrole' model = RackRole form_class = forms.RackRoleForm - obj_list_url = 'dcim:rackrole_list' - use_obj_view = False + + def get_return_url(self, obj): + return reverse('dcim:rackrole_list') class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): @@ -318,8 +320,9 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'dcim.change_manufacturer' model = Manufacturer form_class = forms.ManufacturerForm - obj_list_url = 'dcim:manufacturer_list' - use_obj_view = False + + def get_return_url(self, obj): + return reverse('dcim:manufacturer_list') class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): @@ -537,8 +540,9 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'dcim.change_devicerole' model = DeviceRole form_class = forms.DeviceRoleForm - obj_list_url = 'dcim:devicerole_list' - use_obj_view = False + + def get_return_url(self, obj): + return reverse('dcim:devicerole_list') class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): @@ -562,8 +566,9 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'dcim.change_platform' model = Platform form_class = forms.PlatformForm - obj_list_url = 'dcim:platform_list' - use_obj_view = False + + def get_return_url(self, obj): + return reverse('dcim:platform_list') class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 7dcf03fd9..4182532ab 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -242,8 +242,9 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'ipam.change_rir' model = RIR form_class = forms.RIRForm - obj_list_url = 'ipam:rir_list' - use_obj_view = False + + def get_return_url(self, obj): + return reverse('ipam:rir_list') class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): @@ -353,8 +354,9 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'ipam.change_role' model = Role form_class = forms.RoleForm - obj_list_url = 'ipam:role_list' - use_obj_view = False + + def get_return_url(self, obj): + return reverse('ipam:role_list') class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): @@ -674,8 +676,9 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'ipam.change_vlangroup' model = VLANGroup form_class = forms.VLANGroupForm - obj_list_url = 'ipam:vlangroup_list' - use_obj_view = False + + def get_return_url(self, obj): + return reverse('ipam:vlangroup_list') class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 6db711a72..7fd3b4380 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -30,8 +30,9 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'secrets.change_secretrole' model = SecretRole form_class = forms.SecretRoleForm - obj_list_url = 'secrets:secretrole_list' - use_obj_view = False + + def get_return_url(self, obj): + return reverse('secrets:secretrole_list') class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 768fa390d..e8c772fc3 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -1,4 +1,5 @@ from django.contrib.auth.mixins import PermissionRequiredMixin +from django.core.urlresolvers import reverse from django.db.models import Count, Q from django.shortcuts import get_object_or_404, render @@ -28,8 +29,9 @@ class TenantGroupEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'tenancy.change_tenantgroup' model = TenantGroup form_class = forms.TenantGroupForm - obj_list_url = 'tenancy:tenantgroup_list' - use_obj_view = False + + def get_return_url(self, obj): + return reverse('tenancy:tenantgroup_list') class TenantGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index a158d701f..528f11c6c 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -127,15 +127,12 @@ class ObjectEditView(View): fields_initial: A set of fields that will be prepopulated in the form from the request parameters template_name: The name of the template obj_list_url: The name of the URL used to display a list of this object type - use_obj_view: If True, the user will be directed to a view of the object after it has been edited. Otherwise, the - user will be directed to the object's list view (defined as `obj_list_url`). """ model = None form_class = None fields_initial = [] template_name = 'utilities/obj_edit.html' obj_list_url = None - use_obj_view = True def get_object(self, kwargs): # Look up object by slug or PK. Return None if neither was provided. @@ -152,7 +149,7 @@ class ObjectEditView(View): def get_return_url(self, obj): # Determine where to redirect the user after updating an object (or aborting an update). - if obj.pk and self.use_obj_view and hasattr(obj, 'get_absolute_url'): + if obj.pk and hasattr(obj, 'get_absolute_url'): return obj.get_absolute_url() if self.obj_list_url is not None: return reverse(self.obj_list_url) From 74e48fc4900c0617d9700442b21c6ca2bf54e763 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 18 Jan 2017 14:43:46 -0500 Subject: [PATCH 31/32] PEP8 fixes --- netbox/dcim/views.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 611bacec1..6b01c7bb8 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -607,17 +607,13 @@ def device(request, pk): PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name') ) interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ - .filter(device=device, mgmt_only=False).select_related( - 'connected_as_a__interface_b__device', - 'connected_as_b__interface_a__device', - 'circuit_termination__circuit', - ) + .filter(device=device, mgmt_only=False)\ + .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', + 'circuit_termination__circuit') mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ - .filter(device=device, mgmt_only=True).select_related( - 'connected_as_a__interface_b__device', - 'connected_as_b__interface_a__device', - 'circuit_termination__circuit', - ) + .filter(device=device, mgmt_only=True)\ + .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', + 'circuit_termination__circuit') device_bays = natsorted( DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), key=attrgetter('name') From 6121f97ca9ec46870e95d43de9c1ab61fbbef792 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 18 Jan 2017 16:19:45 -0500 Subject: [PATCH 32/32] Release v1.8.2 --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 0d80592e2..ad21789ec 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.8.2-dev' +VERSION = '1.8.2' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: