From 180446c34d634cba0b52ce1a307455a893bcbc8b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 12 Jun 2017 10:06:19 -0400 Subject: [PATCH 01/15] Removed Debian/RHEL references from installation docs --- docs/installation/netbox.md | 8 ++++---- docs/installation/postgresql.md | 7 +++++-- docs/installation/web-server.md | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/installation/netbox.md b/docs/installation/netbox.md index 1a13353b5..dfdf2af52 100644 --- a/docs/installation/netbox.md +++ b/docs/installation/netbox.md @@ -1,6 +1,6 @@ # Installation -**Debian/Ubuntu** +**Ubuntu** Python 3: @@ -15,7 +15,7 @@ Python 2: # apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev ``` -**CentOS/RHEL** +**CentOS** Python 3: @@ -57,13 +57,13 @@ Create the base directory for the NetBox installation. For this guide, we'll use If `git` is not already installed, install it: -**Debian/Ubuntu** +**Ubuntu** ```no-highlight # apt-get install -y git ``` -**CentOS/RHEL** +**CentOS** ```no-highlight # yum install -y git diff --git a/docs/installation/postgresql.md b/docs/installation/postgresql.md index 543a0a2cf..75c754707 100644 --- a/docs/installation/postgresql.md +++ b/docs/installation/postgresql.md @@ -1,15 +1,18 @@ NetBox requires a PostgreSQL database to store data. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/9.1/static/datatype-net-types.html).) +!!! note + The installation instructions provided here have been tested to work on Ubuntu 16.04 and CentOS 6.9. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors. + # Installation -**Debian/Ubuntu** +**Ubuntu** ```no-highlight # apt-get update # apt-get install -y postgresql libpq-dev ``` -**CentOS/RHEL** +**CentOS** ```no-highlight # yum install -y postgresql postgresql-server postgresql-devel diff --git a/docs/installation/web-server.md b/docs/installation/web-server.md index 7cbfad2e4..9da487f13 100644 --- a/docs/installation/web-server.md +++ b/docs/installation/web-server.md @@ -3,7 +3,7 @@ We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence. !!! info - Only Debian/Ubuntu instructions are provided here, but the installation process for CentOS/RHEL does not differ much. Please consult the documentation for those distributions for details. + For the sake of brevity, only Ubuntu 16.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed. ```no-highlight # apt-get install -y gunicorn supervisor From 5456af6867b1ee79ea01c90684d1904a3cd9b93e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 13 Jun 2017 14:28:38 -0400 Subject: [PATCH 02/15] Removed 'update-alternatives' from Python3 instructions --- docs/installation/netbox.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/installation/netbox.md b/docs/installation/netbox.md index dfdf2af52..c7c2eb8ed 100644 --- a/docs/installation/netbox.md +++ b/docs/installation/netbox.md @@ -6,7 +6,6 @@ Python 3: ```no-highlight # apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev -# update-alternatives --install /usr/bin/python python /usr/bin/python3 1 ``` Python 2: @@ -150,11 +149,14 @@ You may use the script located at `netbox/generate_secret_key.py` to generate a # Run Database Migrations -Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example): +!!! warning + The examples on the rest of this page call the `python` executable, which will be Python2 on most systems. Replace this with `python3` if you're running NetBox on Python3. + +Before NetBox can run, we need to install the database schema. This is done by running `python manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example): ```no-highlight # cd /opt/netbox/netbox/ -# ./manage.py migrate +# python manage.py migrate Operations to perform: Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users Running migrations: @@ -172,7 +174,7 @@ If this step results in a PostgreSQL authentication error, ensure that the usern NetBox does not come with any predefined user accounts. You'll need to create a super user to be able to log into NetBox: ```no-highlight -# ./manage.py createsuperuser +# python manage.py createsuperuser Username: admin Email address: admin@example.com Password: @@ -183,7 +185,7 @@ Superuser created successfully. # Collect Static Files ```no-highlight -# ./manage.py collectstatic --no-input +# python manage.py collectstatic --no-input You have requested to collect static files at the destination location as specified in your settings: @@ -204,7 +206,7 @@ NetBox ships with some initial data to help you get started: RIR definitions, co This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch. ```no-highlight -# ./manage.py loaddata initial_data +# python manage.py loaddata initial_data Installed 43 object(s) from 4 fixture(s) ``` @@ -213,7 +215,7 @@ Installed 43 object(s) from 4 fixture(s) At this point, NetBox should be able to run. We can verify this by starting a development instance: ```no-highlight -# ./manage.py runserver 0.0.0.0:8000 --insecure +# python manage.py runserver 0.0.0.0:8000 --insecure Performing system checks... System check identified no issues (0 silenced). From 54fa51eeff14abfe1420a6e37401b3a91833807a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 13 Jun 2017 15:55:58 -0400 Subject: [PATCH 03/15] 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 4ee969ac9..e6e6327b6 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ except ImportError: ) -VERSION = '2.0.6' +VERSION = '2.0.7-dev' # Import required configuration parameters ALLOWED_HOSTS = DATABASE = SECRET_KEY = None From 469c52be28a9dc83a64e3413095469046e0a8a22 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 13 Jun 2017 16:41:57 -0400 Subject: [PATCH 04/15] Fixes #1263: Differentiate add and edit permissions for objects --- netbox/circuits/urls.py | 8 ++-- netbox/circuits/views.py | 32 +++++++++++---- netbox/dcim/urls.py | 24 +++++------ netbox/dcim/views.py | 89 +++++++++++++++++++++++++++++----------- netbox/ipam/urls.py | 16 ++++---- netbox/ipam/views.py | 72 ++++++++++++++++++++++++-------- netbox/secrets/urls.py | 2 +- netbox/secrets/views.py | 8 +++- netbox/tenancy/urls.py | 4 +- netbox/tenancy/views.py | 16 ++++++-- 10 files changed, 189 insertions(+), 82 deletions(-) diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index 12a7dc298..7dd72ad9d 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -10,7 +10,7 @@ urlpatterns = [ # Providers url(r'^providers/$', views.ProviderListView.as_view(), name='provider_list'), - url(r'^providers/add/$', views.ProviderEditView.as_view(), name='provider_add'), + url(r'^providers/add/$', views.ProviderCreateView.as_view(), name='provider_add'), url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'), url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'), url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), @@ -20,13 +20,13 @@ urlpatterns = [ # Circuit types url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'), - url(r'^circuit-types/add/$', views.CircuitTypeEditView.as_view(), name='circuittype_add'), + url(r'^circuit-types/add/$', views.CircuitTypeCreateView.as_view(), name='circuittype_add'), url(r'^circuit-types/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'), url(r'^circuit-types/(?P[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), # Circuits url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'), - url(r'^circuits/add/$', views.CircuitEditView.as_view(), name='circuit_add'), + url(r'^circuits/add/$', views.CircuitCreateView.as_view(), name='circuit_add'), url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'), url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'), url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'), @@ -36,7 +36,7 @@ urlpatterns = [ url(r'^circuits/(?P\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'), # Circuit terminations - url(r'^circuits/(?P\d+)/terminations/add/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'), + url(r'^circuits/(?P\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'), url(r'^circuit-terminations/(?P\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), url(r'^circuit-terminations/(?P\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 35e37f4c5..eda37340d 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -49,14 +49,18 @@ class ProviderView(View): }) -class ProviderEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'circuits.change_provider' +class ProviderCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'circuits.add_provider' model = Provider form_class = forms.ProviderForm template_name = 'circuits/provider_edit.html' default_return_url = 'circuits:provider_list' +class ProviderEditView(ProviderCreateView): + permission_required = 'circuits.change_provider' + + class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'circuits.delete_provider' model = Provider @@ -96,8 +100,8 @@ class CircuitTypeListView(ObjectListView): template_name = 'circuits/circuittype_list.html' -class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'circuits.change_circuittype' +class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'circuits.add_circuittype' model = CircuitType form_class = forms.CircuitTypeForm @@ -105,6 +109,10 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView): return reverse('circuits:circuittype_list') +class CircuitTypeEditView(CircuitTypeCreateView): + permission_required = 'circuits.change_circuittype' + + class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'circuits.delete_circuittype' cls = CircuitType @@ -146,14 +154,18 @@ class CircuitView(View): }) -class CircuitEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'circuits.change_circuit' +class CircuitCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'circuits.add_circuit' model = Circuit form_class = forms.CircuitForm template_name = 'circuits/circuit_edit.html' default_return_url = 'circuits:circuit_list' +class CircuitEditView(CircuitCreateView): + permission_required = 'circuits.change_circuit' + + class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'circuits.delete_circuit' model = Circuit @@ -232,8 +244,8 @@ def circuit_terminations_swap(request, pk): # Circuit terminations # -class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'circuits.change_circuittermination' +class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'circuits.add_circuittermination' model = CircuitTermination form_class = forms.CircuitTerminationForm template_name = 'circuits/circuittermination_edit.html' @@ -247,6 +259,10 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView): return obj.circuit.get_absolute_url() +class CircuitTerminationEditView(CircuitTerminationCreateView): + permission_required = 'circuits.change_circuittermination' + + class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'circuits.delete_circuittermination' model = CircuitTermination diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 775daeabf..3a72a758b 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.conf.urls import url from extras.views import ImageAttachmentEditView -from ipam.views import ServiceEditView +from ipam.views import ServiceCreateView from secrets.views import secret_add from .models import Device, Rack, Site from . import views @@ -14,13 +14,13 @@ urlpatterns = [ # Regions url(r'^regions/$', views.RegionListView.as_view(), name='region_list'), - url(r'^regions/add/$', views.RegionEditView.as_view(), name='region_add'), + url(r'^regions/add/$', views.RegionCreateView.as_view(), name='region_add'), url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'), url(r'^regions/(?P\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'), # Sites url(r'^sites/$', views.SiteListView.as_view(), name='site_list'), - url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'), + url(r'^sites/add/$', views.SiteCreateView.as_view(), name='site_add'), url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'), url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'), url(r'^sites/(?P[\w-]+)/$', views.SiteView.as_view(), name='site'), @@ -30,13 +30,13 @@ urlpatterns = [ # Rack groups url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'), - url(r'^rack-groups/add/$', views.RackGroupEditView.as_view(), name='rackgroup_add'), + url(r'^rack-groups/add/$', views.RackGroupCreateView.as_view(), name='rackgroup_add'), url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'), url(r'^rack-groups/(?P\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'), # Rack roles url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'), - url(r'^rack-roles/add/$', views.RackRoleEditView.as_view(), name='rackrole_add'), + url(r'^rack-roles/add/$', views.RackRoleCreateView.as_view(), name='rackrole_add'), url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'), url(r'^rack-roles/(?P\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'), @@ -56,18 +56,18 @@ urlpatterns = [ url(r'^racks/(?P\d+)/$', views.RackView.as_view(), name='rack'), url(r'^racks/(?P\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'), url(r'^racks/(?P\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'), - url(r'^racks/(?P\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'), + url(r'^racks/(?P\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'), url(r'^racks/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}), # Manufacturers url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'), - url(r'^manufacturers/add/$', views.ManufacturerEditView.as_view(), name='manufacturer_add'), + url(r'^manufacturers/add/$', views.ManufacturerCreateView.as_view(), name='manufacturer_add'), url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'), url(r'^manufacturers/(?P[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), # Device types url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'), - url(r'^device-types/add/$', views.DeviceTypeEditView.as_view(), name='devicetype_add'), + url(r'^device-types/add/$', views.DeviceTypeCreateView.as_view(), name='devicetype_add'), url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'), url(r'^device-types/(?P\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'), @@ -101,19 +101,19 @@ urlpatterns = [ # Device roles url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'), - url(r'^device-roles/add/$', views.DeviceRoleEditView.as_view(), name='devicerole_add'), + url(r'^device-roles/add/$', views.DeviceRoleCreateView.as_view(), name='devicerole_add'), url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'), url(r'^device-roles/(?P[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'), # Platforms url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'), - url(r'^platforms/add/$', views.PlatformEditView.as_view(), name='platform_add'), + url(r'^platforms/add/$', views.PlatformCreateView.as_view(), name='platform_add'), url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'), url(r'^platforms/(?P[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'), # Devices url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'), - url(r'^devices/add/$', views.DeviceEditView.as_view(), name='device_add'), + url(r'^devices/add/$', views.DeviceCreateView.as_view(), name='device_add'), url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'), url(r'^devices/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'), url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'), @@ -124,7 +124,7 @@ urlpatterns = [ url(r'^devices/(?P\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'), url(r'^devices/(?P\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), url(r'^devices/(?P\d+)/add-secret/$', secret_add, name='device_addsecret'), - url(r'^devices/(?P\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'), + url(r'^devices/(?P\d+)/services/assign/$', ServiceCreateView.as_view(), name='service_assign'), url(r'^devices/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), # Console ports diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 40b18e33a..db4da8c4d 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals from copy import deepcopy -from difflib import SequenceMatcher import re from natsort import natsorted from operator import attrgetter @@ -152,8 +151,8 @@ class RegionListView(ObjectListView): template_name = 'dcim/region_list.html' -class RegionEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_region' +class RegionCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_region' model = Region form_class = forms.RegionForm @@ -161,6 +160,10 @@ class RegionEditView(PermissionRequiredMixin, ObjectEditView): return reverse('dcim:region_list') +class RegionEditView(RegionCreateView): + permission_required = 'dcim.change_region' + + class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_region' cls = Region @@ -204,14 +207,18 @@ class SiteView(View): }) -class SiteEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_site' +class SiteCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_site' model = Site form_class = forms.SiteForm template_name = 'dcim/site_edit.html' default_return_url = 'dcim:site_list' +class SiteEditView(SiteCreateView): + permission_required = 'dcim.change_site' + + class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_site' model = Site @@ -246,8 +253,8 @@ class RackGroupListView(ObjectListView): template_name = 'dcim/rackgroup_list.html' -class RackGroupEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_rackgroup' +class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_rackgroup' model = RackGroup form_class = forms.RackGroupForm @@ -255,6 +262,10 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView): return reverse('dcim:rackgroup_list') +class RackGroupEditView(RackGroupCreateView): + permission_required = 'dcim.change_rackgroup' + + class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rackgroup' cls = RackGroup @@ -272,8 +283,8 @@ class RackRoleListView(ObjectListView): template_name = 'dcim/rackrole_list.html' -class RackRoleEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_rackrole' +class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_rackrole' model = RackRole form_class = forms.RackRoleForm @@ -281,6 +292,10 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView): return reverse('dcim:rackrole_list') +class RackRoleEditView(RackRoleCreateView): + permission_required = 'dcim.change_rackrole' + + class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rackrole' cls = RackRole @@ -374,14 +389,18 @@ class RackView(View): }) -class RackEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_rack' +class RackCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_rack' model = Rack form_class = forms.RackForm template_name = 'dcim/rack_edit.html' default_return_url = 'dcim:rack_list' +class RackEditView(RackCreateView): + permission_required = 'dcim.change_rack' + + class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_rack' model = Rack @@ -423,8 +442,8 @@ class RackReservationListView(ObjectListView): template_name = 'dcim/rackreservation_list.html' -class RackReservationEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_rackreservation' +class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_rackreservation' model = RackReservation form_class = forms.RackReservationForm @@ -438,6 +457,10 @@ class RackReservationEditView(PermissionRequiredMixin, ObjectEditView): return obj.rack.get_absolute_url() +class RackReservationEditView(RackReservationCreateView): + permission_required = 'dcim.change_rackreservation' + + class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_rackreservation' model = RackReservation @@ -462,8 +485,8 @@ class ManufacturerListView(ObjectListView): template_name = 'dcim/manufacturer_list.html' -class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_manufacturer' +class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_manufacturer' model = Manufacturer form_class = forms.ManufacturerForm @@ -471,6 +494,10 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView): return reverse('dcim:manufacturer_list') +class ManufacturerEditView(ManufacturerCreateView): + permission_required = 'dcim.change_manufacturer' + + class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_manufacturer' cls = Manufacturer @@ -542,14 +569,18 @@ class DeviceTypeView(View): }) -class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_devicetype' +class DeviceTypeCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_devicetype' model = DeviceType form_class = forms.DeviceTypeForm template_name = 'dcim/devicetype_edit.html' default_return_url = 'dcim:devicetype_list' +class DeviceTypeEditView(DeviceTypeCreateView): + permission_required = 'dcim.change_devicetype' + + class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_devicetype' model = DeviceType @@ -686,8 +717,8 @@ class DeviceRoleListView(ObjectListView): template_name = 'dcim/devicerole_list.html' -class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_devicerole' +class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_devicerole' model = DeviceRole form_class = forms.DeviceRoleForm @@ -695,6 +726,10 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView): return reverse('dcim:devicerole_list') +class DeviceRoleEditView(DeviceRoleCreateView): + permission_required = 'dcim.change_devicerole' + + class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_devicerole' cls = DeviceRole @@ -711,8 +746,8 @@ class PlatformListView(ObjectListView): template_name = 'dcim/platform_list.html' -class PlatformEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_platform' +class PlatformCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_platform' model = Platform form_class = forms.PlatformForm @@ -720,6 +755,10 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView): return reverse('dcim:platform_list') +class PlatformEditView(PlatformCreateView): + permission_required = 'dcim.change_platform' + + class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_platform' cls = Platform @@ -843,14 +882,18 @@ class DeviceLLDPNeighborsView(View): }) -class DeviceEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_device' +class DeviceCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_device' model = Device form_class = forms.DeviceForm template_name = 'dcim/device_edit.html' default_return_url = 'dcim:device_list' +class DeviceEditView(DeviceCreateView): + permission_required = 'dcim.change_device' + + class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_device' model = Device diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index d28bf8a13..6c647c3dc 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -10,7 +10,7 @@ urlpatterns = [ # VRFs url(r'^vrfs/$', views.VRFListView.as_view(), name='vrf_list'), - url(r'^vrfs/add/$', views.VRFEditView.as_view(), name='vrf_add'), + url(r'^vrfs/add/$', views.VRFCreateView.as_view(), name='vrf_add'), url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'), url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'), url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'), @@ -20,13 +20,13 @@ urlpatterns = [ # RIRs url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'), - url(r'^rirs/add/$', views.RIREditView.as_view(), name='rir_add'), + url(r'^rirs/add/$', views.RIRCreateView.as_view(), name='rir_add'), url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'), url(r'^rirs/(?P[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'), # Aggregates url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'), - url(r'^aggregates/add/$', views.AggregateEditView.as_view(), name='aggregate_add'), + url(r'^aggregates/add/$', views.AggregateCreateView.as_view(), name='aggregate_add'), url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'), url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'), url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'), @@ -36,13 +36,13 @@ urlpatterns = [ # Roles url(r'^roles/$', views.RoleListView.as_view(), name='role_list'), - url(r'^roles/add/$', views.RoleEditView.as_view(), name='role_add'), + url(r'^roles/add/$', views.RoleCreateView.as_view(), name='role_add'), url(r'^roles/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'), url(r'^roles/(?P[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'), # Prefixes url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'), - url(r'^prefixes/add/$', views.PrefixEditView.as_view(), name='prefix_add'), + url(r'^prefixes/add/$', views.PrefixCreateView.as_view(), name='prefix_add'), url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'), url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'), url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'), @@ -53,7 +53,7 @@ urlpatterns = [ # IP addresses url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'), - url(r'^ip-addresses/add/$', views.IPAddressEditView.as_view(), name='ipaddress_add'), + url(r'^ip-addresses/add/$', views.IPAddressCreateView.as_view(), name='ipaddress_add'), url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkAddView.as_view(), name='ipaddress_bulk_add'), url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'), url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'), @@ -64,13 +64,13 @@ urlpatterns = [ # VLAN groups url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'), - url(r'^vlan-groups/add/$', views.VLANGroupEditView.as_view(), name='vlangroup_add'), + url(r'^vlan-groups/add/$', views.VLANGroupCreateView.as_view(), name='vlangroup_add'), url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'), url(r'^vlan-groups/(?P\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'), # VLANs url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'), - url(r'^vlans/add/$', views.VLANEditView.as_view(), name='vlan_add'), + url(r'^vlans/add/$', views.VLANCreateView.as_view(), name='vlan_add'), url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'), url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'), url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'), diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index f8fe0535a..20aa083b1 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -114,14 +114,18 @@ class VRFView(View): }) -class VRFEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_vrf' +class VRFCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_vrf' model = VRF form_class = forms.VRFForm template_name = 'ipam/vrf_edit.html' default_return_url = 'ipam:vrf_list' +class VRFEditView(VRFCreateView): + permission_required = 'ipam.change_vrf' + + class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_vrf' model = VRF @@ -239,8 +243,8 @@ class RIRListView(ObjectListView): } -class RIREditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_rir' +class RIRCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_rir' model = RIR form_class = forms.RIRForm @@ -248,6 +252,10 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView): return reverse('ipam:rir_list') +class RIREditView(RIRCreateView): + permission_required = 'ipam.change_rir' + + class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'ipam.delete_rir' cls = RIR @@ -324,14 +332,18 @@ class AggregateView(View): }) -class AggregateEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_aggregate' +class AggregateCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_aggregate' model = Aggregate form_class = forms.AggregateForm template_name = 'ipam/aggregate_edit.html' default_return_url = 'ipam:aggregate_list' +class AggregateEditView(AggregateCreateView): + permission_required = 'ipam.change_aggregate' + + class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_aggregate' model = Aggregate @@ -371,8 +383,8 @@ class RoleListView(ObjectListView): template_name = 'ipam/role_list.html' -class RoleEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_role' +class RoleCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_role' model = Role form_class = forms.RoleForm @@ -380,6 +392,10 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView): return reverse('ipam:role_list') +class RoleEditView(RoleCreateView): + permission_required = 'ipam.change_role' + + class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'ipam.delete_role' cls = Role @@ -519,14 +535,18 @@ class PrefixIPAddressesView(View): }) -class PrefixEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_prefix' +class PrefixCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_prefix' model = Prefix form_class = forms.PrefixForm template_name = 'ipam/prefix_edit.html' default_return_url = 'ipam:prefix_list' +class PrefixEditView(PrefixCreateView): + permission_required = 'ipam.change_prefix' + + class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_prefix' model = Prefix @@ -612,14 +632,18 @@ class IPAddressView(View): }) -class IPAddressEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_ipaddress' +class IPAddressCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_ipaddress' model = IPAddress form_class = forms.IPAddressForm template_name = 'ipam/ipaddress_edit.html' default_return_url = 'ipam:ipaddress_list' +class IPAddressEditView(IPAddressCreateView): + permission_required = 'ipam.change_ipaddress' + + class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_ipaddress' model = IPAddress @@ -683,8 +707,8 @@ class VLANGroupListView(ObjectListView): template_name = 'ipam/vlangroup_list.html' -class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_vlangroup' +class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_vlangroup' model = VLANGroup form_class = forms.VLANGroupForm @@ -692,6 +716,10 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView): return reverse('ipam:vlangroup_list') +class VLANGroupEditView(VLANGroupCreateView): + permission_required = 'ipam.change_vlangroup' + + class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'ipam.delete_vlangroup' cls = VLANGroup @@ -728,14 +756,18 @@ class VLANView(View): }) -class VLANEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_vlan' +class VLANCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_vlan' model = VLAN form_class = forms.VLANForm template_name = 'ipam/vlan_edit.html' default_return_url = 'ipam:vlan_list' +class VLANEditView(VLANCreateView): + permission_required = 'ipam.change_vlan' + + class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_vlan' model = VLAN @@ -769,8 +801,8 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Services # -class ServiceEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_service' +class ServiceCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_service' model = Service form_class = forms.ServiceForm template_name = 'ipam/service_edit.html' @@ -784,6 +816,10 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView): return obj.device.get_absolute_url() +class ServiceEditView(ServiceCreateView): + permission_required = 'ipam.change_service' + + class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_service' model = Service diff --git a/netbox/secrets/urls.py b/netbox/secrets/urls.py index b28198a2f..961e0f0ed 100644 --- a/netbox/secrets/urls.py +++ b/netbox/secrets/urls.py @@ -10,7 +10,7 @@ urlpatterns = [ # Secret roles url(r'^secret-roles/$', views.SecretRoleListView.as_view(), name='secretrole_list'), - url(r'^secret-roles/add/$', views.SecretRoleEditView.as_view(), name='secretrole_add'), + url(r'^secret-roles/add/$', views.SecretRoleCreateView.as_view(), name='secretrole_add'), url(r'^secret-roles/delete/$', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'), url(r'^secret-roles/(?P[\w-]+)/edit/$', views.SecretRoleEditView.as_view(), name='secretrole_edit'), diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index e046f1dbc..ac4226358 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -40,8 +40,8 @@ class SecretRoleListView(ObjectListView): template_name = 'secrets/secretrole_list.html' -class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'secrets.change_secretrole' +class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'secrets.add_secretrole' model = SecretRole form_class = forms.SecretRoleForm @@ -49,6 +49,10 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView): return reverse('secrets:secretrole_list') +class SecretRoleEditView(SecretRoleCreateView): + permission_required = 'secrets.change_secretrole' + + class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'secrets.delete_secretrole' cls = SecretRole diff --git a/netbox/tenancy/urls.py b/netbox/tenancy/urls.py index cb04cffb9..76d800344 100644 --- a/netbox/tenancy/urls.py +++ b/netbox/tenancy/urls.py @@ -10,13 +10,13 @@ urlpatterns = [ # Tenant groups url(r'^tenant-groups/$', views.TenantGroupListView.as_view(), name='tenantgroup_list'), - url(r'^tenant-groups/add/$', views.TenantGroupEditView.as_view(), name='tenantgroup_add'), + url(r'^tenant-groups/add/$', views.TenantGroupCreateView.as_view(), name='tenantgroup_add'), url(r'^tenant-groups/delete/$', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'), url(r'^tenant-groups/(?P[\w-]+)/edit/$', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'), # Tenants url(r'^tenants/$', views.TenantListView.as_view(), name='tenant_list'), - url(r'^tenants/add/$', views.TenantEditView.as_view(), name='tenant_add'), + url(r'^tenants/add/$', views.TenantCreateView.as_view(), name='tenant_add'), url(r'^tenants/import/$', views.TenantBulkImportView.as_view(), name='tenant_import'), url(r'^tenants/edit/$', views.TenantBulkEditView.as_view(), name='tenant_bulk_edit'), url(r'^tenants/delete/$', views.TenantBulkDeleteView.as_view(), name='tenant_bulk_delete'), diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 5a820b8f6..d151ff5ca 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -26,8 +26,8 @@ class TenantGroupListView(ObjectListView): template_name = 'tenancy/tenantgroup_list.html' -class TenantGroupEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'tenancy.change_tenantgroup' +class TenantGroupCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'tenancy.add_tenantgroup' model = TenantGroup form_class = forms.TenantGroupForm @@ -35,6 +35,10 @@ class TenantGroupEditView(PermissionRequiredMixin, ObjectEditView): return reverse('tenancy:tenantgroup_list') +class TenantGroupEditView(TenantGroupCreateView): + permission_required = 'tenancy.change_tenantgroup' + + class TenantGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'tenancy.delete_tenantgroup' cls = TenantGroup @@ -81,14 +85,18 @@ class TenantView(View): }) -class TenantEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'tenancy.change_tenant' +class TenantCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'tenancy.add_tenant' model = Tenant form_class = forms.TenantForm template_name = 'tenancy/tenant_edit.html' default_return_url = 'tenancy:tenant_list' +class TenantEditView(TenantCreateView): + permission_required = 'tenancy.change_tenant' + + class TenantDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'tenancy.delete_tenant' model = Tenant From 8eb9c451a1f98d65edb0a8864f853e023ba9fd75 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 13 Jun 2017 16:48:21 -0400 Subject: [PATCH 05/15] Renamed AddViews to CreateViews for consistency --- netbox/dcim/urls.py | 24 ++++++++++++------------ netbox/dcim/views.py | 24 ++++++++++++------------ netbox/ipam/urls.py | 2 +- netbox/ipam/views.py | 4 ++-- netbox/utilities/views.py | 2 +- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 3a72a758b..0935d8b96 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -75,28 +75,28 @@ urlpatterns = [ url(r'^device-types/(?P\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), # Console port templates - url(r'^device-types/(?P\d+)/console-ports/add/$', views.ConsolePortTemplateAddView.as_view(), name='devicetype_add_consoleport'), + url(r'^device-types/(?P\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'), url(r'^device-types/(?P\d+)/console-ports/delete/$', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'), # Console server port templates - url(r'^device-types/(?P\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateAddView.as_view(), name='devicetype_add_consoleserverport'), + url(r'^device-types/(?P\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'), url(r'^device-types/(?P\d+)/console-server-ports/delete/$', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'), # Power port templates - url(r'^device-types/(?P\d+)/power-ports/add/$', views.PowerPortTemplateAddView.as_view(), name='devicetype_add_powerport'), + url(r'^device-types/(?P\d+)/power-ports/add/$', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'), url(r'^device-types/(?P\d+)/power-ports/delete/$', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'), # Power outlet templates - url(r'^device-types/(?P\d+)/power-outlets/add/$', views.PowerOutletTemplateAddView.as_view(), name='devicetype_add_poweroutlet'), + url(r'^device-types/(?P\d+)/power-outlets/add/$', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'), url(r'^device-types/(?P\d+)/power-outlets/delete/$', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'), # Interface templates - url(r'^device-types/(?P\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(), name='devicetype_add_interface'), + url(r'^device-types/(?P\d+)/interfaces/add/$', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'), url(r'^device-types/(?P\d+)/interfaces/edit/$', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'), url(r'^device-types/(?P\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'), # Device bay templates - url(r'^device-types/(?P\d+)/device-bays/add/$', views.DeviceBayTemplateAddView.as_view(), name='devicetype_add_devicebay'), + url(r'^device-types/(?P\d+)/device-bays/add/$', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'), url(r'^device-types/(?P\d+)/device-bays/delete/$', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'), # Device roles @@ -129,7 +129,7 @@ urlpatterns = [ # Console ports url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), - url(r'^devices/(?P\d+)/console-ports/add/$', views.ConsolePortAddView.as_view(), name='consoleport_add'), + url(r'^devices/(?P\d+)/console-ports/add/$', views.ConsolePortCreateView.as_view(), name='consoleport_add'), url(r'^devices/(?P\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'), url(r'^console-ports/(?P\d+)/connect/$', views.consoleport_connect, name='consoleport_connect'), url(r'^console-ports/(?P\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'), @@ -138,7 +138,7 @@ urlpatterns = [ # Console server ports url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), - url(r'^devices/(?P\d+)/console-server-ports/add/$', views.ConsoleServerPortAddView.as_view(), name='consoleserverport_add'), + url(r'^devices/(?P\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'), url(r'^devices/(?P\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'), url(r'^console-server-ports/(?P\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'), url(r'^console-server-ports/(?P\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'), @@ -147,7 +147,7 @@ urlpatterns = [ # Power ports url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), - url(r'^devices/(?P\d+)/power-ports/add/$', views.PowerPortAddView.as_view(), name='powerport_add'), + url(r'^devices/(?P\d+)/power-ports/add/$', views.PowerPortCreateView.as_view(), name='powerport_add'), url(r'^devices/(?P\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'), url(r'^power-ports/(?P\d+)/connect/$', views.powerport_connect, name='powerport_connect'), url(r'^power-ports/(?P\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'), @@ -156,7 +156,7 @@ urlpatterns = [ # Power outlets url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), - url(r'^devices/(?P\d+)/power-outlets/add/$', views.PowerOutletAddView.as_view(), name='poweroutlet_add'), + url(r'^devices/(?P\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'), url(r'^devices/(?P\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'), url(r'^power-outlets/(?P\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'), url(r'^power-outlets/(?P\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'), @@ -165,7 +165,7 @@ urlpatterns = [ # Interfaces url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), - url(r'^devices/(?P\d+)/interfaces/add/$', views.InterfaceAddView.as_view(), name='interface_add'), + url(r'^devices/(?P\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'), url(r'^devices/(?P\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), url(r'^devices/(?P\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), url(r'^devices/(?P\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'), @@ -175,7 +175,7 @@ urlpatterns = [ # Device bays url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), - url(r'^devices/(?P\d+)/bays/add/$', views.DeviceBayAddView.as_view(), name='devicebay_add'), + url(r'^devices/(?P\d+)/bays/add/$', views.DeviceBayCreateView.as_view(), name='devicebay_add'), url(r'^devices/(?P\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'), url(r'^device-bays/(?P\d+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'), url(r'^device-bays/(?P\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index db4da8c4d..9dacfb421 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -607,7 +607,7 @@ class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Device type components # -class ConsolePortTemplateAddView(PermissionRequiredMixin, ComponentCreateView): +class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleporttemplate' parent_model = DeviceType parent_field = 'device_type' @@ -624,7 +624,7 @@ class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView) parent_cls = DeviceType -class ConsoleServerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView): +class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleserverporttemplate' parent_model = DeviceType parent_field = 'device_type' @@ -639,7 +639,7 @@ class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDelet parent_cls = DeviceType -class PowerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView): +class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_powerporttemplate' parent_model = DeviceType parent_field = 'device_type' @@ -654,7 +654,7 @@ class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): parent_cls = DeviceType -class PowerOutletTemplateAddView(PermissionRequiredMixin, ComponentCreateView): +class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_poweroutlettemplate' parent_model = DeviceType parent_field = 'device_type' @@ -669,7 +669,7 @@ class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView) parent_cls = DeviceType -class InterfaceTemplateAddView(PermissionRequiredMixin, ComponentCreateView): +class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_interfacetemplate' parent_model = DeviceType parent_field = 'device_type' @@ -692,7 +692,7 @@ class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): parent_cls = DeviceType -class DeviceBayTemplateAddView(PermissionRequiredMixin, ComponentCreateView): +class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_devicebaytemplate' parent_model = DeviceType parent_field = 'device_type' @@ -947,7 +947,7 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Console ports # -class ConsolePortAddView(PermissionRequiredMixin, ComponentCreateView): +class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleport' parent_model = Device parent_field = 'device' @@ -1060,7 +1060,7 @@ class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView): # Console server ports # -class ConsoleServerPortAddView(PermissionRequiredMixin, ComponentCreateView): +class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleserverport' parent_model = Device parent_field = 'device' @@ -1169,7 +1169,7 @@ class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Power ports # -class PowerPortAddView(PermissionRequiredMixin, ComponentCreateView): +class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_powerport' parent_model = Device parent_field = 'device' @@ -1282,7 +1282,7 @@ class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView): # Power outlets # -class PowerOutletAddView(PermissionRequiredMixin, ComponentCreateView): +class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_poweroutlet' parent_model = Device parent_field = 'device' @@ -1391,7 +1391,7 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Interfaces # -class InterfaceAddView(PermissionRequiredMixin, ComponentCreateView): +class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_interface' parent_model = Device parent_field = 'device' @@ -1429,7 +1429,7 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Device bays # -class DeviceBayAddView(PermissionRequiredMixin, ComponentCreateView): +class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_devicebay' parent_model = Device parent_field = 'device' diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index 6c647c3dc..15634c0ee 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -54,7 +54,7 @@ urlpatterns = [ # IP addresses url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'), url(r'^ip-addresses/add/$', views.IPAddressCreateView.as_view(), name='ipaddress_add'), - url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkAddView.as_view(), name='ipaddress_bulk_add'), + url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'), url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'), url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'), url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 20aa083b1..3cc54d252 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -13,7 +13,7 @@ from django.views.generic import View from dcim.models import Device from utilities.paginator import EnhancedPaginator from utilities.views import ( - BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, + BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) from . import filters, forms, tables from .models import ( @@ -650,7 +650,7 @@ class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView): default_return_url = 'ipam:ipaddress_list' -class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView): +class IPAddressBulkCreateView(PermissionRequiredMixin, BulkCreateView): permission_required = 'ipam.add_ipaddress' pattern_form = forms.IPAddressPatternForm model_form = forms.IPAddressBulkAddForm diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 223ef43a9..30d008a48 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -290,7 +290,7 @@ class ObjectDeleteView(GetReturnURLMixin, View): }) -class BulkAddView(View): +class BulkCreateView(View): """ Create new objects in bulk. From 252ab0fbab212b057d62a149da900519c69cef03 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 13 Jun 2017 16:57:25 -0400 Subject: [PATCH 06/15] Fixes #1238: Fix error when editing an IP with a NAT assignment which has no assigned device --- netbox/ipam/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index b4db64d81..e3fe96c4c 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -490,7 +490,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) initial['interface_site'] = instance.interface.device.site initial['interface_rack'] = instance.interface.device.rack initial['interface_device'] = instance.interface.device - if instance and instance.nat_inside is not None: + if instance and instance.nat_inside and instance.nat_inside.device is not None: initial['nat_site'] = instance.nat_inside.device.site initial['nat_rack'] = instance.nat_inside.device.rack initial['nat_device'] = instance.nat_inside.device From 16d694734b63bb5424ec1f64bebb5daf9a5f90b5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 14 Jun 2017 09:55:52 -0400 Subject: [PATCH 07/15] Fixes #1268: Fix CSV import error under Python 3 --- netbox/utilities/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 16c40a727..dff06d393 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -249,7 +249,7 @@ class CSVDataField(forms.CharField): reader = csv.reader(value.splitlines()) # Consume and valdiate the first line of CSV data as column headers - headers = reader.next() + headers = next(reader) for f in self.required_fields: if f not in headers: raise forms.ValidationError('Required column header "{}" not found.'.format(f)) From 78d74261e99358fe434a78e54952323f5d607b46 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 14 Jun 2017 10:57:43 -0400 Subject: [PATCH 08/15] Fixes #1266: Prevent termination a circuit to an already-connected interface --- netbox/circuits/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 46472b228..89f7a598f 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -220,7 +220,7 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm label='Interface', widget=APISelect( api_url='/api/dcim/interfaces/?device_id={{device}}&type=physical', - disabled_indicator='is_connected' + disabled_indicator='connection' ) ) From fcacac7c6f1a4c807c9ef142c9f3b6a6724f6698 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 14 Jun 2017 13:00:36 -0400 Subject: [PATCH 09/15] Fixes #1265: Fix console/power/interface connection validation when selecting a device via live search --- netbox/dcim/forms.py | 7 ++++--- netbox/project-static/js/livesearch.js | 3 ++- netbox/utilities/forms.py | 3 +++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 6aef5808c..390911f7f 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1594,9 +1594,10 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor ] # Mark connected interfaces as disabled - self.fields['interface_b'].choices = [ - (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset - ] + if self.data.get('device_b'): + self.fields['interface_b'].choices = [ + (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset + ] class InterfaceConnectionCSVForm(forms.ModelForm): diff --git a/netbox/project-static/js/livesearch.js b/netbox/project-static/js/livesearch.js index f387372b4..846d2d853 100644 --- a/netbox/project-static/js/livesearch.js +++ b/netbox/project-static/js/livesearch.js @@ -43,8 +43,9 @@ $(document).ready(function() { real_field.empty(); real_field.append($("").attr('value', ui.item.value).text(ui.item.label)); real_field.change(); - // If the field has a parent helper, reset the parent to no selection + // Disable parent selection fields $('select[filter-for="' + real_field.attr('name') + '"]').val(''); + $('#select select').attr('disabled', 'disabled'); }, minLength: 4, delay: 500 diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index dff06d393..1f37efa08 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -472,6 +472,9 @@ class ChainedFieldsMixin(forms.BaseForm): def __init__(self, *args, **kwargs): super(ChainedFieldsMixin, self).__init__(*args, **kwargs) + # if self.is_bound: + # assert False, self.data + for field_name, field in self.fields.items(): if isinstance(field, ChainedModelChoiceField): From 29a71fd9031fd023ef2cc330bc820b013e9a26c9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 14 Jun 2017 14:50:12 -0400 Subject: [PATCH 10/15] #1265: Improved livesearch UI Javascript --- netbox/project-static/js/livesearch.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/netbox/project-static/js/livesearch.js b/netbox/project-static/js/livesearch.js index 846d2d853..e00aefbaf 100644 --- a/netbox/project-static/js/livesearch.js +++ b/netbox/project-static/js/livesearch.js @@ -1,6 +1,7 @@ $(document).ready(function() { var search_field = $('#id_livesearch'); var real_field = $('#id_' + search_field.attr('data-field')); + var select_fields = $('#select select'); var search_key = search_field.attr('data-key'); var label = search_field.attr('data-label'); if (!label) { @@ -40,14 +41,22 @@ $(document).ready(function() { select: function(event, ui) { event.preventDefault(); search_field.val(ui.item.label); + select_fields.val(''); + select_fields.attr('disabled', 'disabled'); real_field.empty(); real_field.append($("").attr('value', ui.item.value).text(ui.item.label)); real_field.change(); // Disable parent selection fields - $('select[filter-for="' + real_field.attr('name') + '"]').val(''); - $('#select select').attr('disabled', 'disabled'); + // $('select[filter-for="' + real_field.attr('name') + '"]').val(''); }, minLength: 4, delay: 500 }); + + search_field.change(function() { + if (!search_field.val()) { + select_fields.removeAttr('disabled'); + select_fields.val(''); + } + }); }); From a16218b3111c2c8dd952c1fa35c13e5d5fd81ea9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 14 Jun 2017 16:22:49 -0400 Subject: [PATCH 11/15] Fixes #1273: Corrected status choices in IP address import form --- netbox/ipam/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index e3fe96c4c..040f2b8cd 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -582,7 +582,7 @@ class IPAddressCSVForm(forms.ModelForm): } ) status = CSVChoiceField( - choices=PREFIX_STATUS_CHOICES, + choices=IPADDRESS_STATUS_CHOICES, help_text='Operational status' ) device = FlexibleModelChoiceField( From 9b082eea1432a3545fe21f08b3dd6505ae903ff1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 15 Jun 2017 10:05:14 -0400 Subject: [PATCH 12/15] Fixes #1274: Exclude unterminated circuits from topology maps --- netbox/extras/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/extras/models.py b/netbox/extras/models.py index bea8a664b..ade251c94 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -371,7 +371,8 @@ class TopologyMap(models.Model): # Add all circuits to the graph for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices): peer_termination = termination.get_peer_termination() - if peer_termination is not None and peer_termination.interface.device in devices: + if (peer_termination is not None and peer_termination.interface is not None and + peer_termination.interface.device in devices): graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue') return graph.pipe(format=img_format) From 7195b7c80307de0b8f69ee60b5f72e1fa4f3b4c3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 15 Jun 2017 14:01:49 -0400 Subject: [PATCH 13/15] Closes #626: Added bulk disconnect function for console/power/interface connections on device view --- netbox/dcim/forms.py | 16 ++++- netbox/dcim/urls.py | 3 + netbox/dcim/views.py | 72 +++++++++++++++++++++- netbox/templates/dcim/bulk_disconnect.html | 13 ++++ netbox/templates/dcim/device.html | 23 +++++-- 5 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 netbox/templates/dcim/bulk_disconnect.html diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 390911f7f..e05ffec50 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -13,8 +13,8 @@ from tenancy.forms import TenancyForm from tenancy.models import Tenant from utilities.forms import ( APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, - ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVChoiceField, ExpandableNameField, FilterChoiceField, - FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField, + ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ConfirmationForm, CSVChoiceField, ExpandableNameField, + FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField, ) from .formfields import MACAddressFormField @@ -1174,6 +1174,10 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms. } +class ConsoleServerPortBulkDisconnectForm(ConfirmationForm): + pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput) + + # # Power ports # @@ -1431,6 +1435,10 @@ class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): } +class PowerOutletBulkDisconnectForm(ConfirmationForm): + pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput) + + # # Interfaces # @@ -1508,6 +1516,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm): self.fields['lag'].choices = [] +class InterfaceBulkDisconnectForm(ConfirmationForm): + pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput) + + # # Interface connections # diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 0935d8b96..53031ebbe 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -139,6 +139,7 @@ urlpatterns = [ # Console server ports url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), url(r'^devices/(?P\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'), + url(r'^devices/(?P\d+)/console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'), url(r'^devices/(?P\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'), url(r'^console-server-ports/(?P\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'), url(r'^console-server-ports/(?P\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'), @@ -157,6 +158,7 @@ urlpatterns = [ # Power outlets url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), url(r'^devices/(?P\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'), + url(r'^devices/(?P\d+)/power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'), url(r'^devices/(?P\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'), url(r'^power-outlets/(?P\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'), url(r'^power-outlets/(?P\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'), @@ -167,6 +169,7 @@ urlpatterns = [ url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), url(r'^devices/(?P\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'), url(r'^devices/(?P\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), + url(r'^devices/(?P\d+)/interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), url(r'^devices/(?P\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), url(r'^devices/(?P\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'), url(r'^interface-connections/(?P\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 9dacfb421..e6b77cb59 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -8,7 +8,7 @@ from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.paginator import EmptyPage, PageNotAnInteger -from django.db.models import Count +from django.db.models import Count, Q from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse @@ -141,6 +141,44 @@ class ComponentDeleteView(ObjectDeleteView): return obj.device.get_absolute_url() +class BulkDisconnectView(View): + """ + An extendable view for disconnection console/power/interface components in bulk. + """ + model = None + form = None + template_name = 'dcim/bulk_disconnect.html' + + def disconnect_objects(self, objects): + raise NotImplementedError() + + def post(self, request, pk): + + device = get_object_or_404(Device, pk=pk) + selected_objects = [] + + if '_confirm' in request.POST: + form = self.form(request.POST) + if form.is_valid(): + count = self.disconnect_objects(form.cleaned_data['pk']) + messages.success(request, "Disconnected {} {} on {}".format( + count, self.model._meta.verbose_name_plural, device + )) + return redirect(device.get_absolute_url()) + + else: + form = self.form(initial={'pk': request.POST.getlist('pk')}) + selected_objects = self.model.objects.filter(pk__in=form.initial['pk']) + + return render(request, self.template_name, { + 'form': form, + 'device': device, + 'obj_type_plural': self.model._meta.verbose_name_plural, + 'selected_objects': selected_objects, + 'return_url': device.get_absolute_url(), + }) + + # # Regions # @@ -1159,6 +1197,15 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView): model = ConsoleServerPort +class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): + permission_required = 'dcim.change_consoleserverport' + model = ConsoleServerPort + form = forms.ConsoleServerPortBulkDisconnectForm + + def disconnect_objects(self, cs_ports): + return ConsolePort.objects.filter(cs_port__in=cs_ports).update(cs_port=None, connection_status=None) + + class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleserverport' cls = ConsoleServerPort @@ -1381,6 +1428,17 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ComponentDeleteView): model = PowerOutlet +class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): + permission_required = 'dcim.change_poweroutlet' + model = PowerOutlet + form = forms.PowerOutletBulkDisconnectForm + + def disconnect_objects(self, power_outlets): + return PowerPort.objects.filter(power_outlet__in=power_outlets).update( + power_outlet=None, connection_status=None + ) + + class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_poweroutlet' cls = PowerOutlet @@ -1411,6 +1469,18 @@ class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView): model = Interface +class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): + permission_required = 'dcim.change_interface' + model = Interface + form = forms.InterfaceBulkDisconnectForm + + def disconnect_objects(self, interfaces): + count, _ = InterfaceConnection.objects.filter( + Q(interface_a__in=interfaces) | Q(interface_b__in=interfaces) + ).delete() + return count + + class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_interface' cls = Interface diff --git a/netbox/templates/dcim/bulk_disconnect.html b/netbox/templates/dcim/bulk_disconnect.html new file mode 100644 index 000000000..82cc86a7a --- /dev/null +++ b/netbox/templates/dcim/bulk_disconnect.html @@ -0,0 +1,13 @@ +{% extends 'utilities/confirmation_form.html' %} +{% load helpers %} + +{% block title %}Disconnect {{ obj_type_plural|bettertitle }}{% endblock %} + +{% block message %} +

Are you sure you want to disconnect all {{ selected_objects|length }} of these {{ obj_type_plural }} on {{ device }}?

+
    + {% for obj in selected_objects %} +
  • {{ obj }}
  • + {% endfor %} +
+{% endblock %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index bafcdd224..a6e5d1dbe 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -424,12 +424,17 @@