Merge pull request #587 from digitalocean/develop

Release v1.6.2
This commit is contained in:
Jeremy Stretch 2016-09-30 11:24:16 -04:00 committed by GitHub
commit 7336fdf162
43 changed files with 465 additions and 239 deletions

View File

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

View File

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

View File

@ -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
``` ```
# sudo apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev # apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
```
**CentOS/RHEL**
```
# 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

View File

@ -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
``` ```
# sudo apt-get install -y postgresql libpq-dev python-psycopg2 # apt-get install -y postgresql libpq-dev python-psycopg2
``` ```
# Configuration **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:
```
# systemctl start postgresql
```
# 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.

View File

@ -2,8 +2,11 @@
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,7 +41,7 @@ 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/
@ -50,7 +53,6 @@ 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

View File

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

View File

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

View File

@ -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.")

View File

@ -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=( def test_get_list(self, endpoint=('/{}api/dcim/related-connections/?peer-device=test1-edge1&peer-interface=xe-0/0/3'
'/api/dcim/related-connections/' .format(settings.BASE_PATH))):
'?peer-device=test1-edge1&peer-interface=xe-0/0/3')):
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)

View File

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

View File

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

View File

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

View File

@ -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)) try:
return self.choices.get(pk=int(serialized_value)) 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)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,18 @@
$(document).ready(function() { $(document).ready(function() {
// "Select all" checkbox in a table header // "Toggle all" checkbox in a table header
$('th input:checkbox[name=_all]').click(function (event) { $('#toggle_all').click(function (event) {
$(this).parents('table').find('td input:checkbox').prop('checked', $(this).prop('checked')); $('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);
} }
}); });

View File

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

View File

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

View File

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

View File

@ -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.port_speed_human }}</td>
<td>{{ circuit.commit_rate }}</td> <td>{{ circuit.commit_rate_human }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View File

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

View File

@ -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"> {% if interfaces or mgmt_interfaces %}
<a href="{% url 'dcim:ipaddress_assign' pk=device.pk %}" class="btn btn-xs btn-primary"> <div class="panel-footer text-right">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <a href="{% url 'dcim:ipaddress_assign' pk=device.pk %}" class="btn btn-xs btn-primary">
Assign IP address <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign IP address
</a> </a>
</div> </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> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interface
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> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
Add console
</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> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
Add power
</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"> {% if device_bays and perms.dcim.delete_devicebay %}
<div class="col-md-6"> <button type="submit" class="btn btn-danger btn-xs">
{% if device_bays and perms.dcim.delete_devicebay %} <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
<button type="submit" class="btn btn-xs btn-danger"> </button>
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected {% endif %}
</button> {% if perms.dcim.add_devicebay %}
{% endif %} <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 class="col-md-6 text-right"> <div class="clearfix"></div>
{% if perms.dcim.add_devicebay %} {% endif %}
<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 %}
</div>
</div>
</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"> {% if interfaces and perms.dcim.delete_interface %}
<div class="col-md-6"> <button type="submit" class="btn btn-danger btn-xs">
{% if interfaces and perms.dcim.delete_interface %} <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
<button type="submit" class="btn btn-xs btn-danger"> </button>
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected {% endif %}
</button> {% if perms.dcim.add_interface %}
{% endif %} <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 class="col-md-6 text-right"> <div class="clearfix"></div>
{% if perms.dcim.add_interface %} {% endif %}
<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 %}
</div>
</div>
</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"> {% if cs_ports and perms.dcim.delete_consoleserverport %}
<div class="col-md-6"> <button type="submit" class="btn btn-danger btn-xs">
{% if cs_ports and perms.dcim.delete_consoleserverport %} <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
<button type="submit" class="btn btn-xs btn-danger"> </button>
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected {% endif %}
</button> {% if perms.dcim.add_consoleserverport %}
{% endif %} <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 class="col-md-6 text-right"> <div class="clearfix"></div>
{% if perms.dcim.add_consoleserverport %} {% endif %}
<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 %}
</div>
</div>
</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"> {% if power_outlets and perms.dcim.delete_poweroutlet %}
<div class="col-md-6"> <button type="submit" class="btn btn-danger btn-xs">
{% if power_outlets and perms.dcim.delete_poweroutlet %} <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
<button type="submit" class="btn btn-xs btn-danger"> </button>
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected {% endif %}
</button> {% if perms.dcim.add_poweroutlet %}
{% endif %} <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 class="col-md-6 text-right"> <div class="clearfix"></div>
{% if perms.dcim.add_poweroutlet %} {% endif %}
<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 %}
</div>
</div>
</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>

View File

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

View File

@ -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><a href="{% url 'dcim:devicetype' pk=devicetype.pk %}">{{ devicetype.model }}</a></td>
<td>{{ devicetype.model }}</td>
<td>{{ devicetype.manufacturer }}</td> <td>{{ devicetype.manufacturer }}</td>
<td>{{ devicetype.u_height }}U</td> <td>{{ devicetype.u_height }}U</td>
</tr> </tr>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>{% if ipaddress.interface %}<i class="glyphicon glyphicon-ok text-success" title="{{ ipaddress.interface.device }} {{ ipaddress.interface }}"></i>{% endif %}</td>
<td>{{ ipaddress.interface }}</td>
<td>{{ ipaddress.description }}</td> <td>{{ ipaddress.description }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -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.get_status_display }}</td>
<td>{{ prefix.status }}</td>
<td>{{ prefix.role }}</td> <td>{{ prefix.role }}</td>
<td>{{ prefix.description }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View File

@ -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><a href="{% url 'ipam:vlan' pk=vlan.pk %}">{{ vlan }}</a></td>
<td>{{ vlan.name }}</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 %}

View File

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

View File

@ -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><a href="{% url 'secrets:secret' pk=secret.pk %}">{{ secret.device }}</a></td>
<td>{{ secret.device }}</td>
<td>{{ secret.role }}</td> <td>{{ secret.role }}</td>
<td>{{ secret.name }}</td> <td>{{ secret.name }}</td>
</tr> </tr>

View File

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

View File

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

View File

@ -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> <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
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> <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
Delete Selected
</button> </button>
{% endif %} {% endif %}
</form> </form>

View File

@ -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
# #

View File

@ -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" />')