Parcourir la source

Merge pull request #587 from digitalocean/develop

Release v1.6.2
Jeremy Stretch il y a 8 ans
Parent
commit
7336fdf162
43 fichiers modifiés avec 463 ajouts et 237 suppressions
  1. 1 1
      README.md
  2. 12 0
      docs/configuration/optional-settings.md
  3. 21 16
      docs/installation/netbox.md
  4. 22 6
      docs/installation/postgresql.md
  5. 12 10
      docs/installation/web-server.md
  6. 10 1
      netbox/dcim/api/serializers.py
  7. 1 1
      netbox/dcim/forms.py
  8. 18 0
      netbox/dcim/models.py
  9. 33 30
      netbox/dcim/tests/test_apis.py
  10. 1 1
      netbox/extras/admin.py
  11. 3 2
      netbox/extras/forms.py
  12. 25 0
      netbox/extras/migrations/0003_exporttemplate_add_description.py
  13. 12 3
      netbox/extras/models.py
  14. 4 0
      netbox/netbox/configuration.docker.py
  15. 4 0
      netbox/netbox/configuration.example.py
  16. 4 1
      netbox/netbox/settings.py
  17. 7 1
      netbox/netbox/urls.py
  18. 10 5
      netbox/project-static/js/forms.js
  19. 1 1
      netbox/secrets/forms.py
  20. 1 1
      netbox/templates/500.html
  21. 3 2
      netbox/templates/_base.html
  22. 10 3
      netbox/templates/circuits/circuit_bulk_edit.html
  23. 6 1
      netbox/templates/circuits/provider_bulk_edit.html
  24. 98 84
      netbox/templates/dcim/device.html
  25. 8 1
      netbox/templates/dcim/device_bulk_edit.html
  26. 7 3
      netbox/templates/dcim/devicetype_bulk_edit.html
  27. 9 30
      netbox/templates/dcim/inc/device_table.html
  28. 14 4
      netbox/templates/dcim/inc/devicetype_component_table.html
  29. 6 1
      netbox/templates/dcim/interface_add_multi.html
  30. 3 1
      netbox/templates/dcim/rack_bulk_edit.html
  31. 5 1
      netbox/templates/dcim/site_bulk_edit.html
  32. 1 1
      netbox/templates/inc/export_button.html
  33. 7 1
      netbox/templates/ipam/aggregate_bulk_edit.html
  34. 9 3
      netbox/templates/ipam/ipaddress_bulk_edit.html
  35. 11 4
      netbox/templates/ipam/prefix_bulk_edit.html
  36. 11 4
      netbox/templates/ipam/vlan_bulk_edit.html
  37. 7 1
      netbox/templates/ipam/vrf_bulk_edit.html
  38. 7 3
      netbox/templates/secrets/secret_bulk_edit.html
  39. 5 1
      netbox/templates/tenancy/tenant_bulk_edit.html
  40. 2 2
      netbox/templates/utilities/bulk_edit_form.html
  41. 13 4
      netbox/templates/utilities/obj_table.html
  42. 18 1
      netbox/utilities/forms.py
  43. 1 1
      netbox/utilities/tables.py

+ 1 - 1
README.md

@@ -6,7 +6,7 @@ NetBox runs as a web application atop the [Django](https://www.djangoproject.com
 
 
 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/latest/).
 
 
-Questions? Comments? Please subscribe to [the netbox-disucss mailing list](https://groups.google.com/forum/#!forum/netbox-discuss), or join us on IRC in **#netbox** on **irc.freenode.net**!
+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**!
 
 
 ### Build Status
 ### Build Status
 
 

+ 12 - 0
docs/configuration/optional-settings.md

@@ -26,6 +26,18 @@ BANNER_BOTTOM = BANNER_TOP
 
 
 ---
 ---
 
 
+## BASE_PATH
+
+Default: None
+
+The base URL path to use when accessing NetBox. Do not include the scheme or domain name. For example, if installed at http://example.com/netbox/, set:
+
+```
+BASE_PATH = 'netbox/'
+```
+
+---
+
 ## DEBUG
 ## DEBUG
 
 
 Default: False
 Default: False

+ 21 - 16
docs/installation/netbox.md

@@ -1,19 +1,16 @@
 # Installation
 # Installation
 
 
-NetBox requires following system dependencies:
+**Debian/Ubuntu**
 
 
-* python2.7
-* python-dev
-* python-pip
-* libxml2-dev
-* libxslt1-dev
-* libffi-dev
-* graphviz
-* libpq-dev
-* libssl-dev
+```
+# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
+```
+
+**CentOS/RHEL**
 
 
 ```
 ```
-# sudo apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
+# yum install -y epel-release
+# yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
 ```
 ```
 
 
 You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.
 You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.
@@ -41,8 +38,16 @@ Create the base directory for the NetBox installation. For this guide, we'll use
 
 
 If `git` is not already installed, install it:
 If `git` is not already installed, install it:
 
 
+**Debian/Ubuntu**
+
 ```
 ```
-# sudo apt-get install -y git
+# apt-get install -y git
+```
+
+**CentOS/RHEL**
+
+```
+# yum install -y git
 ```
 ```
 
 
 Next, clone the **master** branch of the NetBox GitHub repository into the current directory:
 Next, clone the **master** branch of the NetBox GitHub repository into the current directory:
@@ -63,7 +68,7 @@ Checking connectivity... done.
 Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.)
 Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.)
 
 
 ```
 ```
-# sudo pip install -r requirements.txt
+# pip install -r requirements.txt
 ```
 ```
 
 
 # Configuration
 # Configuration
@@ -76,7 +81,7 @@ Move into the NetBox configuration directory and make a copy of `configuration.e
 ```
 ```
 
 
 Open `configuration.py` with your preferred editor and set the following variables:
 Open `configuration.py` with your preferred editor and set the following variables:
- 
+
 * ALLOWED_HOSTS
 * ALLOWED_HOSTS
 * DATABASE
 * DATABASE
 * SECRET_KEY
 * SECRET_KEY
@@ -143,8 +148,8 @@ NetBox does not come with any predefined user accounts. You'll need to create a
 # ./manage.py createsuperuser
 # ./manage.py createsuperuser
 Username: admin
 Username: admin
 Email address: admin@example.com
 Email address: admin@example.com
-Password: 
-Password (again): 
+Password:
+Password (again):
 Superuser created successfully.
 Superuser created successfully.
 ```
 ```
 
 

+ 22 - 6
docs/installation/postgresql.md

@@ -2,17 +2,33 @@ NetBox requires a PostgreSQL database to store data. MySQL is not supported, as
 
 
 # Installation
 # Installation
 
 
-The following packages are needed to install PostgreSQL with Python support:
+**Debian/Ubuntu**
 
 
-* postgresql
-* libpq-dev
-* python-psycopg2
+```
+# apt-get install -y postgresql libpq-dev python-psycopg2
+```
+
+**CentOS/RHEL**
+
+```
+# yum install -y postgresql postgresql-server postgresql-devel python-psycopg2
+# postgresql-setup initdb
+```
+
+If using CentOS, modify the PostgreSQL configuration to accept password-based authentication by replacing `ident` with `md5` for all host entries within `/var/lib/pgsql/data/pg_hba.conf`. For example:
+
+```
+host    all             all             127.0.0.1/32            md5
+host    all             all             ::1/128                 md5
+```
+
+Then, start the service:
 
 
 ```
 ```
-# sudo apt-get install -y postgresql libpq-dev python-psycopg2
+# systemctl start postgresql
 ```
 ```
 
 
-# Configuration
+# Database Creation
 
 
 At a minimum, we need to create a database for NetBox and assign it a username and password for authentication. This is done with the following commands.
 At a minimum, we need to create a database for NetBox and assign it a username and password for authentication. This is done with the following commands.
 
 

+ 12 - 10
docs/installation/web-server.md

@@ -1,9 +1,12 @@
 # Web Server Installation
 # Web Server Installation
 
 
-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. 
+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.
 
 
 ```
 ```
-# sudo apt-get install -y gunicorn supervisor
+# apt-get install -y gunicorn supervisor
 ```
 ```
 
 
 ## Option A: nginx
 ## Option A: nginx
@@ -11,10 +14,10 @@ We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for
 The following will serve as a minimal nginx configuration. Be sure to modify your server name and installation path appropriately.
 The following will serve as a minimal nginx configuration. Be sure to modify your server name and installation path appropriately.
 
 
 ```
 ```
-# sudo apt-get install -y nginx
+# apt-get install -y nginx
 ```
 ```
 
 
-Once nginx is installed, proceed with the following configuration:
+Once nginx is installed, save the following configuration to `/etc/nginx/sites-available/netbox`. Be sure to replace `netbox.example.com` with the domain name or IP address of your installation. (This should match the value configured for `ALLOWED_HOSTS` in `configuration.py`.)
 
 
 ```
 ```
 server {
 server {
@@ -38,19 +41,18 @@ server {
 }
 }
 ```
 ```
 
 
-Save this configuration to `/etc/nginx/sites-available/netbox`. Then, delete `/etc/nginx/sites-enabled/default` and create a symlink in the `sites-enabled` directory to the configuration file you just created.
+Then, delete `/etc/nginx/sites-enabled/default` and create a symlink in the `sites-enabled` directory to the configuration file you just created.
 
 
 ```
 ```
 # cd /etc/nginx/sites-enabled/
 # cd /etc/nginx/sites-enabled/
 # rm default
 # rm default
-# ln -s /etc/nginx/sites-available/netbox 
+# ln -s /etc/nginx/sites-available/netbox
 ```
 ```
 
 
 Restart the nginx service to use the new configuration.
 Restart the nginx service to use the new configuration.
 
 
 ```
 ```
 # service nginx restart
 # service nginx restart
- * Restarting nginx nginx
 ```
 ```
 
 
 To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04).
 To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04).
@@ -58,7 +60,7 @@ To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https:
 ## Option B: Apache
 ## Option B: Apache
 
 
 ```
 ```
-# sudo apt-get install -y apache2
+# apt-get install -y apache2
 ```
 ```
 
 
 Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately):
 Once Apache is installed, proceed with the following configuration (Be sure to modify the `ServerName` appropriately):
@@ -99,7 +101,7 @@ To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https
 
 
 # gunicorn Installation
 # gunicorn Installation
 
 
-Save the following configuration file in the root netbox installation path (in this example, `/opt/netbox/`) as `gunicorn_config.py`. Be sure to verify the location of the gunicorn executable (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed.
+Save the following configuration file in the root netbox installation path (in this example, `/opt/netbox/`) as `gunicorn_config.py`. Be sure to verify the location of the gunicorn executable (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed. If using CentOS/RHEL change the username from `www-data` to `nginx` or `apache`.
 
 
 ```
 ```
 command = '/usr/bin/gunicorn'
 command = '/usr/bin/gunicorn'
@@ -120,7 +122,7 @@ directory = /opt/netbox/netbox/
 user = www-data
 user = www-data
 ```
 ```
 
 
-Finally, restart the supervisor service to detect and run the gunicorn service:
+Then, restart the supervisor service to detect and run the gunicorn service:
 
 
 ```
 ```
 # service supervisor restart
 # service supervisor restart

+ 10 - 1
netbox/dcim/api/serializers.py

@@ -5,6 +5,7 @@ from dcim.models import (
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
     DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
     DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
     PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
     PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
+    SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
 )
 )
 from extras.api.serializers import CustomFieldSerializer
 from extras.api.serializers import CustomFieldSerializer
 from tenancy.api.serializers import TenantNestedSerializer
 from tenancy.api.serializers import TenantNestedSerializer
@@ -131,11 +132,19 @@ class ManufacturerNestedSerializer(ManufacturerSerializer):
 
 
 class DeviceTypeSerializer(serializers.ModelSerializer):
 class DeviceTypeSerializer(serializers.ModelSerializer):
     manufacturer = ManufacturerNestedSerializer()
     manufacturer = ManufacturerNestedSerializer()
+    subdevice_role = serializers.SerializerMethodField()
 
 
     class Meta:
     class Meta:
         model = DeviceType
         model = DeviceType
         fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
         fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
-                  'is_console_server', 'is_pdu', 'is_network_device']
+                  'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role']
+
+    def get_subdevice_role(self, obj):
+        return {
+            SUBDEVICE_ROLE_PARENT: 'parent',
+            SUBDEVICE_ROLE_CHILD: 'child',
+            None: None,
+        }[obj.subdevice_role]
 
 
 
 
 class DeviceTypeNestedSerializer(DeviceTypeSerializer):
 class DeviceTypeNestedSerializer(DeviceTypeSerializer):

+ 1 - 1
netbox/dcim/forms.py

@@ -593,7 +593,7 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
 class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
 class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Device
     model = Device
     site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')), to_field_name='slug')
     site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')), to_field_name='slug')
-    rack_group_id = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')),
+    rack_group_id = FilterChoiceField(queryset=RackGroup.objects.annotate(filter_count=Count('racks__devices')),
                                       label='Rack Group')
                                       label='Rack Group')
     role = FilterChoiceField(queryset=DeviceRole.objects.annotate(filter_count=Count('devices')), to_field_name='slug')
     role = FilterChoiceField(queryset=DeviceRole.objects.annotate(filter_count=Count('devices')), to_field_name='slug')
     tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
     tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',

+ 18 - 0
netbox/dcim/models.py

@@ -576,11 +576,29 @@ class DeviceType(models.Model):
     def __unicode__(self):
     def __unicode__(self):
         return u'{} {}'.format(self.manufacturer, self.model)
         return u'{} {}'.format(self.manufacturer, self.model)
 
 
+    def __init__(self, *args, **kwargs):
+        super(DeviceType, self).__init__(*args, **kwargs)
+
+        # Save a copy of u_height for validation in clean()
+        self._original_u_height = self.u_height
+
     def get_absolute_url(self):
     def get_absolute_url(self):
         return reverse('dcim:devicetype', args=[self.pk])
         return reverse('dcim:devicetype', args=[self.pk])
 
 
     def clean(self):
     def clean(self):
 
 
+        # If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have
+        # room to expand within their racks. This validation will impose a very high performance penalty when there are
+        # many instances to check, but increasing the u_height of a DeviceType should be a very rare occurrence.
+        if self.pk is not None and self.u_height > self._original_u_height:
+            for d in Device.objects.filter(device_type=self, position__isnull=False):
+                face_required = None if self.is_full_depth else d.face
+                u_available = d.rack.get_available_units(u_height=self.u_height, rack_face=face_required,
+                                                         exclude=[d.pk])
+                if d.position not in u_available:
+                    raise ValidationError("Device {} in rack {} does not have sufficient space to accommodate a height "
+                                          "of {}U".format(d, d.rack, self.u_height))
+
         if not self.is_console_server and self.cs_port_templates.count():
         if not self.is_console_server and self.cs_port_templates.count():
             raise ValidationError("Must delete all console server port templates associated with this device before "
             raise ValidationError("Must delete all console server port templates associated with this device before "
                                   "declassifying it as a console server.")
                                   "declassifying it as a console server.")

+ 33 - 30
netbox/dcim/tests/test_apis.py

@@ -2,6 +2,8 @@ import json
 from rest_framework import status
 from rest_framework import status
 from rest_framework.test import APITestCase
 from rest_framework.test import APITestCase
 
 
+from django.conf import settings
+
 
 
 class SiteTest(APITestCase):
 class SiteTest(APITestCase):
 
 
@@ -57,7 +59,7 @@ class SiteTest(APITestCase):
         'embed_link',
         'embed_link',
     ]
     ]
 
 
-    def test_get_list(self, endpoint='/api/dcim/sites/'):
+    def test_get_list(self, endpoint='/{}api/dcim/sites/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -67,7 +69,7 @@ class SiteTest(APITestCase):
                 sorted(self.standard_fields),
                 sorted(self.standard_fields),
             )
             )
 
 
-    def test_get_detail(self, endpoint='/api/dcim/sites/1/'):
+    def test_get_detail(self, endpoint='/{}api/dcim/sites/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -76,7 +78,7 @@ class SiteTest(APITestCase):
             sorted(self.standard_fields),
             sorted(self.standard_fields),
         )
         )
 
 
-    def test_get_site_list_rack(self, endpoint='/api/dcim/sites/1/racks/'):
+    def test_get_site_list_rack(self, endpoint='/{}api/dcim/sites/1/racks/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -91,7 +93,7 @@ class SiteTest(APITestCase):
                 sorted(self.nested_fields),
                 sorted(self.nested_fields),
             )
             )
 
 
-    def test_get_site_list_graphs(self, endpoint='/api/dcim/sites/1/graphs/'):
+    def test_get_site_list_graphs(self, endpoint='/{}api/dcim/sites/1/graphs/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -149,7 +151,7 @@ class RackTest(APITestCase):
         'rear_units'
         'rear_units'
     ]
     ]
 
 
-    def test_get_list(self, endpoint='/api/dcim/racks/'):
+    def test_get_list(self, endpoint='/{}api/dcim/racks/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -163,7 +165,7 @@ class RackTest(APITestCase):
                 sorted(SiteTest.nested_fields),
                 sorted(SiteTest.nested_fields),
             )
             )
 
 
-    def test_get_detail(self, endpoint='/api/dcim/racks/1/'):
+    def test_get_detail(self, endpoint='/{}api/dcim/racks/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -192,7 +194,7 @@ class ManufacturersTest(APITestCase):
 
 
     nested_fields = standard_fields
     nested_fields = standard_fields
 
 
-    def test_get_list(self, endpoint='/api/dcim/manufacturers/'):
+    def test_get_list(self, endpoint='/{}api/dcim/manufacturers/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -202,7 +204,7 @@ class ManufacturersTest(APITestCase):
                 sorted(self.standard_fields),
                 sorted(self.standard_fields),
             )
             )
 
 
-    def test_get_detail(self, endpoint='/api/dcim/manufacturers/1/'):
+    def test_get_detail(self, endpoint='/{}api/dcim/manufacturers/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -227,6 +229,7 @@ class DeviceTypeTest(APITestCase):
         'is_console_server',
         'is_console_server',
         'is_pdu',
         'is_pdu',
         'is_network_device',
         'is_network_device',
+        'subdevice_role',
     ]
     ]
 
 
     nested_fields = [
     nested_fields = [
@@ -236,7 +239,7 @@ class DeviceTypeTest(APITestCase):
         'slug'
         'slug'
     ]
     ]
 
 
-    def test_get_list(self, endpoint='/api/dcim/device-types/'):
+    def test_get_list(self, endpoint='/{}api/dcim/device-types/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -246,7 +249,7 @@ class DeviceTypeTest(APITestCase):
                 sorted(self.standard_fields),
                 sorted(self.standard_fields),
             )
             )
 
 
-    def test_detail_list(self, endpoint='/api/dcim/device-types/1/'):
+    def test_detail_list(self, endpoint='/{}api/dcim/device-types/1/'.format(settings.BASE_PATH)):
         # TODO: details returns list view.
         # TODO: details returns list view.
         # response = self.client.get(endpoint)
         # response = self.client.get(endpoint)
         # content = json.loads(response.content)
         # content = json.loads(response.content)
@@ -270,7 +273,7 @@ class DeviceRolesTest(APITestCase):
 
 
     nested_fields = ['id', 'name', 'slug']
     nested_fields = ['id', 'name', 'slug']
 
 
-    def test_get_list(self, endpoint='/api/dcim/device-roles/'):
+    def test_get_list(self, endpoint='/{}api/dcim/device-roles/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -280,7 +283,7 @@ class DeviceRolesTest(APITestCase):
                 sorted(self.standard_fields),
                 sorted(self.standard_fields),
             )
             )
 
 
-    def test_get_detail(self, endpoint='/api/dcim/device-roles/1/'):
+    def test_get_detail(self, endpoint='/{}api/dcim/device-roles/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -298,7 +301,7 @@ class PlatformsTest(APITestCase):
 
 
     nested_fields = ['id', 'name', 'slug']
     nested_fields = ['id', 'name', 'slug']
 
 
-    def test_get_list(self, endpoint='/api/dcim/platforms/'):
+    def test_get_list(self, endpoint='/{}api/dcim/platforms/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -308,7 +311,7 @@ class PlatformsTest(APITestCase):
                 sorted(self.standard_fields),
                 sorted(self.standard_fields),
             )
             )
 
 
-    def test_get_detail(self, endpoint='/api/dcim/platforms/1/'):
+    def test_get_detail(self, endpoint='/{}api/dcim/platforms/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -346,7 +349,7 @@ class DeviceTest(APITestCase):
 
 
     nested_fields = ['id', 'name', 'display_name']
     nested_fields = ['id', 'name', 'display_name']
 
 
-    def test_get_list(self, endpoint='/api/dcim/devices/'):
+    def test_get_list(self, endpoint='/{}api/dcim/devices/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -373,7 +376,7 @@ class DeviceTest(APITestCase):
                 sorted(RackTest.nested_fields),
                 sorted(RackTest.nested_fields),
             )
             )
 
 
-    def test_get_list_flat(self, endpoint='/api/dcim/devices/?format=json_flat'):
+    def test_get_list_flat(self, endpoint='/{}api/dcim/devices/?format=json_flat'.format(settings.BASE_PATH)):
 
 
         flat_fields = [
         flat_fields = [
             'asset_tag',
             'asset_tag',
@@ -421,7 +424,7 @@ class DeviceTest(APITestCase):
             sorted(flat_fields),
             sorted(flat_fields),
         )
         )
 
 
-    def test_get_detail(self, endpoint='/api/dcim/devices/1/'):
+    def test_get_detail(self, endpoint='/{}api/dcim/devices/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -439,7 +442,7 @@ class ConsoleServerPortsTest(APITestCase):
 
 
     nested_fields = ['id', 'device', 'name']
     nested_fields = ['id', 'device', 'name']
 
 
-    def test_get_list(self, endpoint='/api/dcim/devices/9/console-server-ports/'):
+    def test_get_list(self, endpoint='/{}api/dcim/devices/9/console-server-ports/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -461,7 +464,7 @@ class ConsolePortsTest(APITestCase):
 
 
     nested_fields = ['id', 'device', 'name']
     nested_fields = ['id', 'device', 'name']
 
 
-    def test_get_list(self, endpoint='/api/dcim/devices/1/console-ports/'):
+    def test_get_list(self, endpoint='/{}api/dcim/devices/1/console-ports/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -479,7 +482,7 @@ class ConsolePortsTest(APITestCase):
                 sorted(ConsoleServerPortsTest.nested_fields),
                 sorted(ConsoleServerPortsTest.nested_fields),
             )
             )
 
 
-    def test_get_detail(self, endpoint='/api/dcim/console-ports/1/'):
+    def test_get_detail(self, endpoint='/{}api/dcim/console-ports/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -500,7 +503,7 @@ class PowerPortsTest(APITestCase):
 
 
     nested_fields = ['id', 'device', 'name']
     nested_fields = ['id', 'device', 'name']
 
 
-    def test_get_list(self, endpoint='/api/dcim/devices/1/power-ports/'):
+    def test_get_list(self, endpoint='/{}api/dcim/devices/1/power-ports/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -514,7 +517,7 @@ class PowerPortsTest(APITestCase):
                 sorted(DeviceTest.nested_fields),
                 sorted(DeviceTest.nested_fields),
             )
             )
 
 
-    def test_get_detail(self, endpoint='/api/dcim/power-ports/1/'):
+    def test_get_detail(self, endpoint='/{}api/dcim/power-ports/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -535,7 +538,7 @@ class PowerOutletsTest(APITestCase):
 
 
     nested_fields = ['id', 'device', 'name']
     nested_fields = ['id', 'device', 'name']
 
 
-    def test_get_list(self, endpoint='/api/dcim/devices/11/power-outlets/'):
+    def test_get_list(self, endpoint='/{}api/dcim/devices/11/power-outlets/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -585,7 +588,7 @@ class InterfaceTest(APITestCase):
         'connection_status',
         'connection_status',
     ]
     ]
 
 
-    def test_get_list(self, endpoint='/api/dcim/devices/1/interfaces/'):
+    def test_get_list(self, endpoint='/{}api/dcim/devices/1/interfaces/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -599,7 +602,7 @@ class InterfaceTest(APITestCase):
                 sorted(DeviceTest.nested_fields),
                 sorted(DeviceTest.nested_fields),
             )
             )
 
 
-    def test_get_detail(self, endpoint='/api/dcim/interfaces/1/'):
+    def test_get_detail(self, endpoint='/{}api/dcim/interfaces/1/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -612,7 +615,7 @@ class InterfaceTest(APITestCase):
             sorted(DeviceTest.nested_fields),
             sorted(DeviceTest.nested_fields),
         )
         )
 
 
-    def test_get_graph_list(self, endpoint='/api/dcim/interfaces/1/graphs/'):
+    def test_get_graph_list(self, endpoint='/{}api/dcim/interfaces/1/graphs/'.format(settings.BASE_PATH)):
             response = self.client.get(endpoint)
             response = self.client.get(endpoint)
             content = json.loads(response.content)
             content = json.loads(response.content)
             self.assertEqual(response.status_code, status.HTTP_200_OK)
             self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -622,7 +625,8 @@ class InterfaceTest(APITestCase):
                     sorted(SiteTest.graph_fields),
                     sorted(SiteTest.graph_fields),
                 )
                 )
 
 
-    def test_get_interface_connections(self, endpoint='/api/dcim/interface-connections/4/'):
+    def test_get_interface_connections(self, endpoint='/{}api/dcim/interface-connections/4/'
+                                       .format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -643,9 +647,8 @@ class RelatedConnectionsTest(APITestCase):
         'interfaces',
         'interfaces',
     ]
     ]
 
 
-    def test_get_list(self, endpoint=(
-            '/api/dcim/related-connections/'
-            '?peer-device=test1-edge1&peer-interface=xe-0/0/3')):
+    def test_get_list(self, endpoint=('/{}api/dcim/related-connections/?peer-device=test1-edge1&peer-interface=xe-0/0/3'
+                                      .format(settings.BASE_PATH))):
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)
         content = json.loads(response.content)
         content = json.loads(response.content)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)

+ 1 - 1
netbox/extras/admin.py

@@ -40,7 +40,7 @@ class GraphAdmin(admin.ModelAdmin):
 
 
 @admin.register(ExportTemplate)
 @admin.register(ExportTemplate)
 class ExportTemplateAdmin(admin.ModelAdmin):
 class ExportTemplateAdmin(admin.ModelAdmin):
-    list_display = ['content_type', 'name', 'mime_type', 'file_extension']
+    list_display = ['name', 'content_type', 'description', 'mime_type', 'file_extension']
 
 
 
 
 @admin.register(TopologyMap)
 @admin.register(TopologyMap)

+ 3 - 2
netbox/extras/forms.py

@@ -3,6 +3,7 @@ from collections import OrderedDict
 from django import forms
 from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 
 
+from utilities.forms import LaxURLField
 from .models import (
 from .models import (
     CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue
     CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue
 )
 )
@@ -56,7 +57,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
 
 
         # URL
         # URL
         elif cf.type == CF_TYPE_URL:
         elif cf.type == CF_TYPE_URL:
-            field = forms.URLField(required=cf.required, initial=cf.default)
+            field = LaxURLField(required=cf.required, initial=cf.default)
 
 
         # Text
         # Text
         else:
         else:
@@ -92,7 +93,7 @@ class CustomFieldForm(forms.ModelForm):
             existing_values = CustomFieldValue.objects.filter(obj_type=self.obj_type, obj_id=self.instance.pk)\
             existing_values = CustomFieldValue.objects.filter(obj_type=self.obj_type, obj_id=self.instance.pk)\
                 .select_related('field')
                 .select_related('field')
             for cfv in existing_values:
             for cfv in existing_values:
-                self.initial['cf_{}'.format(str(cfv.field.name))] = cfv.value
+                self.initial['cf_{}'.format(str(cfv.field.name))] = cfv.serialized_value
 
 
     def _save_custom_fields(self):
     def _save_custom_fields(self):
 
 

+ 25 - 0
netbox/extras/migrations/0003_exporttemplate_add_description.py

@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10 on 2016-09-27 20:20
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('extras', '0002_custom_fields'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='exporttemplate',
+            name='description',
+            field=models.CharField(blank=True, max_length=200),
+        ),
+        migrations.AlterField(
+            model_name='exporttemplate',
+            name='name',
+            field=models.CharField(max_length=100),
+        ),
+    ]

+ 12 - 3
netbox/extras/models.py

@@ -146,8 +146,10 @@ class CustomField(models.Model):
             # Read date as YYYY-MM-DD
             # Read date as YYYY-MM-DD
             return date(*[int(n) for n in serialized_value.split('-')])
             return date(*[int(n) for n in serialized_value.split('-')])
         if self.type == CF_TYPE_SELECT:
         if self.type == CF_TYPE_SELECT:
-            # return CustomFieldChoice.objects.get(pk=int(serialized_value))
-            return self.choices.get(pk=int(serialized_value))
+            try:
+                return self.choices.get(pk=int(serialized_value))
+            except CustomFieldChoice.DoesNotExist:
+                return None
         return serialized_value
         return serialized_value
 
 
 
 
@@ -198,6 +200,12 @@ class CustomFieldChoice(models.Model):
         if self.field.type != CF_TYPE_SELECT:
         if self.field.type != CF_TYPE_SELECT:
             raise ValidationError("Custom field choices can only be assigned to selection fields.")
             raise ValidationError("Custom field choices can only be assigned to selection fields.")
 
 
+    def delete(self, using=None, keep_parents=False):
+        # When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
+        pk = self.pk
+        super(CustomFieldChoice, self).delete(using, keep_parents)
+        CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
+
 
 
 class Graph(models.Model):
 class Graph(models.Model):
     type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
     type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
@@ -225,7 +233,8 @@ class Graph(models.Model):
 
 
 class ExportTemplate(models.Model):
 class ExportTemplate(models.Model):
     content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
     content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
-    name = models.CharField(max_length=200)
+    name = models.CharField(max_length=100)
+    description = models.CharField(max_length=200, blank=True)
     template_code = models.TextField()
     template_code = models.TextField()
     mime_type = models.CharField(max_length=15, blank=True)
     mime_type = models.CharField(max_length=15, blank=True)
     file_extension = models.CharField(max_length=15, blank=True)
     file_extension = models.CharField(max_length=15, blank=True)

+ 4 - 0
netbox/netbox/configuration.docker.py

@@ -52,6 +52,10 @@ EMAIL = {
 # are permitted to access most data in NetBox (excluding secrets) but not make any changes.
 # are permitted to access most data in NetBox (excluding secrets) but not make any changes.
 LOGIN_REQUIRED = os.environ.get('LOGIN_REQUIRED', False)
 LOGIN_REQUIRED = os.environ.get('LOGIN_REQUIRED', False)
 
 
+# Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set:
+# BASE_PATH = 'netbox/'
+BASE_PATH = os.environ.get('BASE_PATH', '')
+
 # Setting this to True will display a "maintenance mode" banner at the top of every page.
 # Setting this to True will display a "maintenance mode" banner at the top of every page.
 MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', False)
 MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', False)
 
 

+ 4 - 0
netbox/netbox/configuration.example.py

@@ -52,6 +52,10 @@ EMAIL = {
 # are permitted to access most data in NetBox (excluding secrets) but not make any changes.
 # are permitted to access most data in NetBox (excluding secrets) but not make any changes.
 LOGIN_REQUIRED = False
 LOGIN_REQUIRED = False
 
 
+# Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set:
+# BASE_PATH = 'netbox/'
+BASE_PATH = ''
+
 # Setting this to True will display a "maintenance mode" banner at the top of every page.
 # Setting this to True will display a "maintenance mode" banner at the top of every page.
 MAINTENANCE_MODE = False
 MAINTENANCE_MODE = False
 
 

+ 4 - 1
netbox/netbox/settings.py

@@ -12,7 +12,7 @@ except ImportError:
                                "the documentation.")
                                "the documentation.")
 
 
 
 
-VERSION = '1.6.1-r1'
+VERSION = '1.6.2'
 
 
 # Import local configuration
 # Import local configuration
 for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
 for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
@@ -27,6 +27,9 @@ ADMINS = getattr(configuration, 'ADMINS', [])
 DEBUG = getattr(configuration, 'DEBUG', False)
 DEBUG = getattr(configuration, 'DEBUG', False)
 EMAIL = getattr(configuration, 'EMAIL', {})
 EMAIL = getattr(configuration, 'EMAIL', {})
 LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
 LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
+BASE_PATH = getattr(configuration, 'BASE_PATH', '')
+if BASE_PATH:
+    BASE_PATH = BASE_PATH.strip('/') + '/'  # Enforce trailing slash only
 MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
 MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
 PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
 PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
 NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')
 NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')

+ 7 - 1
netbox/netbox/urls.py

@@ -1,3 +1,4 @@
+from django.conf import settings
 from django.conf.urls import include, url
 from django.conf.urls import include, url
 from django.contrib import admin
 from django.contrib import admin
 from django.views.defaults import page_not_found
 from django.views.defaults import page_not_found
@@ -8,7 +9,7 @@ from users.views import login, logout
 
 
 handler500 = handle_500
 handler500 = handle_500
 
 
-urlpatterns = [
+_patterns = [
 
 
     # Default page
     # Default page
     url(r'^$', home, name='home'),
     url(r'^$', home, name='home'),
@@ -42,3 +43,8 @@ urlpatterns = [
     url(r'^admin/', include(admin.site.urls)),
     url(r'^admin/', include(admin.site.urls)),
 
 
 ]
 ]
+
+# Prepend BASE_PATH
+urlpatterns = [
+    url(r'^{}'.format(settings.BASE_PATH), include(_patterns))
+]

+ 10 - 5
netbox/project-static/js/forms.js

@@ -1,13 +1,18 @@
 $(document).ready(function() {
 $(document).ready(function() {
 
 
-    // "Select all" checkbox in a table header
-    $('th input:checkbox[name=_all]').click(function (event) {
-        $(this).parents('table').find('td input:checkbox').prop('checked', $(this).prop('checked'));
+    // "Toggle all" checkbox in a table header
+    $('#toggle_all').click(function (event) {
+        $('td input:checkbox[name=pk]').prop('checked', $(this).prop('checked'));
+        if ($(this).is(':checked')) {
+            $('#select_all_box').removeClass('hidden');
+        } else {
+            $('#select_all').prop('checked', false);
+        }
     });
     });
-    // Uncheck the "select all" checkbox if an item is unchecked
+    // Uncheck the "toggle all" checkbox if an item is unchecked
     $('input:checkbox[name=pk]').click(function (event) {
     $('input:checkbox[name=pk]').click(function (event) {
         if (!$(this).attr('checked')) {
         if (!$(this).attr('checked')) {
-            $(this).parents('table').find('input:checkbox[name=_all]').prop('checked', false);
+            $('#select_all, #toggle_all').prop('checked', false);
         }
         }
     });
     });
 
 

+ 1 - 1
netbox/secrets/forms.py

@@ -91,7 +91,7 @@ class SecretImportForm(BulkImportForm, BootstrapMixin):
 
 
 class SecretBulkEditForm(forms.Form, BootstrapMixin):
 class SecretBulkEditForm(forms.Form, BootstrapMixin):
     pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
     pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
-    role = forms.ModelChoiceField(queryset=SecretRole.objects.all())
+    role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False)
     name = forms.CharField(max_length=100, required=False)
     name = forms.CharField(max_length=100, required=False)
 
 
 
 

+ 1 - 1
netbox/templates/500.html

@@ -26,7 +26,7 @@
                     <pre><strong>{{ exception }}</strong><br />
                     <pre><strong>{{ exception }}</strong><br />
 {{ error }}</pre>
 {{ error }}</pre>
                     <div class="text-right">
                     <div class="text-right">
-                        <a href="/" class="btn btn-primary">Home Page</a>
+                        <a href="{% url 'home' %}" class="btn btn-primary">Home Page</a>
                     </div>
                     </div>
                 </div>
                 </div>
             </div>
             </div>

+ 3 - 2
netbox/templates/_base.html

@@ -9,6 +9,7 @@
     <link rel="stylesheet" href="{% static 'jquery-ui-1.11.4/jquery-ui.css' %}">
     <link rel="stylesheet" href="{% static 'jquery-ui-1.11.4/jquery-ui.css' %}">
 	<link rel="stylesheet" href="{% static 'css/base.css' %}">
 	<link rel="stylesheet" href="{% static 'css/base.css' %}">
     <link rel="icon" type="image/png" href="{% static 'img/netbox.ico' %}" />
     <link rel="icon" type="image/png" href="{% static 'img/netbox.ico' %}" />
+    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
 </head>
 </head>
 <body>
 <body>
     <nav class="navbar navbar-default navbar-fixed-top">
     <nav class="navbar navbar-default navbar-fixed-top">
@@ -20,7 +21,7 @@
                     <span class="icon-bar"></span>
                     <span class="icon-bar"></span>
                     <span class="icon-bar"></span>
                     <span class="icon-bar"></span>
                 </button>
                 </button>
-                <a class="navbar-brand" href="/">
+                <a class="navbar-brand" href="{% url 'home' %}">
                     <img src="{% static 'img/netbox_logo.png' %}" />
                     <img src="{% static 'img/netbox_logo.png' %}" />
                 </a>
                 </a>
             </div>
             </div>
@@ -288,7 +289,7 @@
                 <div class="col-xs-4 text-right">
                 <div class="col-xs-4 text-right">
                     <p class="text-muted">
                     <p class="text-muted">
                         <i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/" target="_blank">Docs</a> &middot;
                         <i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/" target="_blank">Docs</a> &middot;
-                        <i class="fa fa-fw fa-cloud text-primary"></i> <a href="/api/docs/">API</a> &middot;
+                        <i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'django.swagger.base.view' %}">API</a> &middot;
                         <i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a>
                         <i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a>
                     </p>
                     </p>
                 </div>
                 </div>

+ 10 - 3
netbox/templates/circuits/circuit_bulk_edit.html

@@ -3,14 +3,21 @@
 
 
 {% block title %}Circuit Bulk Edit{% endblock %}
 {% block title %}Circuit Bulk Edit{% endblock %}
 
 
-{% block select_objects_table %}
+{% block selected_objects_table %}
+    <tr>
+        <th>Circuit</th>
+        <th>Type</th>
+        <th>Provider</th>
+        <th>Port speed</th>
+        <th>Commit rate</th>
+    </tr>
     {% for circuit in selected_objects %}
     {% for circuit in selected_objects %}
         <tr>
         <tr>
             <td><a href="{% url 'circuits:circuit' pk=circuit.pk %}">{{ circuit }}</a></td>
             <td><a href="{% url 'circuits:circuit' pk=circuit.pk %}">{{ circuit }}</a></td>
             <td>{{ circuit.type }}</td>
             <td>{{ circuit.type }}</td>
             <td>{{ circuit.provider }}</td>
             <td>{{ circuit.provider }}</td>
-            <td>{{ circuit.port_speed }} Kbps</td>
-            <td>{{ circuit.commit_rate }}</td>
+            <td>{{ circuit.port_speed_human }}</td>
+            <td>{{ circuit.commit_rate_human }}</td>
         </tr>
         </tr>
     {% endfor %}
     {% endfor %}
 {% endblock %}
 {% endblock %}

+ 6 - 1
netbox/templates/circuits/provider_bulk_edit.html

@@ -3,7 +3,12 @@
 
 
 {% block title %}Provider Bulk Edit{% endblock %}
 {% block title %}Provider Bulk Edit{% endblock %}
 
 
-{% block select_objects_table %}
+{% block selected_objects_table %}
+    <tr>
+        <th>Provider</th>
+        <th>Account</th>
+        <th>ASN</th>
+    </tr>
     {% for provider in selected_objects %}
     {% for provider in selected_objects %}
         <tr>
         <tr>
             <td><a href="{% url 'circuits:provider' slug=provider.slug %}">{{ provider }}</a></td>
             <td><a href="{% url 'circuits:provider' slug=provider.slug %}">{{ provider }}</a></td>

+ 98 - 84
netbox/templates/dcim/device.html

@@ -186,18 +186,23 @@
                         {% include 'dcim/inc/_ipaddress.html' %}
                         {% include 'dcim/inc/_ipaddress.html' %}
                     {% endfor %}
                     {% endfor %}
                 </table>
                 </table>
-            {% else %}
+            {% elif interfaces or mgmt_interfaces %}
                 <div class="panel-body text-muted">
                 <div class="panel-body text-muted">
-                    None found
+                    None assigned
+                </div>
+            {% else %}
+                <div class="panel-body">
+                    <a href="{% url 'dcim:interface_add' pk=device.pk %}">Create an interface</a> to assign an IP.
                 </div>
                 </div>
             {% endif %}
             {% endif %}
             {% if perms.ipam.add_ipaddress %}
             {% if perms.ipam.add_ipaddress %}
-                <div class="panel-footer text-right">
-                    <a href="{% url 'dcim:ipaddress_assign' pk=device.pk %}" class="btn btn-xs btn-primary">
-                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
-                        Assign IP address
-                    </a>
-                </div>
+                {% if interfaces or mgmt_interfaces %}
+                    <div class="panel-footer text-right">
+                        <a href="{% url 'dcim:ipaddress_assign' pk=device.pk %}" class="btn btn-xs btn-primary">
+                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign IP address
+                        </a>
+                    </div>
+                {% endif %}
             {% endif %}
             {% endif %}
         </div>
         </div>
         <div class="panel panel-default">
         <div class="panel panel-default">
@@ -210,7 +215,7 @@
                 {% empty %}
                 {% empty %}
                     <tr>
                     <tr>
                         <td colspan="5" class="alert-warning">
                         <td colspan="5" class="alert-warning">
-                            <i class="fa fa-fw fa-warning"></i> No management interfaces defined!
+                            <i class="fa fa-fw fa-warning"></i> No management interfaces defined
                             {% if perms.dcim.add_interface %}
                             {% if perms.dcim.add_interface %}
                                 <a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
                                 <a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
                             {% endif %}
                             {% endif %}
@@ -222,7 +227,7 @@
                 {% empty %}
                 {% empty %}
                     <tr>
                     <tr>
                         <td colspan="5" class="alert-warning">
                         <td colspan="5" class="alert-warning">
-                            <i class="fa fa-fw fa-warning"></i> No console ports defined!
+                            <i class="fa fa-fw fa-warning"></i> No console ports defined
                             {% if perms.dcim.add_consoleport %}
                             {% if perms.dcim.add_consoleport %}
                                 <a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
                                 <a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
                             {% endif %}
                             {% endif %}
@@ -235,7 +240,7 @@
                     {% if not device.device_type.is_pdu %}
                     {% if not device.device_type.is_pdu %}
                         <tr>
                         <tr>
                             <td colspan="5" class="alert-warning">
                             <td colspan="5" class="alert-warning">
-                                <i class="fa fa-fw fa-warning"></i> No power ports defined!
+                                <i class="fa fa-fw fa-warning"></i> No power ports defined
                                 {% if perms.dcim.add_powerport %}
                                 {% if perms.dcim.add_powerport %}
                                     <a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
                                     <a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
                                 {% endif %}
                                 {% endif %}
@@ -248,20 +253,17 @@
                 <div class="panel-footer text-right">
                 <div class="panel-footer text-right">
                     {% if perms.dcim.add_interface %}
                     {% if perms.dcim.add_interface %}
                         <a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-xs btn-primary">
                         <a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-xs btn-primary">
-                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
-                            Add interface
+                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interface
                         </a>
                         </a>
                     {% endif %}
                     {% endif %}
                     {% if perms.dcim.add_consoleport %}
                     {% if perms.dcim.add_consoleport %}
                         <a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
                         <a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
-                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
-                            Add console
+                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
                         </a>
                         </a>
                     {% endif %}
                     {% endif %}
                     {% if perms.dcim.add_powerport and not device.device_type.is_pdu %}
                     {% if perms.dcim.add_powerport and not device.device_type.is_pdu %}
                         <a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
                         <a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
-                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
-                            Add power
+                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
                         </a>
                         </a>
                     {% endif %}
                     {% endif %}
                 </div>
                 </div>
@@ -312,6 +314,13 @@
             <div class="panel panel-default">
             <div class="panel panel-default">
                 <div class="panel-heading">
                 <div class="panel-heading">
                     <strong>Device Bays</strong>
                     <strong>Device Bays</strong>
+                    {% if perms.dcim.add_devicebay and device_bays|length > 10 %}
+                        <div class="pull-right">
+                            <a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs">
+                                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
+                            </a>
+                        </div>
+                    {% endif %}
                 </div>
                 </div>
                 <table class="table table-hover panel-body">
                 <table class="table table-hover panel-body">
                     {% for devicebay in device_bays %}
                     {% for devicebay in device_bays %}
@@ -324,23 +333,19 @@
                 </table>
                 </table>
                 {% if perms.dcim.add_devicebay or perms.dcim.delete_devicebay %}
                 {% if perms.dcim.add_devicebay or perms.dcim.delete_devicebay %}
                     <div class="panel-footer">
                     <div class="panel-footer">
-                        <div class="row">
-                            <div class="col-md-6">
-                                {% if device_bays and perms.dcim.delete_devicebay %}
-                                    <button type="submit" class="btn btn-xs btn-danger">
-                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
-                                    </button>
-                                {% endif %}
-                            </div>
-                            <div class="col-md-6 text-right">
-                                {% if perms.dcim.add_devicebay %}
-                                    <a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
-                                        Add device bay
-                                    </a>
-                                {% endif %}
+                        {% if device_bays and perms.dcim.delete_devicebay %}
+                            <button type="submit" class="btn btn-danger btn-xs">
+                                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
+                            </button>
+                        {% endif %}
+                        {% if perms.dcim.add_devicebay %}
+                            <div class="pull-right">
+                                <a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs">
+                                    <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
+                                </a>
                             </div>
                             </div>
-                        </div>
+                            <div class="clearfix"></div>
+                        {% endif %}
                      </div>
                      </div>
                 {% endif %}
                 {% endif %}
             </div>
             </div>
@@ -356,6 +361,13 @@
             <div class="panel panel-default">
             <div class="panel panel-default">
                 <div class="panel-heading">
                 <div class="panel-heading">
                     <strong>Interfaces</strong>
                     <strong>Interfaces</strong>
+                    {% if perms.dcim.add_interface and interfaces|length > 10 %}
+                        <div class="pull-right">
+                            <a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs">
+                                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
+                            </a>
+                        </div>
+                    {% endif %}
                 </div>
                 </div>
                 <table class="table table-hover panel-body">
                 <table class="table table-hover panel-body">
                     {% for iface in interfaces %}
                     {% for iface in interfaces %}
@@ -368,23 +380,19 @@
                 </table>
                 </table>
                 {% if perms.dcim.add_interface or perms.dcim.delete_interface %}
                 {% if perms.dcim.add_interface or perms.dcim.delete_interface %}
                     <div class="panel-footer">
                     <div class="panel-footer">
-                        <div class="row">
-                            <div class="col-md-6">
-                                {% if interfaces and perms.dcim.delete_interface %}
-                                    <button type="submit" class="btn btn-xs btn-danger">
-                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
-                                    </button>
-                                {% endif %}
-                            </div>
-                            <div class="col-md-6 text-right">
-                                {% if perms.dcim.add_interface %}
-                                    <a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
-                                        Add interface
-                                    </a>
-                                {% endif %}
+                        {% if interfaces and perms.dcim.delete_interface %}
+                            <button type="submit" class="btn btn-danger btn-xs">
+                                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
+                            </button>
+                        {% endif %}
+                        {% if perms.dcim.add_interface %}
+                            <div class="pull-right">
+                                <a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs">
+                                    <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
+                                </a>
                             </div>
                             </div>
-                        </div>
+                            <div class="clearfix"></div>
+                        {% endif %}
                      </div>
                      </div>
                 {% endif %}
                 {% endif %}
             </div>
             </div>
@@ -400,6 +408,13 @@
             <div class="panel panel-default">
             <div class="panel panel-default">
                 <div class="panel-heading">
                 <div class="panel-heading">
                     <strong>Console Server Ports</strong>
                     <strong>Console Server Ports</strong>
+                    {% if perms.dcim.add_consoleserverport and cs_ports|length > 10 %}
+                        <div class="pull-right">
+                            <a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
+                                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
+                            </a>
+                        </div>
+                    {% endif %}
                 </div>
                 </div>
                 <table class="table table-hover panel-body">
                 <table class="table table-hover panel-body">
                     {% for csp in cs_ports %}
                     {% for csp in cs_ports %}
@@ -412,23 +427,19 @@
                 </table>
                 </table>
                 {% if perms.dcim.add_consoleserverport or perms.dcim.delete_consoleserverport %}
                 {% if perms.dcim.add_consoleserverport or perms.dcim.delete_consoleserverport %}
                     <div class="panel-footer">
                     <div class="panel-footer">
-                        <div class="row">
-                            <div class="col-md-6">
-                                {% if cs_ports and perms.dcim.delete_consoleserverport %}
-                                    <button type="submit" class="btn btn-xs btn-danger">
-                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
-                                    </button>
-                                {% endif %}
-                            </div>
-                            <div class="col-md-6 text-right">
-                                {% if perms.dcim.add_consoleserverport %}
-                                    <a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
-                                        Add console server ports
-                                    </a>
-                                {% endif %}
+                        {% if cs_ports and perms.dcim.delete_consoleserverport %}
+                            <button type="submit" class="btn btn-danger btn-xs">
+                                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
+                            </button>
+                        {% endif %}
+                        {% if perms.dcim.add_consoleserverport %}
+                            <div class="pull-right">
+                                <a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
+                                    <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
+                                </a>
                             </div>
                             </div>
-                        </div>
+                            <div class="clearfix"></div>
+                        {% endif %}
                     </div>
                     </div>
                 {% endif %}
                 {% endif %}
             </div>
             </div>
@@ -444,6 +455,13 @@
             <div class="panel panel-default">
             <div class="panel panel-default">
                 <div class="panel-heading">
                 <div class="panel-heading">
                     <strong>Power Outlets</strong>
                     <strong>Power Outlets</strong>
+                    {% if perms.dcim.add_poweroutlet and power_outlets|length > 10 %}
+                        <div class="pull-right">
+                            <a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs">
+                                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
+                            </a>
+                        </div>
+                    {% endif %}
                 </div>
                 </div>
                 <table class="table table-hover panel-body">
                 <table class="table table-hover panel-body">
                     {% for po in power_outlets %}
                     {% for po in power_outlets %}
@@ -456,23 +474,19 @@
                 </table>
                 </table>
                 {% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
                 {% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
                     <div class="panel-footer">
                     <div class="panel-footer">
-                        <div class="row">
-                            <div class="col-md-6">
-                                {% if power_outlets and perms.dcim.delete_poweroutlet %}
-                                    <button type="submit" class="btn btn-xs btn-danger">
-                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
-                                    </button>
-                                {% endif %}
-                            </div>
-                            <div class="col-md-6 text-right">
-                                {% if perms.dcim.add_poweroutlet %}
-                                    <a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
-                                        Add power outlets
-                                    </a>
-                                {% endif %}
+                        {% if power_outlets and perms.dcim.delete_poweroutlet %}
+                            <button type="submit" class="btn btn-danger btn-xs">
+                                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
+                            </button>
+                        {% endif %}
+                        {% if perms.dcim.add_poweroutlet %}
+                            <div class="pull-right">
+                                <a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs">
+                                    <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
+                                </a>
                             </div>
                             </div>
-                        </div>
+                            <div class="clearfix"></div>
+                        {% endif %}
                     </div>
                     </div>
                 {% endif %}
                 {% endif %}
             </div>
             </div>
@@ -531,13 +545,13 @@ function toggleConnection(elem, api_url) {
     return false;
     return false;
 }
 }
 $(".consoleport-toggle").click(function() {
 $(".consoleport-toggle").click(function() {
-    return toggleConnection($(this), "/api/dcim/console-ports/");
+    return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/console-ports/");
 });
 });
 $(".powerport-toggle").click(function() {
 $(".powerport-toggle").click(function() {
-    return toggleConnection($(this), "/api/dcim/power-ports/");
+    return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/power-ports/");
 });
 });
 $(".interface-toggle").click(function() {
 $(".interface-toggle").click(function() {
-    return toggleConnection($(this), "/api/dcim/interface-connections/");
+    return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/interface-connections/");
 });
 });
 </script>
 </script>
 <script src="{% static 'js/graphs.js' %}"></script>
 <script src="{% static 'js/graphs.js' %}"></script>

+ 8 - 1
netbox/templates/dcim/device_bulk_edit.html

@@ -3,7 +3,14 @@
 
 
 {% block title %}Device Bulk Edit{% endblock %}
 {% block title %}Device Bulk Edit{% endblock %}
 
 
-{% block select_objects_table %}
+{% block selected_objects_table %}
+    <tr>
+        <th>Device</th>
+        <th>Type</th>
+        <th>Role</th>
+        <th>Tenant</th>
+        <th>Serial</th>
+    </tr>
     {% for device in selected_objects %}
     {% for device in selected_objects %}
         <tr>
         <tr>
             <td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
             <td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>

+ 7 - 3
netbox/templates/dcim/devicetype_bulk_edit.html

@@ -3,11 +3,15 @@
 
 
 {% block title %}Device Type Bulk Edit{% endblock %}
 {% block title %}Device Type Bulk Edit{% endblock %}
 
 
-{% block select_objects_table %}
+{% block selected_objects_table %}
+    <tr>
+        <th>Device type</th>
+        <th>Manufacturer</th>
+        <th>Height</th>
+    </tr>
     {% for devicetype in selected_objects %}
     {% for devicetype in selected_objects %}
         <tr>
         <tr>
-            <td><a href="{% url 'dcim:devicetype' pk=devicetype.pk %}">{{ devicetype }}</a></td>
-            <td>{{ devicetype.model }}</td>
+            <td><a href="{% url 'dcim:devicetype' pk=devicetype.pk %}">{{ devicetype.model }}</a></td>
             <td>{{ devicetype.manufacturer }}</td>
             <td>{{ devicetype.manufacturer }}</td>
             <td>{{ devicetype.u_height }}U</td>
             <td>{{ devicetype.u_height }}U</td>
         </tr>
         </tr>

+ 9 - 30
netbox/templates/dcim/inc/device_table.html

@@ -1,30 +1,9 @@
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% if table.model|user_can_change:request.user or table.model|user_can_delete:request.user %}
-    <form method="post" class="form form-horizontal">
-        {% csrf_token %}
-        <input type="hidden" name="redirect_url" value="{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" />
-        <input type="hidden" name="pk_all" value="{% for row in table.rows %}{{ row.record.pk|default:'' }}{% if not forloop.last %},{% endif %}{% endfor %}" />
-        {% render_table table table_template|default:'table.html' %}
-        {% if perms.dcim.add_interface %}
-            <button type="submit" name="_edit" formaction="{% url 'dcim:interface_add_multi' %}" class="btn btn-primary btn-sm">
-                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
-                Add Interfaces
-            </button>
-        {% endif %}
-        {% if bulk_edit_url and table.model|user_can_change:request.user %}
-            <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}" class="btn btn-warning btn-sm">
-                <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
-                Edit Selected
-            </button>
-        {% endif %}
-        {% if bulk_delete_url and table.model|user_can_delete:request.user %}
-            <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}" class="btn btn-danger btn-sm">
-                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
-                Delete Selected
-            </button>
-        {% endif %}
-    </form>
-{% else %}
-    {% render_table table table_template|default:'table.html' %}
-{% endif %}
+{% extends 'utilities/obj_table.html' %}
+
+{% block extra_actions %}
+    {% if perms.dcim.add_interface %}
+        <button type="submit" name="_edit" formaction="{% url 'dcim:interface_add_multi' %}" class="btn btn-primary btn-sm">
+            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Interfaces
+        </button>
+    {% endif %}
+{% endblock %}

+ 14 - 4
netbox/templates/dcim/inc/devicetype_component_table.html

@@ -4,11 +4,15 @@
         {% csrf_token %}
         {% csrf_token %}
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
-                <a href="{% url add_url pk=devicetype.pk %}{{ add_url_extra }}" class="btn btn-primary btn-xs pull-right">
-                    <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
-                    Add {{ title }}
-                </a>
                 <strong>{{ title }}</strong>
                 <strong>{{ title }}</strong>
+                {% if table.rows|length > 10 %}
+                    <div class="pull-right">
+                        <a href="{% url add_url pk=devicetype.pk %}{{ add_url_extra }}" class="btn btn-primary btn-xs">
+                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+                            Add {{ title }}
+                        </a>
+                    </div>
+                {% endif %}
             </div>
             </div>
             {% render_table table 'table.html' %}
             {% render_table table 'table.html' %}
             {% if table.rows %}
             {% if table.rows %}
@@ -16,6 +20,12 @@
                     <button type="submit" class="btn btn-xs btn-danger">
                     <button type="submit" class="btn btn-xs btn-danger">
                         <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
                         <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
                     </button>
                     </button>
+                    <div class="pull-right">
+                        <a href="{% url add_url pk=devicetype.pk %}{{ add_url_extra }}" class="btn btn-primary btn-xs">
+                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+                            Add {{ title }}
+                        </a>
+                    </div>
                 </div>
                 </div>
             {% endif %}
             {% endif %}
         </div>
         </div>

+ 6 - 1
netbox/templates/dcim/interface_add_multi.html

@@ -7,7 +7,12 @@
 
 
 {% block form_title %}Interface(s) to Add{% endblock %}
 {% block form_title %}Interface(s) to Add{% endblock %}
 
 
-{% block select_objects_table %}
+{% block selected_objects_table %}
+    <tr>
+        <th>Device</th>
+        <th>Type</th>
+        <th>Role</th>
+    </tr>
     {% for device in selected_objects %}
     {% for device in selected_objects %}
         <tr>
         <tr>
             <td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
             <td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>

+ 3 - 1
netbox/templates/dcim/rack_bulk_edit.html

@@ -3,12 +3,13 @@
 
 
 {% block title %}Rack Bulk Edit{% endblock %}
 {% block title %}Rack Bulk Edit{% endblock %}
 
 
-{% block select_objects_table %}
+{% block selected_objects_table %}
     <tr>
     <tr>
         <th>Name</th>
         <th>Name</th>
         <th>Site</th>
         <th>Site</th>
         <th>Group</th>
         <th>Group</th>
         <th>Tenant</th>
         <th>Tenant</th>
+        <th>Role</th>
         <th>Type</th>
         <th>Type</th>
         <th>Width</th>
         <th>Width</th>
         <th>Height</th>
         <th>Height</th>
@@ -19,6 +20,7 @@
             <td>{{ rack.site }}</td>
             <td>{{ rack.site }}</td>
             <td>{{ rack.group }}</td>
             <td>{{ rack.group }}</td>
             <td>{{ rack.tenant }}</td>
             <td>{{ rack.tenant }}</td>
+            <td>{{ rack.role }}</td>
             <td>{{ rack.get_type_display }}</td>
             <td>{{ rack.get_type_display }}</td>
             <td>{{ rack.get_width_display }}</td>
             <td>{{ rack.get_width_display }}</td>
             <td>{{ rack.u_height }}U</td>
             <td>{{ rack.u_height }}U</td>

+ 5 - 1
netbox/templates/dcim/site_bulk_edit.html

@@ -3,7 +3,11 @@
 
 
 {% block title %}Site Bulk Edit{% endblock %}
 {% block title %}Site Bulk Edit{% endblock %}
 
 
-{% block select_objects_table %}
+{% block selected_objects_table %}
+    <tr>
+        <th>Site</th>
+        <th>Tenant</th>
+    </tr>
     {% for site in selected_objects %}
     {% for site in selected_objects %}
         <tr>
         <tr>
             <td><a href="{% url 'dcim:site' slug=site.slug %}">{{ site }}</a></td>
             <td><a href="{% url 'dcim:site' slug=site.slug %}">{{ site }}</a></td>

+ 1 - 1
netbox/templates/inc/export_button.html

@@ -8,7 +8,7 @@
             <li><a href="?{% if request.GET %}{{ request.GET.urlencode }}&{% endif %}export">CSV (default)</a></li>
             <li><a href="?{% if request.GET %}{{ request.GET.urlencode }}&{% endif %}export">CSV (default)</a></li>
             <li class="divider"></li>
             <li class="divider"></li>
             {% for et in export_templates %}
             {% for et in export_templates %}
-                <li><a href="?{% if request.GET %}{{ request.GET.urlencode }}&{% endif %}export={{ et.name }}">{{ et.name }}</a></li>
+                <li><a href="?{% if request.GET %}{{ request.GET.urlencode }}&{% endif %}export={{ et.name }}"{% if et.description %} title="{{ et.description }}"{% endif %}>{{ et.name }}</a></li>
             {% endfor %}
             {% endfor %}
         </ul>
         </ul>
     </div>
     </div>

+ 7 - 1
netbox/templates/ipam/aggregate_bulk_edit.html

@@ -3,7 +3,13 @@
 
 
 {% block title %}Aggregate Bulk Edit{% endblock %}
 {% block title %}Aggregate Bulk Edit{% endblock %}
 
 
-{% block select_objects_table %}
+{% block selected_objects_table %}
+    <tr>
+        <th>Aggregate</th>
+        <th>RIR</th>
+        <th>Date Added</th>
+        <th>Description</th>
+    </tr>
     {% for aggregate in selected_objects %}
     {% for aggregate in selected_objects %}
         <tr>
         <tr>
             <td><a href="{% url 'ipam:aggregate' pk=aggregate.pk %}">{{ aggregate }}</a></td>
             <td><a href="{% url 'ipam:aggregate' pk=aggregate.pk %}">{{ aggregate }}</a></td>

+ 9 - 3
netbox/templates/ipam/ipaddress_bulk_edit.html

@@ -3,14 +3,20 @@
 
 
 {% block title %}IP Address Bulk Edit{% endblock %}
 {% block title %}IP Address Bulk Edit{% endblock %}
 
 
-{% block select_objects_table %}
+{% block selected_objects_table %}
+    <tr>
+        <th>IP Address</th>
+        <th>VRF</th>
+        <th>Tenant</th>
+        <th>Assigned</th>
+        <th>Description</th>
+    </tr>
     {% for ipaddress in selected_objects %}
     {% for ipaddress in selected_objects %}
         <tr>
         <tr>
             <td><a href="{% url 'ipam:ipaddress' pk=ipaddress.pk %}">{{ ipaddress }}</a></td>
             <td><a href="{% url 'ipam:ipaddress' pk=ipaddress.pk %}">{{ ipaddress }}</a></td>
             <td>{{ ipaddress.vrf|default:"Global" }}</td>
             <td>{{ ipaddress.vrf|default:"Global" }}</td>
             <td>{{ ipaddress.tenant }}</td>
             <td>{{ ipaddress.tenant }}</td>
-            <td>{{ ipaddress.interface.device }}</td>
-            <td>{{ ipaddress.interface }}</td>
+            <td>{% if ipaddress.interface %}<i class="glyphicon glyphicon-ok text-success" title="{{ ipaddress.interface.device }} {{ ipaddress.interface }}"></i>{% endif %}</td>
             <td>{{ ipaddress.description }}</td>
             <td>{{ ipaddress.description }}</td>
         </tr>
         </tr>
     {% endfor %}
     {% endfor %}

+ 11 - 4
netbox/templates/ipam/prefix_bulk_edit.html

@@ -3,16 +3,23 @@
 
 
 {% block title %}Prefix Bulk Edit{% endblock %}
 {% block title %}Prefix Bulk Edit{% endblock %}
 
 
-{% block select_objects_table %}
+{% block selected_objects_table %}
+    <tr>
+        <th>Prefix</th>
+        <th>Site</th>
+        <th>VRF</th>
+        <th>Tenant</th>
+        <th>Status</th>
+        <th>Role</th>
+    </tr>
     {% for prefix in selected_objects %}
     {% for prefix in selected_objects %}
         <tr>
         <tr>
             <td><a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a></td>
             <td><a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a></td>
+            <td>{{ prefix.site }}</td>
             <td>{{ prefix.vrf|default:"Global" }}</td>
             <td>{{ prefix.vrf|default:"Global" }}</td>
             <td>{{ prefix.tenant }}</td>
             <td>{{ prefix.tenant }}</td>
-            <td>{{ prefix.site }}</td>
-            <td>{{ prefix.status }}</td>
+            <td>{{ prefix.get_status_display }}</td>
             <td>{{ prefix.role }}</td>
             <td>{{ prefix.role }}</td>
-            <td>{{ prefix.description }}</td>
         </tr>
         </tr>
     {% endfor %}
     {% endfor %}
 {% endblock %}
 {% endblock %}

+ 11 - 4
netbox/templates/ipam/vlan_bulk_edit.html

@@ -3,16 +3,23 @@
 
 
 {% block title %}VLAN Bulk Edit{% endblock %}
 {% block title %}VLAN Bulk Edit{% endblock %}
 
 
-{% block select_objects_table %}
+{% block selected_objects_table %}
+    <tr>
+        <th>VLAN</th>
+        <th>Site</th>
+        <th>Group</th>
+        <th>Tenant</th>
+        <th>Status</th>
+        <th>Role</th>
+    </tr>
     {% for vlan in selected_objects %}
     {% for vlan in selected_objects %}
         <tr>
         <tr>
-            <td><a href="{% url 'ipam:vlan' pk=vlan.pk %}">{{ vlan.vid }}</a></td>
-            <td>{{ vlan.name }}</td>
+            <td><a href="{% url 'ipam:vlan' pk=vlan.pk %}">{{ vlan }}</a></td>
             <td>{{ vlan.site }}</td>
             <td>{{ vlan.site }}</td>
+            <td>{{ vlan.group }}</td>
             <td>{{ vlan.tenant }}</td>
             <td>{{ vlan.tenant }}</td>
             <td>{{ vlan.get_status_display }}</td>
             <td>{{ vlan.get_status_display }}</td>
             <td>{{ vlan.role }}</td>
             <td>{{ vlan.role }}</td>
-            <td>{{ vlan.description }}</td>
         </tr>
         </tr>
     {% endfor %}
     {% endfor %}
 {% endblock %}
 {% endblock %}

+ 7 - 1
netbox/templates/ipam/vrf_bulk_edit.html

@@ -3,7 +3,13 @@
 
 
 {% block title %}VRF Bulk Edit{% endblock %}
 {% block title %}VRF Bulk Edit{% endblock %}
 
 
-{% block select_objects_table %}
+{% block selected_objects_table %}
+    <tr>
+        <th>VRF</th>
+        <th>RD</th>
+        <th>Tenant</th>
+        <th>Description</th>
+    </tr>
     {% for vrf in selected_objects %}
     {% for vrf in selected_objects %}
         <tr>
         <tr>
             <td><a href="{% url 'ipam:vrf' pk=vrf.pk %}">{{ vrf.name }}</a></td>
             <td><a href="{% url 'ipam:vrf' pk=vrf.pk %}">{{ vrf.name }}</a></td>

+ 7 - 3
netbox/templates/secrets/secret_bulk_edit.html

@@ -3,11 +3,15 @@
 
 
 {% block title %}Secret Bulk Edit{% endblock %}
 {% block title %}Secret Bulk Edit{% endblock %}
 
 
-{% block select_objects_table %}
+{% block selected_objects_table %}
+    <tr>
+        <th>Device</th>
+        <th>Role</th>
+        <th>Name</th>
+    </tr>
     {% for secret in selected_objects %}
     {% for secret in selected_objects %}
         <tr>
         <tr>
-            <td><a href="{% url 'secrets:secret' pk=secret.pk %}">{{ secret }}</a></td>
-            <td>{{ secret.device }}</td>
+            <td><a href="{% url 'secrets:secret' pk=secret.pk %}">{{ secret.device }}</a></td>
             <td>{{ secret.role }}</td>
             <td>{{ secret.role }}</td>
             <td>{{ secret.name }}</td>
             <td>{{ secret.name }}</td>
         </tr>
         </tr>

+ 5 - 1
netbox/templates/tenancy/tenant_bulk_edit.html

@@ -3,7 +3,11 @@
 
 
 {% block title %}Tenant Bulk Edit{% endblock %}
 {% block title %}Tenant Bulk Edit{% endblock %}
 
 
-{% block select_objects_table %}
+{% block selected_objects_table %}
+    <tr>
+        <th>Tenant</th>
+        <th>Group</th>
+    </tr>
     {% for tenant in selected_objects %}
     {% for tenant in selected_objects %}
         <tr>
         <tr>
             <td><a href="{% url 'tenancy:tenant' slug=tenant.slug %}">{{ tenant }}</a></td>
             <td><a href="{% url 'tenancy:tenant' slug=tenant.slug %}">{{ tenant }}</a></td>

+ 2 - 2
netbox/templates/utilities/bulk_edit_form.html

@@ -11,9 +11,9 @@
     <div class="row">
     <div class="row">
         <div class="col-md-7">
         <div class="col-md-7">
             <div class="panel panel-default">
             <div class="panel panel-default">
-                <div class="panel-heading"><strong>{% block selected_objects_title %}Selected For Editing{% endblock %}</strong></div>
+                <div class="panel-heading"><strong>{% block selected_objects_title %}{{ selected_objects|length }} Selected For Editing{% endblock %}</strong></div>
                 <table class="panel-body table table-hover">
                 <table class="panel-body table table-hover">
-                    {% block select_objects_table %}{% endblock %}
+                    {% block selected_objects_table %}{% endblock %}
                 </table>
                 </table>
             </div>
             </div>
         </div>
         </div>

+ 13 - 4
netbox/templates/utilities/obj_table.html

@@ -5,17 +5,26 @@
         {% csrf_token %}
         {% csrf_token %}
         <input type="hidden" name="redirect_url" value="{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" />
         <input type="hidden" name="redirect_url" value="{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" />
         <input type="hidden" name="pk_all" value="{% for row in table.rows %}{{ row.record.pk|default:'' }}{% if not forloop.last %},{% endif %}{% endfor %}" />
         <input type="hidden" name="pk_all" value="{% for row in table.rows %}{{ row.record.pk|default:'' }}{% if not forloop.last %},{% endif %}{% endfor %}" />
+        {% if table.paginator.num_pages > 1 %}
+            <div id="select_all_box" class="hidden alert alert-info">
+                <div class="checkbox-inline">
+                    <label for="select_all">
+                        <input type="checkbox" id="select_all" name="_all" />
+                        Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
+                    </label>
+                </div>
+            </div>
+        {% endif %}
         {% render_table table table_template|default:'table.html' %}
         {% render_table table table_template|default:'table.html' %}
+        {% block extra_actions %}{% endblock %}
         {% if bulk_edit_url and table.model|user_can_change:request.user %}
         {% if bulk_edit_url and table.model|user_can_change:request.user %}
             <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}" class="btn btn-warning btn-sm">
             <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}" class="btn btn-warning btn-sm">
-                <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
-                Edit Selected
+                <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
             </button>
             </button>
         {% endif %}
         {% endif %}
         {% if bulk_delete_url and table.model|user_can_delete:request.user %}
         {% if bulk_delete_url and table.model|user_can_delete:request.user %}
             <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}" class="btn btn-danger btn-sm">
             <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}" class="btn btn-danger btn-sm">
-                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
-                Delete Selected
+                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
             </button>
             </button>
         {% endif %}
         {% endif %}
     </form>
     </form>

+ 18 - 1
netbox/utilities/forms.py

@@ -3,7 +3,9 @@ import itertools
 import re
 import re
 
 
 from django import forms
 from django import forms
+from django.conf import settings
 from django.core.urlresolvers import reverse_lazy
 from django.core.urlresolvers import reverse_lazy
+from django.core.validators import URLValidator
 from django.utils.encoding import force_text
 from django.utils.encoding import force_text
 from django.utils.html import format_html
 from django.utils.html import format_html
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
@@ -90,7 +92,7 @@ class APISelect(SelectWithDisabled):
         super(APISelect, self).__init__(*args, **kwargs)
         super(APISelect, self).__init__(*args, **kwargs)
 
 
         self.attrs['class'] = 'api-select'
         self.attrs['class'] = 'api-select'
-        self.attrs['api-url'] = api_url
+        self.attrs['api-url'] = '/{}{}'.format(settings.BASE_PATH, api_url.lstrip('/'))  # Inject BASE_PATH
         if display_field:
         if display_field:
             self.attrs['display-field'] = display_field
             self.attrs['display-field'] = display_field
         if disabled_indicator:
         if disabled_indicator:
@@ -253,6 +255,21 @@ class FilterChoiceField(forms.ModelMultipleChoiceField):
     choices = property(_get_choices, forms.ChoiceField._set_choices)
     choices = property(_get_choices, forms.ChoiceField._set_choices)
 
 
 
 
+class LaxURLField(forms.URLField):
+    """
+    Custom URLField which allows any valid URL scheme
+    """
+
+    class AnyURLScheme(object):
+        # A fake URL list which "contains" all scheme names abiding by the syntax defined in RFC 3986 section 3.1
+        def __contains__(self, item):
+            if not item or not re.match('^[a-z][0-9a-z+\-.]*$', item.lower()):
+                return False
+            return True
+
+    default_validators = [URLValidator(schemes=AnyURLScheme())]
+
+
 #
 #
 # Forms
 # Forms
 #
 #

+ 1 - 1
netbox/utilities/tables.py

@@ -27,4 +27,4 @@ class ToggleColumn(tables.CheckBoxColumn):
 
 
     @property
     @property
     def header(self):
     def header(self):
-        return mark_safe('<input type="checkbox" name="_all" title="Select all" />')
+        return mark_safe('<input type="checkbox" id="toggle_all" title="Toggle all" />')