mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-06 08:59:32 +01:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c171547037 | ||
|
|
493b7d594d | ||
|
|
4d40c015e4 | ||
|
|
4405bc4182 | ||
|
|
54a0639a6e | ||
|
|
334b286ebf | ||
|
|
c09cb5df3d | ||
|
|
0da3661ff0 | ||
|
|
5a4ccbc066 | ||
|
|
49cbdc22da | ||
|
|
579ed0a985 | ||
|
|
464797858f | ||
|
|
0ff46bf5d0 | ||
|
|
330abe5a2d | ||
|
|
73945899fe | ||
|
|
8227a9ff9c | ||
|
|
f1c70cd896 | ||
|
|
7055292803 | ||
|
|
3503c77699 | ||
|
|
b68c64041e | ||
|
|
36066068d4 | ||
|
|
8ed174e7af | ||
|
|
7336fdf162 | ||
|
|
b5a7dd7d6d | ||
|
|
35918ae966 | ||
|
|
ce01bb59a3 | ||
|
|
18a5a966e3 | ||
|
|
833499ffe8 | ||
|
|
5b7f350ded | ||
|
|
d5fc0e9ce7 | ||
|
|
c6592faeb2 | ||
|
|
dec00cdb55 | ||
|
|
30c7c2d359 | ||
|
|
118bb5ea73 | ||
|
|
35b3d8e33a | ||
|
|
187a6dee17 | ||
|
|
0900a6bf49 | ||
|
|
6cba2e92f2 | ||
|
|
796b131f73 | ||
|
|
bdb8d62cef | ||
|
|
d049c1c244 | ||
|
|
45432a6f29 | ||
|
|
a803bd8033 | ||
|
|
0001bbc966 | ||
|
|
1ebba3ee26 | ||
|
|
fde24258e3 | ||
|
|
59c6d5b1ec | ||
|
|
33694030b7 | ||
|
|
f8f973dac2 | ||
|
|
bffabef556 | ||
|
|
325d96dabb | ||
|
|
b7b1682f42 | ||
|
|
aa2612aeba | ||
|
|
af519b93b7 | ||
|
|
2213e3e0cf |
@@ -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/).
|
||||
|
||||
Questions? Comments? Please 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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Default: False
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
# Installation
|
||||
|
||||
NetBox requires following system dependencies:
|
||||
|
||||
* python2.7
|
||||
* python-dev
|
||||
* python-pip
|
||||
* libxml2-dev
|
||||
* libxslt1-dev
|
||||
* libffi-dev
|
||||
* graphviz
|
||||
* libpq-dev
|
||||
* libssl-dev
|
||||
**Debian/Ubuntu**
|
||||
|
||||
```
|
||||
# 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.
|
||||
@@ -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:
|
||||
|
||||
**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:
|
||||
@@ -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.)
|
||||
|
||||
```
|
||||
# sudo pip install -r requirements.txt
|
||||
# pip install -r requirements.txt
|
||||
```
|
||||
|
||||
# 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:
|
||||
|
||||
|
||||
* ALLOWED_HOSTS
|
||||
* DATABASE
|
||||
* SECRET_KEY
|
||||
@@ -143,8 +148,8 @@ NetBox does not come with any predefined user accounts. You'll need to create a
|
||||
# ./manage.py createsuperuser
|
||||
Username: admin
|
||||
Email address: admin@example.com
|
||||
Password:
|
||||
Password (again):
|
||||
Password:
|
||||
Password (again):
|
||||
Superuser created successfully.
|
||||
```
|
||||
|
||||
|
||||
@@ -2,17 +2,33 @@ NetBox requires a PostgreSQL database to store data. MySQL is not supported, as
|
||||
|
||||
# Installation
|
||||
|
||||
The following packages are needed to install PostgreSQL with Python support:
|
||||
|
||||
* postgresql
|
||||
* libpq-dev
|
||||
* python-psycopg2
|
||||
**Debian/Ubuntu**
|
||||
|
||||
```
|
||||
# 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.
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# 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
|
||||
@@ -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.
|
||||
|
||||
```
|
||||
# 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 {
|
||||
@@ -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/
|
||||
# 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.
|
||||
|
||||
```
|
||||
# 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).
|
||||
@@ -58,7 +60,7 @@ To enable SSL, consider this guide on [securing nginx with Let's Encrypt](https:
|
||||
## 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):
|
||||
@@ -99,7 +101,7 @@ To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https
|
||||
|
||||
# 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'
|
||||
@@ -120,7 +122,7 @@ directory = /opt/netbox/netbox/
|
||||
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
|
||||
|
||||
@@ -3,7 +3,6 @@ from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.forms import bulkedit_tenant_choices
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, Livesearch, SmallTextarea,
|
||||
@@ -57,6 +56,9 @@ class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
admin_contact = forms.CharField(required=False, widget=SmallTextarea, label='Admin contact')
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
||||
|
||||
|
||||
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Provider
|
||||
@@ -86,7 +88,7 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
||||
attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||
attrs={'filter-for': 'interface'}))
|
||||
display_field='display_name', attrs={'filter-for': 'interface'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||
)
|
||||
@@ -178,11 +180,14 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
|
||||
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
|
||||
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['tenant', 'port_speed', 'commit_rate', 'comments']
|
||||
|
||||
|
||||
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Circuit
|
||||
|
||||
@@ -5,6 +5,7 @@ from dcim.models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
|
||||
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
|
||||
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 tenancy.api.serializers import TenantNestedSerializer
|
||||
@@ -131,11 +132,19 @@ class ManufacturerNestedSerializer(ManufacturerSerializer):
|
||||
|
||||
class DeviceTypeSerializer(serializers.ModelSerializer):
|
||||
manufacturer = ManufacturerNestedSerializer()
|
||||
subdevice_role = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
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):
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Count, Q
|
||||
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.forms import bulkedit_tenant_choices
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, add_blank_choice, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
|
||||
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
||||
APISelect, add_blank_choice, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, CSVDataField,
|
||||
ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea,
|
||||
SlugField,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||
Interface, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
|
||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackRole,
|
||||
Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
||||
Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
|
||||
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES,
|
||||
Rack, RackGroup, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
||||
)
|
||||
|
||||
|
||||
@@ -42,37 +43,12 @@ def get_device_by_name_or_pk(name):
|
||||
return device
|
||||
|
||||
|
||||
def bulkedit_platform_choices():
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'None'),
|
||||
]
|
||||
choices += [(p.pk, p.name) for p in Platform.objects.all()]
|
||||
return choices
|
||||
|
||||
|
||||
def bulkedit_rackgroup_choices():
|
||||
def validate_connection_status(value):
|
||||
"""
|
||||
Include an option to remove the currently assigned group from a rack.
|
||||
Custom validator for connection statuses. value must be either "planned" or "connected" (case-insensitive).
|
||||
"""
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'None'),
|
||||
]
|
||||
choices += [(r.pk, r) for r in RackGroup.objects.all()]
|
||||
return choices
|
||||
|
||||
|
||||
def bulkedit_rackrole_choices():
|
||||
"""
|
||||
Include an option to remove the currently assigned role from a rack.
|
||||
"""
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'None'),
|
||||
]
|
||||
choices += [(r.pk, r.name) for r in RackRole.objects.all()]
|
||||
return choices
|
||||
if value.lower() not in ['planned', 'connected']:
|
||||
raise ValidationError('Invalid connection status ({}); must be either "planned" or "connected".'.format(value))
|
||||
|
||||
|
||||
#
|
||||
@@ -114,7 +90,10 @@ class SiteImportForm(BulkImportForm, BootstrapMixin):
|
||||
|
||||
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['tenant']
|
||||
|
||||
|
||||
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
@@ -234,14 +213,17 @@ class RackImportForm(BulkImportForm, BootstrapMixin):
|
||||
class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site')
|
||||
group = forms.TypedChoiceField(choices=bulkedit_rackgroup_choices, coerce=int, required=False, label='Group')
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
role = forms.TypedChoiceField(choices=bulkedit_rackrole_choices, coerce=int, required=False, label='Role')
|
||||
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
role = forms.ModelChoiceField(queryset=RackRole.objects.all(), required=False)
|
||||
type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
|
||||
width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
|
||||
u_height = forms.IntegerField(required=False, label='Height (U)')
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['group', 'tenant', 'role', 'comments']
|
||||
|
||||
|
||||
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Rack
|
||||
@@ -279,7 +261,7 @@ class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
|
||||
'is_pdu', 'is_network_device', 'subdevice_role']
|
||||
|
||||
|
||||
class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
|
||||
class DeviceTypeBulkEditForm(BulkEditForm, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
|
||||
u_height = forms.IntegerField(min_value=1, required=False)
|
||||
@@ -334,6 +316,14 @@ class InterfaceTemplateForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['name_pattern', 'form_factor', 'mgmt_only']
|
||||
|
||||
|
||||
class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=InterfaceTemplate.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class DeviceBayTemplateForm(forms.ModelForm, BootstrapMixin):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
@@ -583,17 +573,19 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
|
||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
platform = forms.TypedChoiceField(choices=bulkedit_platform_choices, coerce=int, required=False,
|
||||
label='Platform')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
|
||||
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['tenant', 'platform']
|
||||
|
||||
|
||||
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Device
|
||||
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')
|
||||
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',
|
||||
@@ -631,7 +623,7 @@ class ConsoleConnectionCSVForm(forms.Form):
|
||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found'})
|
||||
console_port = forms.CharField()
|
||||
status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
|
||||
status = forms.CharField(validators=[validate_connection_status])
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -695,6 +687,7 @@ class ConsolePortConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
widget=forms.Select(attrs={'filter-for': 'console_server'}))
|
||||
console_server = forms.ModelChoiceField(queryset=Device.objects.all(), label='Console Server', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_console_server=True',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'cs_port'}))
|
||||
livesearch = forms.CharField(required=False, label='Console Server', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='console_server')
|
||||
@@ -762,7 +755,7 @@ class ConsoleServerPortConnectionForm(forms.Form, BootstrapMixin):
|
||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||
attrs={'filter-for': 'port'}))
|
||||
display_field='display_name', attrs={'filter-for': 'port'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||
)
|
||||
@@ -826,7 +819,7 @@ class PowerConnectionCSVForm(forms.Form):
|
||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found'})
|
||||
power_port = forms.CharField()
|
||||
status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
|
||||
status = forms.CharField(validators=[validate_connection_status])
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -891,7 +884,7 @@ class PowerPortConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
widget=forms.Select(attrs={'filter-for': 'pdu'}))
|
||||
pdu = forms.ModelChoiceField(queryset=Device.objects.all(), label='PDU', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_pdu=True',
|
||||
attrs={'filter-for': 'power_outlet'}))
|
||||
display_field='display_name', attrs={'filter-for': 'power_outlet'}))
|
||||
livesearch = forms.CharField(required=False, label='PDU', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='pdu')
|
||||
)
|
||||
@@ -958,7 +951,7 @@ class PowerOutletConnectionForm(forms.Form, BootstrapMixin):
|
||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||
attrs={'filter-for': 'port'}))
|
||||
display_field='display_name', attrs={'filter-for': 'port'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||
)
|
||||
@@ -1023,6 +1016,15 @@ class InterfaceBulkCreateForm(InterfaceCreateForm, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
#
|
||||
# Interface connections
|
||||
#
|
||||
@@ -1033,6 +1035,7 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
widget=forms.Select(attrs={'filter-for': 'device_b'}))
|
||||
device_b = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack_b}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'interface_b'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device_b')
|
||||
@@ -1087,7 +1090,7 @@ class InterfaceConnectionCSVForm(forms.Form):
|
||||
device_b = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device B not found.'})
|
||||
interface_b = forms.CharField()
|
||||
status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
|
||||
status = forms.CharField(validators=[validate_connection_status])
|
||||
|
||||
def clean(self):
|
||||
|
||||
|
||||
@@ -576,11 +576,29 @@ class DeviceType(models.Model):
|
||||
def __unicode__(self):
|
||||
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):
|
||||
return reverse('dcim:devicetype', args=[self.pk])
|
||||
|
||||
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():
|
||||
raise ValidationError("Must delete all console server port templates associated with this device before "
|
||||
"declassifying it as a console server.")
|
||||
|
||||
@@ -2,6 +2,8 @@ import json
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class SiteTest(APITestCase):
|
||||
|
||||
@@ -57,7 +59,7 @@ class SiteTest(APITestCase):
|
||||
'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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -67,7 +69,7 @@ class SiteTest(APITestCase):
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -76,7 +78,7 @@ class SiteTest(APITestCase):
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -91,7 +93,7 @@ class SiteTest(APITestCase):
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -149,7 +151,7 @@ class RackTest(APITestCase):
|
||||
'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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -163,7 +165,7 @@ class RackTest(APITestCase):
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -192,7 +194,7 @@ class ManufacturersTest(APITestCase):
|
||||
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -202,7 +204,7 @@ class ManufacturersTest(APITestCase):
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -227,6 +229,7 @@ class DeviceTypeTest(APITestCase):
|
||||
'is_console_server',
|
||||
'is_pdu',
|
||||
'is_network_device',
|
||||
'subdevice_role',
|
||||
]
|
||||
|
||||
nested_fields = [
|
||||
@@ -236,7 +239,7 @@ class DeviceTypeTest(APITestCase):
|
||||
'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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -246,7 +249,7 @@ class DeviceTypeTest(APITestCase):
|
||||
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.
|
||||
# response = self.client.get(endpoint)
|
||||
# content = json.loads(response.content)
|
||||
@@ -270,7 +273,7 @@ class DeviceRolesTest(APITestCase):
|
||||
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -280,7 +283,7 @@ class DeviceRolesTest(APITestCase):
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -298,7 +301,7 @@ class PlatformsTest(APITestCase):
|
||||
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -308,7 +311,7 @@ class PlatformsTest(APITestCase):
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -346,7 +349,7 @@ class DeviceTest(APITestCase):
|
||||
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -373,7 +376,7 @@ class DeviceTest(APITestCase):
|
||||
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 = [
|
||||
'asset_tag',
|
||||
@@ -421,7 +424,7 @@ class DeviceTest(APITestCase):
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -439,7 +442,7 @@ class ConsoleServerPortsTest(APITestCase):
|
||||
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -461,7 +464,7 @@ class ConsolePortsTest(APITestCase):
|
||||
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -479,7 +482,7 @@ class ConsolePortsTest(APITestCase):
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -500,7 +503,7 @@ class PowerPortsTest(APITestCase):
|
||||
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -514,7 +517,7 @@ class PowerPortsTest(APITestCase):
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -535,7 +538,7 @@ class PowerOutletsTest(APITestCase):
|
||||
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -585,7 +588,7 @@ class InterfaceTest(APITestCase):
|
||||
'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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -599,7 +602,7 @@ class InterfaceTest(APITestCase):
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -612,7 +615,7 @@ class InterfaceTest(APITestCase):
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -622,7 +625,8 @@ class InterfaceTest(APITestCase):
|
||||
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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -643,9 +647,8 @@ class RelatedConnectionsTest(APITestCase):
|
||||
'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)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
@@ -3,10 +3,6 @@ from django.conf.urls import url
|
||||
from secrets.views import secret_add
|
||||
|
||||
from . import views
|
||||
from .models import (
|
||||
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, PowerPortTemplate, PowerOutletTemplate,
|
||||
InterfaceTemplate,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
@@ -75,6 +71,7 @@ urlpatterns = [
|
||||
|
||||
# Interface templates
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(), name='devicetype_add_interface'),
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'),
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
|
||||
|
||||
# Device bay templates
|
||||
@@ -159,6 +156,7 @@ urlpatterns = [
|
||||
# Interfaces
|
||||
url(r'^devices/interfaces/add/$', views.InterfaceBulkAddView.as_view(), name='interface_add_multi'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.interface_add, name='interface_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
|
||||
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
|
||||
|
||||
@@ -457,6 +457,14 @@ class InterfaceTemplateAddView(ComponentTemplateCreateView):
|
||||
form = forms.InterfaceTemplateForm
|
||||
|
||||
|
||||
class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_interfacetemplate'
|
||||
cls = InterfaceTemplate
|
||||
parent_cls = DeviceType
|
||||
form = forms.InterfaceTemplateBulkEditForm
|
||||
template_name = 'dcim/interfacetemplate_bulk_edit.html'
|
||||
|
||||
|
||||
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_interfacetemplate'
|
||||
cls = InterfaceTemplate
|
||||
@@ -1425,6 +1433,14 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
|
||||
len(selected_devices)))
|
||||
|
||||
|
||||
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_interface'
|
||||
cls = Interface
|
||||
parent_cls = Device
|
||||
form = forms.InterfaceBulkEditForm
|
||||
template_name = 'dcim/interface_bulk_edit.html'
|
||||
|
||||
|
||||
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_interface'
|
||||
cls = Interface
|
||||
|
||||
@@ -40,7 +40,7 @@ class GraphAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(ExportTemplate)
|
||||
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)
|
||||
|
||||
@@ -3,6 +3,7 @@ from collections import OrderedDict
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from utilities.forms import BulkEditForm, LaxURLField
|
||||
from .models import (
|
||||
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue
|
||||
)
|
||||
@@ -48,15 +49,13 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
||||
# Select
|
||||
elif cf.type == CF_TYPE_SELECT:
|
||||
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
|
||||
if not cf.required:
|
||||
choices = [(0, 'None')] + choices
|
||||
if bulk_edit or filterable_only:
|
||||
choices = [(None, '---------')] + choices
|
||||
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)
|
||||
|
||||
# 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
|
||||
else:
|
||||
@@ -72,10 +71,10 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
||||
|
||||
|
||||
class CustomFieldForm(forms.ModelForm):
|
||||
custom_fields = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
self.custom_fields = []
|
||||
self.obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||
|
||||
super(CustomFieldForm, self).__init__(*args, **kwargs)
|
||||
@@ -92,7 +91,7 @@ class CustomFieldForm(forms.ModelForm):
|
||||
existing_values = CustomFieldValue.objects.filter(obj_type=self.obj_type, obj_id=self.instance.pk)\
|
||||
.select_related('field')
|
||||
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):
|
||||
|
||||
@@ -125,22 +124,25 @@ class CustomFieldForm(forms.ModelForm):
|
||||
return obj
|
||||
|
||||
|
||||
class CustomFieldBulkEditForm(forms.Form):
|
||||
custom_fields = []
|
||||
|
||||
def __init__(self, model, *args, **kwargs):
|
||||
|
||||
self.obj_type = ContentType.objects.get_for_model(model)
|
||||
class CustomFieldBulkEditForm(BulkEditForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CustomFieldBulkEditForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.custom_fields = []
|
||||
self.obj_type = ContentType.objects.get_for_model(self.model)
|
||||
|
||||
# Add all applicable CustomFields to the form
|
||||
custom_fields = []
|
||||
for name, field in get_custom_fields_for_model(self.obj_type, bulk_edit=True).items():
|
||||
custom_fields = get_custom_fields_for_model(self.obj_type, bulk_edit=True).items()
|
||||
for name, field in custom_fields:
|
||||
# Annotate non-required custom fields as nullable
|
||||
if not field.required:
|
||||
self.nullable_fields.append(name)
|
||||
field.required = False
|
||||
self.fields[name] = field
|
||||
custom_fields.append(name)
|
||||
self.custom_fields = custom_fields
|
||||
# Annotate this as a custom field
|
||||
self.custom_fields.append(name)
|
||||
print(self.nullable_fields)
|
||||
|
||||
|
||||
class CustomFieldFilterForm(forms.Form):
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -71,9 +71,9 @@ class CustomFieldModel(object):
|
||||
"""
|
||||
Name-based CustomFieldValue accessor for use in templates
|
||||
"""
|
||||
if not hasattr(self, 'custom_fields'):
|
||||
if not hasattr(self, 'get_custom_fields'):
|
||||
return dict()
|
||||
return {field.name: value for field, value in self.custom_fields.items()}
|
||||
return {field.name: value for field, value in self.get_custom_fields().items()}
|
||||
|
||||
def get_custom_fields(self):
|
||||
"""
|
||||
@@ -146,8 +146,10 @@ class CustomField(models.Model):
|
||||
# Read date as YYYY-MM-DD
|
||||
return date(*[int(n) for n in serialized_value.split('-')])
|
||||
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
|
||||
|
||||
|
||||
@@ -198,6 +200,12 @@ class CustomFieldChoice(models.Model):
|
||||
if self.field.type != CF_TYPE_SELECT:
|
||||
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):
|
||||
type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
|
||||
@@ -225,7 +233,8 @@ class Graph(models.Model):
|
||||
|
||||
class ExportTemplate(models.Model):
|
||||
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()
|
||||
mime_type = models.CharField(max_length=15, blank=True)
|
||||
file_extension = models.CharField(max_length=15, blank=True)
|
||||
|
||||
@@ -3,7 +3,6 @@ from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Device, Interface
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.forms import bulkedit_tenant_choices
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, CSVDataField, BulkImportForm, FilterChoiceField, Livesearch, SlugField,
|
||||
@@ -23,18 +22,6 @@ IP_FAMILY_CHOICES = [
|
||||
]
|
||||
|
||||
|
||||
def bulkedit_vrf_choices():
|
||||
"""
|
||||
Include an option to assign the object to the global table.
|
||||
"""
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'Global'),
|
||||
]
|
||||
choices += [(v.pk, v.name) for v in VRF.objects.all()]
|
||||
return choices
|
||||
|
||||
|
||||
#
|
||||
# VRFs
|
||||
#
|
||||
@@ -67,9 +54,12 @@ class VRFImportForm(BulkImportForm, BootstrapMixin):
|
||||
|
||||
class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['tenant', 'description']
|
||||
|
||||
|
||||
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = VRF
|
||||
@@ -124,6 +114,9 @@ class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
date_added = forms.DateField(required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['date_added', 'description']
|
||||
|
||||
|
||||
class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Aggregate
|
||||
@@ -253,12 +246,15 @@ class PrefixImportForm(BulkImportForm, BootstrapMixin):
|
||||
class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||
vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False)
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['site', 'vrf', 'tenant', 'role', 'description']
|
||||
|
||||
|
||||
def prefix_status_choices():
|
||||
status_counts = {}
|
||||
@@ -294,6 +290,7 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
widget=forms.Select(attrs={'filter-for': 'nat_device'}))
|
||||
nat_device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
|
||||
widget=APISelect(api_url='/api/dcim/devices/?site_id={{nat_site}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'nat_inside'}))
|
||||
livesearch = forms.CharField(required=False, label='IP Address', widget=Livesearch(
|
||||
query_key='q', query_url='ipam-api:ipaddress_list', field_to_update='nat_inside', obj_label='address')
|
||||
@@ -407,10 +404,13 @@ class IPAddressImportForm(BulkImportForm, BootstrapMixin):
|
||||
|
||||
class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['vrf', 'tenant', 'description']
|
||||
|
||||
|
||||
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = IPAddress
|
||||
@@ -509,11 +509,14 @@ class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['group', 'tenant', 'role', 'description']
|
||||
|
||||
|
||||
def vlan_status_choices():
|
||||
status_counts = {}
|
||||
|
||||
@@ -139,7 +139,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
||||
if self.pk:
|
||||
covered_aggregates = covered_aggregates.exclude(pk=self.pk)
|
||||
if covered_aggregates:
|
||||
raise ValidationError("{} is overlaps with an existing aggregate ({})"
|
||||
raise ValidationError("{} overlaps with an existing aggregate ({})"
|
||||
.format(self.prefix, covered_aggregates[0]))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@@ -9,7 +9,7 @@ import os
|
||||
# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
|
||||
#
|
||||
# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
|
||||
ALLOWED_HOSTS = [os.environ.get('ALLOWED_HOSTS', '')]
|
||||
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(' ')
|
||||
|
||||
# PostgreSQL database configuration.
|
||||
DATABASE = {
|
||||
@@ -52,6 +52,10 @@ EMAIL = {
|
||||
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
|
||||
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.
|
||||
MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', False)
|
||||
|
||||
|
||||
@@ -52,6 +52,10 @@ EMAIL = {
|
||||
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
|
||||
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.
|
||||
MAINTENANCE_MODE = False
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ except ImportError:
|
||||
"the documentation.")
|
||||
|
||||
|
||||
VERSION = '1.6.1'
|
||||
VERSION = '1.6.3'
|
||||
|
||||
# Import local configuration
|
||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
@@ -27,6 +27,9 @@ ADMINS = getattr(configuration, 'ADMINS', [])
|
||||
DEBUG = getattr(configuration, 'DEBUG', False)
|
||||
EMAIL = getattr(configuration, 'EMAIL', {})
|
||||
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)
|
||||
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
|
||||
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')
|
||||
@@ -159,7 +162,7 @@ USE_TZ = True
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.8/howto/static-files/
|
||||
STATIC_ROOT = BASE_DIR + '/static/'
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_URL = '/{}static/'.format(BASE_PATH)
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, "project-static"),
|
||||
)
|
||||
@@ -173,8 +176,7 @@ MESSAGE_TAGS = {
|
||||
}
|
||||
|
||||
# Authentication URLs
|
||||
LOGIN_URL = '/login/'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
LOGIN_URL = '/{}login/'.format(BASE_PATH)
|
||||
|
||||
# Secrets
|
||||
SECRETS_MIN_PUBKEY_SIZE = 2048
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
from django.views.defaults import page_not_found
|
||||
@@ -8,7 +9,7 @@ from users.views import login, logout
|
||||
|
||||
handler500 = handle_500
|
||||
|
||||
urlpatterns = [
|
||||
_patterns = [
|
||||
|
||||
# Default page
|
||||
url(r'^$', home, name='home'),
|
||||
@@ -42,3 +43,8 @@ urlpatterns = [
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
|
||||
]
|
||||
|
||||
# Prepend BASE_PATH
|
||||
urlpatterns = [
|
||||
url(r'^{}'.format(settings.BASE_PATH), include(_patterns))
|
||||
]
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
$(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 (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) {
|
||||
if (!$(this).attr('checked')) {
|
||||
$(this).parents('table').find('input:checkbox[name=_all]').prop('checked', false);
|
||||
$('#select_all, #toggle_all').prop('checked', false);
|
||||
}
|
||||
});
|
||||
|
||||
// Simple "Toggle all" button (panel)
|
||||
$('button.toggle').click(function (event) {
|
||||
var selected = $(this).attr('selected');
|
||||
$(this).closest('form').find('input:checkbox[name=pk]').prop('checked', !selected);
|
||||
$(this).attr('selected', !selected);
|
||||
$(this).children('span').toggleClass('glyphicon-unchecked glyphicon-check');
|
||||
return false;
|
||||
});
|
||||
|
||||
// Slugify
|
||||
function slugify(s, num_chars) {
|
||||
s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars
|
||||
@@ -32,6 +46,11 @@ $(document).ready(function() {
|
||||
})
|
||||
}
|
||||
|
||||
// Bulk edit nullification
|
||||
$('input:checkbox[name=_nullify]').click(function (event) {
|
||||
$('#id_' + this.value).toggle('disabled');
|
||||
});
|
||||
|
||||
// API select widget
|
||||
$('select[filter-for]').change(function () {
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Device
|
||||
from utilities.forms import BootstrapMixin, BulkImportForm, CSVDataField, FilterChoiceField, SlugField
|
||||
from utilities.forms import BootstrapMixin, BulkEditForm, BulkImportForm, CSVDataField, FilterChoiceField, SlugField
|
||||
|
||||
from .models import Secret, SecretRole, UserKey
|
||||
|
||||
@@ -89,11 +89,14 @@ class SecretImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-private-key'}))
|
||||
|
||||
|
||||
class SecretBulkEditForm(forms.Form, BootstrapMixin):
|
||||
class SecretBulkEditForm(BulkEditForm, BootstrapMixin):
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['name']
|
||||
|
||||
|
||||
class SecretFilterForm(forms.Form, BootstrapMixin):
|
||||
role = FilterChoiceField(queryset=SecretRole.objects.annotate(filter_count=Count('secrets')), to_field_name='slug')
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<pre><strong>{{ exception }}</strong><br />
|
||||
{{ error }}</pre>
|
||||
<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>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<link rel="stylesheet" href="{% static 'jquery-ui-1.11.4/jquery-ui.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/base.css' %}">
|
||||
<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>
|
||||
<body>
|
||||
<nav class="navbar navbar-default navbar-fixed-top">
|
||||
@@ -20,7 +21,7 @@
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/">
|
||||
<a class="navbar-brand" href="{% url 'home' %}">
|
||||
<img src="{% static 'img/netbox_logo.png' %}" />
|
||||
</a>
|
||||
</div>
|
||||
@@ -288,7 +289,7 @@
|
||||
<div class="col-xs-4 text-right">
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/" target="_blank">Docs</a> ·
|
||||
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="/api/docs/">API</a> ·
|
||||
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'django.swagger.base.view' %}">API</a> ·
|
||||
<i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -3,14 +3,21 @@
|
||||
|
||||
{% 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 %}
|
||||
<tr>
|
||||
<td><a href="{% url 'circuits:circuit' pk=circuit.pk %}">{{ circuit }}</a></td>
|
||||
<td>{{ circuit.type }}</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>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
|
||||
{% 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 %}
|
||||
<tr>
|
||||
<td><a href="{% url 'circuits:provider' slug=provider.slug %}">{{ provider }}</a></td>
|
||||
|
||||
@@ -186,18 +186,23 @@
|
||||
{% include 'dcim/inc/_ipaddress.html' %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
{% elif interfaces or mgmt_interfaces %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
@@ -210,7 +215,7 @@
|
||||
{% empty %}
|
||||
<tr>
|
||||
<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 %}
|
||||
<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 %}
|
||||
@@ -222,7 +227,7 @@
|
||||
{% empty %}
|
||||
<tr>
|
||||
<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 %}
|
||||
<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 %}
|
||||
@@ -235,7 +240,7 @@
|
||||
{% if not device.device_type.is_pdu %}
|
||||
<tr>
|
||||
<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 %}
|
||||
<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 %}
|
||||
@@ -248,20 +253,17 @@
|
||||
<div class="panel-footer text-right">
|
||||
{% if perms.dcim.add_interface %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% 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">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add power
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -312,6 +314,16 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Device Bays</strong>
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% if perms.dcim.add_devicebay and device_bays|length > 10 %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
{% for devicebay in device_bays %}
|
||||
@@ -324,23 +336,19 @@
|
||||
</table>
|
||||
{% if perms.dcim.add_devicebay or perms.dcim.delete_devicebay %}
|
||||
<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 %}
|
||||
{% 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 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 %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -350,12 +358,22 @@
|
||||
{% endif %}
|
||||
{% if interfaces or device.device_type.is_network_device %}
|
||||
{% if perms.dcim.delete_interface %}
|
||||
<form method="post" action="{% url 'dcim:interface_bulk_delete' pk=device.pk %}">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interfaces</strong>
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% if perms.dcim.add_interface and interfaces|length > 10 %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
{% for iface in interfaces %}
|
||||
@@ -368,23 +386,24 @@
|
||||
</table>
|
||||
{% if perms.dcim.add_interface or perms.dcim.delete_interface %}
|
||||
<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 %}
|
||||
{% if interfaces and perms.dcim.change_interface %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' pk=device.pk %}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit selected
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if interfaces and perms.dcim.delete_interface %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}" 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 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 %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -400,6 +419,16 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Console Server Ports</strong>
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% if perms.dcim.add_consoleserverport and cs_ports|length > 10 %}
|
||||
<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>
|
||||
<table class="table table-hover panel-body">
|
||||
{% for csp in cs_ports %}
|
||||
@@ -412,23 +441,19 @@
|
||||
</table>
|
||||
{% if perms.dcim.add_consoleserverport or perms.dcim.delete_consoleserverport %}
|
||||
<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 %}
|
||||
{% 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 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 %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -444,6 +469,16 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Outlets</strong>
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% if perms.dcim.add_poweroutlet and power_outlets|length > 10 %}
|
||||
<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>
|
||||
<table class="table table-hover panel-body">
|
||||
{% for po in power_outlets %}
|
||||
@@ -456,23 +491,19 @@
|
||||
</table>
|
||||
{% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
|
||||
<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 %}
|
||||
{% 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 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 %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -531,13 +562,13 @@ function toggleConnection(elem, api_url) {
|
||||
return false;
|
||||
}
|
||||
$(".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() {
|
||||
return toggleConnection($(this), "/api/dcim/power-ports/");
|
||||
return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/power-ports/");
|
||||
});
|
||||
$(".interface-toggle").click(function() {
|
||||
return toggleConnection($(this), "/api/dcim/interface-connections/");
|
||||
return toggleConnection($(this), "/{{ settings.BASE_PATH }}api/dcim/interface-connections/");
|
||||
});
|
||||
</script>
|
||||
<script src="{% static 'js/graphs.js' %}"></script>
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
|
||||
{% 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 %}
|
||||
<tr>
|
||||
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
|
||||
|
||||
@@ -72,6 +72,10 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Instances</td>
|
||||
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
@@ -143,14 +147,14 @@
|
||||
</div>
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=mgmt_interface_table title='Management Interfaces' add_url='dcim:devicetype_add_interface' add_url_extra='?mgmt_only=1' delete_url='dcim:devicetype_delete_interface' %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=mgmt_interface_table title='Management Interfaces' add_url='dcim:devicetype_add_interface' add_url_extra='?mgmt_only=1' edit_url='dcim:devicetype_bulkedit_interface' delete_url='dcim:devicetype_delete_interface' %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if devicetype.is_parent_device %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicetype_add_devicebay' delete_url='dcim:devicetype_delete_devicebay' %}
|
||||
{% endif %}
|
||||
{% if devicetype.is_network_device %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' delete_url='dcim:devicetype_delete_interface' %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' edit_url='dcim:devicetype_bulkedit_interface' delete_url='dcim:devicetype_delete_interface' %}
|
||||
{% endif %}
|
||||
{% if devicetype.is_console_server %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:devicetype_add_consoleserverport' delete_url='dcim:devicetype_delete_consoleserverport' %}
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
|
||||
{% 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 %}
|
||||
<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.u_height }}U</td>
|
||||
</tr>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -1,23 +1,46 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% if perms.dcim.change_devicetype %}
|
||||
<form method="post" action="{% url delete_url pk=devicetype.pk %}">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-default">
|
||||
<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>
|
||||
<div class="pull-right">
|
||||
{% if table.rows|length > 3 %}
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if table.rows|length > 10 %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% render_table table 'table.html' %}
|
||||
{% if table.rows %}
|
||||
<div class="panel-footer">
|
||||
<button type="submit" class="btn btn-xs btn-danger">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||
</button>
|
||||
<div class="panel-footer">
|
||||
{% if table.rows %}
|
||||
{% if edit_url %}
|
||||
<button type="submit" name="_edit" formaction="{% url edit_url pk=devicetype.pk %}" class="btn btn-xs btn-warning">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if delete_url %}
|
||||
<button type="submit" name="_delete" formaction="{% url delete_url pk=devicetype.pk %}" class="btn btn-xs btn-danger">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<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 class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
|
||||
{% 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 %}
|
||||
<tr>
|
||||
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
|
||||
|
||||
17
netbox/templates/dcim/interface_bulk_edit.html
Normal file
17
netbox/templates/dcim/interface_bulk_edit.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Interface Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Form Factor</th>
|
||||
</tr>
|
||||
{% for iface in selected_objects %}
|
||||
<tr>
|
||||
<td>{{ iface.name }}</td>
|
||||
<td>{{ iface.get_form_factor_display }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
25
netbox/templates/dcim/interfacetemplate_bulk_edit.html
Normal file
25
netbox/templates/dcim/interfacetemplate_bulk_edit.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Interface Template Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Form Factor</th>
|
||||
<th>Management</th>
|
||||
</tr>
|
||||
{% for iface in selected_objects %}
|
||||
<tr>
|
||||
<td>{{ iface.name }}</td>
|
||||
<td>{{ iface.get_form_factor_display }}</td>
|
||||
<td>
|
||||
{% if iface.mgmt_only %}
|
||||
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -3,12 +3,13 @@
|
||||
|
||||
{% block title %}Rack Bulk Edit{% endblock %}
|
||||
|
||||
{% block select_objects_table %}
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Site</th>
|
||||
<th>Group</th>
|
||||
<th>Tenant</th>
|
||||
<th>Role</th>
|
||||
<th>Type</th>
|
||||
<th>Width</th>
|
||||
<th>Height</th>
|
||||
@@ -19,6 +20,7 @@
|
||||
<td>{{ rack.site }}</td>
|
||||
<td>{{ rack.group }}</td>
|
||||
<td>{{ rack.tenant }}</td>
|
||||
<td>{{ rack.role }}</td>
|
||||
<td>{{ rack.get_type_display }}</td>
|
||||
<td>{{ rack.get_width_display }}</td>
|
||||
<td>{{ rack.u_height }}U</td>
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
|
||||
{% 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 %}
|
||||
<tr>
|
||||
<td><a href="{% url 'dcim:site' slug=site.slug %}">{{ site }}</a></td>
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<tr>
|
||||
<td>{{ field }}</td>
|
||||
<td>
|
||||
{% if value == True %}
|
||||
{% if field.type == 300 and value == True %}
|
||||
<i class="glyphicon glyphicon-ok text-success" title="True"></i>
|
||||
{% elif value == False %}
|
||||
{% elif field.type == 300 and value == False %}
|
||||
<i class="glyphicon glyphicon-remove text-danger" title="False"></i>
|
||||
{% elif field.type == 500 and value %}
|
||||
{{ value|urlizetrunc:75 }}
|
||||
{% elif value %}
|
||||
<a href="{{ value }}">{{ value|truncatechars:70 }}</a>
|
||||
{% elif field.type == 200 or value %}
|
||||
{{ value }}
|
||||
{% elif field.required %}
|
||||
<span class="text-warning">Not defined</span>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<li><a href="?{% if request.GET %}{{ request.GET.urlencode }}&{% endif %}export">CSV (default)</a></li>
|
||||
<li class="divider"></li>
|
||||
{% 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 %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
|
||||
{% 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 %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ipam:aggregate' pk=aggregate.pk %}">{{ aggregate }}</a></td>
|
||||
|
||||
@@ -3,14 +3,20 @@
|
||||
|
||||
{% 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 %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ipam:ipaddress' pk=ipaddress.pk %}">{{ ipaddress }}</a></td>
|
||||
<td>{{ ipaddress.vrf|default:"Global" }}</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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -3,16 +3,23 @@
|
||||
|
||||
{% 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 %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a></td>
|
||||
<td>{{ prefix.site }}</td>
|
||||
<td>{{ prefix.vrf|default:"Global" }}</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.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,16 +3,23 @@
|
||||
|
||||
{% 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 %}
|
||||
<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.group }}</td>
|
||||
<td>{{ vlan.tenant }}</td>
|
||||
<td>{{ vlan.get_status_display }}</td>
|
||||
<td>{{ vlan.role }}</td>
|
||||
<td>{{ vlan.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
|
||||
{% 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 %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ipam:vrf' pk=vrf.pk %}">{{ vrf.name }}</a></td>
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
|
||||
{% 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 %}
|
||||
<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.name }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
|
||||
{% 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 %}
|
||||
<tr>
|
||||
<td><a href="{% url 'tenancy:tenant' slug=tenant.slug %}">{{ tenant }}</a></td>
|
||||
|
||||
@@ -8,12 +8,15 @@
|
||||
{% if request.POST.redirect_url %}
|
||||
<input type="hidden" name="redirect_url" value="{{ request.POST.redirect_url }}" />
|
||||
{% endif %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<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">
|
||||
{% block select_objects_table %}{% endblock %}
|
||||
{% block selected_objects_table %}{% endblock %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,7 +32,13 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>{% block form_title %}Attributes{% endblock %}</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_form form %}
|
||||
{% for field in form.visible_fields %}
|
||||
{% if field.name in form.nullable_fields %}
|
||||
{% render_field field bulk_nullable=True %}
|
||||
{% else %}
|
||||
{% render_field field %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group text-right">
|
||||
|
||||
@@ -5,17 +5,26 @@
|
||||
{% 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 %}" />
|
||||
{% 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' %}
|
||||
{% block extra_actions %}{% endblock %}
|
||||
{% 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
|
||||
<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
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
@@ -5,26 +5,26 @@
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<div class="checkbox{% if field.errors %} has-error{% endif %}">
|
||||
<label for="{{ field.id_for_label }}">
|
||||
{{ field }}
|
||||
{{ field.label }}
|
||||
{{ field }} {{ field.label }}
|
||||
</label>
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text|safe }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% elif field|widget_type == 'radioselect' %}
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<div class="radio{% if field.errors %} has-error{% endif %}">
|
||||
<label for="{{ field.id_for_label }}">
|
||||
{{ field }}
|
||||
{{ field.label }}
|
||||
{% if bulk_nullable %}
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" name="_nullify" value="{{ field.name }}" /> Set null
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif field|widget_type == 'textarea' %}
|
||||
<div class="col-md-12">
|
||||
{{ field }}
|
||||
{% if bulk_nullable %}
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" name="_nullify" value="{{ field.name }}" /> Set null
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text|safe }}</span>
|
||||
{% endif %}
|
||||
@@ -40,6 +40,11 @@
|
||||
<label class="col-md-3 control-label{% if field.field.required %} required{% endif %}" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||
<div class="col-md-9">
|
||||
{{ field }}
|
||||
{% if bulk_nullable %}
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" name="_nullify" value="{{ field.name }}" /> Set null
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
<span class="help-block">{{ field.help_text|safe }}</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -7,30 +7,6 @@ from utilities.forms import BootstrapMixin, BulkImportForm, CommentField, CSVDat
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
|
||||
def bulkedit_tenantgroup_choices():
|
||||
"""
|
||||
Include an option to remove the currently assigned TenantGroup from a Tenant.
|
||||
"""
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'None'),
|
||||
]
|
||||
choices += [(g.pk, g.name) for g in TenantGroup.objects.all()]
|
||||
return choices
|
||||
|
||||
|
||||
def bulkedit_tenant_choices():
|
||||
"""
|
||||
Include an option to remove the currently assigned Tenant from an object.
|
||||
"""
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'None'),
|
||||
]
|
||||
choices += [(t.pk, t.name) for t in Tenant.objects.all()]
|
||||
return choices
|
||||
|
||||
|
||||
#
|
||||
# Tenant groups
|
||||
#
|
||||
@@ -71,7 +47,10 @@ class TenantImportForm(BulkImportForm, BootstrapMixin):
|
||||
|
||||
class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
group = forms.TypedChoiceField(choices=bulkedit_tenantgroup_choices, coerce=int, required=False, label='Group')
|
||||
group = forms.ModelChoiceField(queryset=TenantGroup.objects.all(), required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['group']
|
||||
|
||||
|
||||
class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import redirect, render, resolve_url
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils.http import is_safe_url
|
||||
|
||||
from secrets.forms import UserKeyForm
|
||||
@@ -26,7 +25,7 @@ def login(request):
|
||||
# Determine where to direct user after successful login
|
||||
redirect_to = request.POST.get('next', '')
|
||||
if not is_safe_url(url=redirect_to, host=request.get_host()):
|
||||
redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)
|
||||
redirect_to = reverse('home')
|
||||
|
||||
# Authenticate user
|
||||
auth_login(request, form.get_user())
|
||||
|
||||
@@ -3,7 +3,9 @@ import itertools
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.core.validators import URLValidator
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
@@ -32,7 +34,7 @@ def add_blank_choice(choices):
|
||||
"""
|
||||
Add a blank choice to the beginning of a choices list.
|
||||
"""
|
||||
return ((None, '---------'),) + choices
|
||||
return ((None, '---------'),) + tuple(choices)
|
||||
|
||||
|
||||
#
|
||||
@@ -90,7 +92,7 @@ class APISelect(SelectWithDisabled):
|
||||
super(APISelect, self).__init__(*args, **kwargs)
|
||||
|
||||
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:
|
||||
self.attrs['display-field'] = display_field
|
||||
if disabled_indicator:
|
||||
@@ -253,6 +255,21 @@ class FilterChoiceField(forms.ModelMultipleChoiceField):
|
||||
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
|
||||
#
|
||||
@@ -277,6 +294,18 @@ class ConfirmationForm(forms.Form, BootstrapMixin):
|
||||
confirm = forms.BooleanField(required=True)
|
||||
|
||||
|
||||
class BulkEditForm(forms.Form):
|
||||
|
||||
def __init__(self, model, *args, **kwargs):
|
||||
super(BulkEditForm, self).__init__(*args, **kwargs)
|
||||
self.model = model
|
||||
# Copy any nullable fields defined in Meta
|
||||
if hasattr(self.Meta, 'nullable_fields'):
|
||||
self.nullable_fields = [field for field in self.Meta.nullable_fields]
|
||||
else:
|
||||
self.nullable_fields = []
|
||||
|
||||
|
||||
class BulkImportForm(forms.Form):
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -12,4 +12,4 @@ class LoginRequiredMiddleware:
|
||||
def process_request(self, request):
|
||||
if LOGIN_REQUIRED and not request.user.is_authenticated():
|
||||
if request.path_info != settings.LOGIN_URL:
|
||||
return HttpResponseRedirect(settings.LOGIN_URL)
|
||||
return HttpResponseRedirect('{}?next={}'.format(settings.LOGIN_URL, request.path_info))
|
||||
|
||||
@@ -27,4 +27,4 @@ class ToggleColumn(tables.CheckBoxColumn):
|
||||
|
||||
@property
|
||||
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" />')
|
||||
|
||||
@@ -5,12 +5,13 @@ register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag('utilities/render_field.html')
|
||||
def render_field(field):
|
||||
def render_field(field, bulk_nullable=False):
|
||||
"""
|
||||
Render a single form field from template
|
||||
"""
|
||||
return {
|
||||
'field': field,
|
||||
'bulk_nullable': bulk_nullable,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -280,42 +280,59 @@ class BulkImportView(View):
|
||||
|
||||
class BulkEditView(View):
|
||||
cls = None
|
||||
parent_cls = None
|
||||
form = None
|
||||
template_name = None
|
||||
default_redirect_url = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
def get(self):
|
||||
return redirect(self.default_redirect_url)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
def post(self, request, **kwargs):
|
||||
|
||||
# Attempt to derive parent object if a parent class has been given
|
||||
if self.parent_cls:
|
||||
parent_obj = get_object_or_404(self.parent_cls, **kwargs)
|
||||
else:
|
||||
parent_obj = None
|
||||
|
||||
# Determine URL to redirect users upon modification of objects
|
||||
posted_redirect_url = request.POST.get('redirect_url')
|
||||
if posted_redirect_url and is_safe_url(url=posted_redirect_url, host=request.get_host()):
|
||||
redirect_url = posted_redirect_url
|
||||
else:
|
||||
elif parent_obj:
|
||||
redirect_url = parent_obj.get_absolute_url()
|
||||
elif self.default_redirect_url:
|
||||
redirect_url = reverse(self.default_redirect_url)
|
||||
else:
|
||||
raise ImproperlyConfigured('No redirect URL has been provided.')
|
||||
|
||||
# Are we editing *all* objects in the queryset or just a selected subset?
|
||||
if request.POST.get('_all'):
|
||||
pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk]
|
||||
else:
|
||||
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||
|
||||
if '_apply' in request.POST:
|
||||
if hasattr(self.form, 'custom_fields'):
|
||||
form = self.form(self.cls, request.POST)
|
||||
else:
|
||||
form = self.form(request.POST)
|
||||
form = self.form(self.cls, request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
|
||||
standard_fields = [field for field in form.fields if field not in custom_fields and field != 'pk']
|
||||
|
||||
# Update objects
|
||||
updated_count = self.update_objects(pk_list, form, standard_fields)
|
||||
# Update standard fields. If a field is listed in _nullify, delete its value.
|
||||
nullified_fields = request.POST.getlist('_nullify')
|
||||
fields_to_update = {}
|
||||
for field in standard_fields:
|
||||
if field in form.nullable_fields and field in nullified_fields:
|
||||
fields_to_update[field] = ''
|
||||
elif form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
|
||||
# Update custom fields for objects
|
||||
if custom_fields:
|
||||
objs_updated = self.update_custom_fields(pk_list, form, custom_fields)
|
||||
objs_updated = self.update_custom_fields(pk_list, form, custom_fields, nullified_fields)
|
||||
if objs_updated and not updated_count:
|
||||
updated_count = objs_updated
|
||||
|
||||
@@ -326,10 +343,7 @@ class BulkEditView(View):
|
||||
return redirect(redirect_url)
|
||||
|
||||
else:
|
||||
if hasattr(self.form, 'custom_fields'):
|
||||
form = self.form(self.cls, initial={'pk': pk_list})
|
||||
else:
|
||||
form = self.form(initial={'pk': pk_list})
|
||||
form = self.form(self.cls, initial={'pk': pk_list})
|
||||
|
||||
selected_objects = self.cls.objects.filter(pk__in=pk_list)
|
||||
if not selected_objects:
|
||||
@@ -342,26 +356,23 @@ class BulkEditView(View):
|
||||
'cancel_url': redirect_url,
|
||||
})
|
||||
|
||||
def update_objects(self, pk_list, form, fields):
|
||||
fields_to_update = {}
|
||||
|
||||
for name in fields:
|
||||
# Check for zero value (bulk editing)
|
||||
if isinstance(form.fields[name], TypedChoiceField) and form.cleaned_data[name] == 0:
|
||||
fields_to_update[name] = None
|
||||
elif form.cleaned_data[name]:
|
||||
fields_to_update[name] = form.cleaned_data[name]
|
||||
|
||||
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
|
||||
def update_custom_fields(self, pk_list, form, fields):
|
||||
def update_custom_fields(self, pk_list, form, fields, nullified_fields):
|
||||
obj_type = ContentType.objects.get_for_model(self.cls)
|
||||
objs_updated = False
|
||||
|
||||
for name in fields:
|
||||
if form.cleaned_data[name] not in [None, u'']:
|
||||
|
||||
field = form.fields[name].model
|
||||
field = form.fields[name].model
|
||||
|
||||
# Setting the field to null
|
||||
if name in form.nullable_fields and name in nullified_fields:
|
||||
|
||||
# Delete all CustomFieldValues for instances of this field belonging to the selected objects.
|
||||
CustomFieldValue.objects.filter(field=field, obj_type=obj_type, obj_id__in=pk_list).delete()
|
||||
objs_updated = True
|
||||
|
||||
# Updating the value of the field
|
||||
elif form.cleaned_data[name] not in [None, u'']:
|
||||
|
||||
# Check for zero value (bulk editing)
|
||||
if isinstance(form.fields[name], TypedChoiceField) and form.cleaned_data[name] == 0:
|
||||
@@ -400,7 +411,7 @@ class BulkDeleteView(View):
|
||||
template_name = 'utilities/confirm_bulk_delete.html'
|
||||
default_redirect_url = None
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
def post(self, request, **kwargs):
|
||||
|
||||
# Attempt to derive parent object if a parent class has been given
|
||||
if self.parent_cls:
|
||||
@@ -421,9 +432,9 @@ class BulkDeleteView(View):
|
||||
|
||||
# Are we deleting *all* objects in the queryset or just a selected subset?
|
||||
if request.POST.get('_all'):
|
||||
pk_list = [x for x in request.POST.get('pk_all').split(',') if x]
|
||||
pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk]
|
||||
else:
|
||||
pk_list = request.POST.getlist('pk')
|
||||
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||
|
||||
form_cls = self.get_form()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user