Merge branch 'develop' of https://github.com/digitalocean/netbox into 2902-systemd

This commit is contained in:
dansheps 2019-03-20 08:52:36 -05:00
commit 2dae2e60d6
135 changed files with 1768 additions and 309 deletions

View File

@ -1,18 +1,76 @@
v2.5.7 (FUTURE) v2.5.9 (FUTURE)
## Enhancements
* [#3011](https://github.com/digitalocean/netbox/issues/3011) - Add SSL support for django-rq (requires django-rq v1.3.1+)
## Bug Fixes
* [#2207](https://github.com/digitalocean/netbox/issues/2207) - Fixes Deterministic Ordering of Interfaces
* [#2577](https://github.com/digitalocean/netbox/issues/2577) - Clarification of wording in API regarding filtering
* [#2924](https://github.com/digitalocean/netbox/issues/2924) - Add interface type for QSFP28 50GE
* [#2936](https://github.com/digitalocean/netbox/issues/2936) - Fix device role selection showing duplicate first entry
* [#2998](https://github.com/digitalocean/netbox/issues/2998) - Limit device query to non-racked devices if no rack selected when creating a cable
* [#3014](https://github.com/digitalocean/netbox/issues/3014) - Fixes VM Role filtering
v2.5.8 (2019-03-11)
## Enhancements
* [#2435](https://github.com/digitalocean/netbox/issues/2435) - Printer friendly CSS
## Bug Fixes
* [#2065](https://github.com/digitalocean/netbox/issues/2065) - Correct documentation for VM interface serializer
* [#2705](https://github.com/digitalocean/netbox/issues/2705) - Fix endpoint grouping in API docs
* [#2781](https://github.com/digitalocean/netbox/issues/2781) - Fix filtering of sites/devices/VMs by multiple regions
* [#2923](https://github.com/digitalocean/netbox/issues/2923) - Provider filter form's site field should be blank by default
* [#2938](https://github.com/digitalocean/netbox/issues/2938) - Enforce deterministic ordering of device components returned by API
* [#2939](https://github.com/digitalocean/netbox/issues/2939) - Exclude circuit terminations from API interface connections endpoint
* [#2940](https://github.com/digitalocean/netbox/issues/2940) - Allow CSV import of prefixes/IPs to VRF without an RD assigned
* [#2944](https://github.com/digitalocean/netbox/issues/2944) - Record the deletion of an IP address in the changelog of its parent interface (if any)
* [#2952](https://github.com/digitalocean/netbox/issues/2952) - Added the `slug` field to the Tenant filter for use in the API and search function
* [#2954](https://github.com/digitalocean/netbox/issues/2954) - Remove trailing slashes to fix root/template paths on Windows
* [#2961](https://github.com/digitalocean/netbox/issues/2961) - Prevent exception when exporting inventory items belonging to unnamed devices
* [#2962](https://github.com/digitalocean/netbox/issues/2962) - Increase ExportTemplate `mime_type` field length
* [#2966](https://github.com/digitalocean/netbox/issues/2966) - Accept `null` cable length_unit via API
* [#2972](https://github.com/digitalocean/netbox/issues/2972) - Improve ContentTypeField serializer to elegantly handle invalid data
* [#2976](https://github.com/digitalocean/netbox/issues/2976) - Add delete button to tag view
* [#2980](https://github.com/digitalocean/netbox/issues/2980) - Improve rendering time for API docs
* [#2982](https://github.com/digitalocean/netbox/issues/2982) - Correct CSS class assignment on color picker
* [#2984](https://github.com/digitalocean/netbox/issues/2984) - Fix logging of unlabeled cable ID on cable deletion
* [#2985](https://github.com/digitalocean/netbox/issues/2985) - Fix pagination page length for rack elevations
---
v2.5.7 (2019-02-21)
## Enhancements ## Enhancements
* [#2902](https://github.com/digitalocean/netbox/issues/2902) - Replace supervisord with systemd * [#2902](https://github.com/digitalocean/netbox/issues/2902) - Replace supervisord with systemd
* [#2357](https://github.com/digitalocean/netbox/issues/2357) - Enable filtering of devices by rack face * [#2357](https://github.com/digitalocean/netbox/issues/2357) - Enable filtering of devices by rack face
* [#2638](https://github.com/digitalocean/netbox/issues/2638) - Add button to copy unlocked secret to clipboard
* [#2870](https://github.com/digitalocean/netbox/issues/2870) - Add Markdown rendering for provider NOC/admin contact fields
* [#2878](https://github.com/digitalocean/netbox/issues/2878) - Add cable types for OS1/OS2 singlemode fiber
* [#2890](https://github.com/digitalocean/netbox/issues/2890) - Add port types for APC fiber
* [#2898](https://github.com/digitalocean/netbox/issues/2898) - Enable filtering cables list by connection status
* [#2903](https://github.com/digitalocean/netbox/issues/2903) - Clarify purpose of tags field on interface edit form * [#2903](https://github.com/digitalocean/netbox/issues/2903) - Clarify purpose of tags field on interface edit form
## Bug Fixes ## Bug Fixes
* [#2852](https://github.com/digitalocean/netbox/issues/2852) - Allow filtering devices by null rack position
* [#2884](https://github.com/digitalocean/netbox/issues/2884) - Don't display connect button for wireless interfaces * [#2884](https://github.com/digitalocean/netbox/issues/2884) - Don't display connect button for wireless interfaces
* [#2888](https://github.com/digitalocean/netbox/issues/2888) - Correct foreground color of device roles in rack elevations * [#2888](https://github.com/digitalocean/netbox/issues/2888) - Correct foreground color of device roles in rack elevations
* [#2893](https://github.com/digitalocean/netbox/issues/2893) - Remove duplicate display of VRF RD on IP address view * [#2893](https://github.com/digitalocean/netbox/issues/2893) - Remove duplicate display of VRF RD on IP address view
* [#2895](https://github.com/digitalocean/netbox/issues/2895) - Fix filtering of nullable character fields * [#2895](https://github.com/digitalocean/netbox/issues/2895) - Fix filtering of nullable character fields
* [#2901](https://github.com/digitalocean/netbox/issues/2901) - Fix ordering regions by site count * [#2901](https://github.com/digitalocean/netbox/issues/2901) - Fix ordering regions by site count
* [#2910](https://github.com/digitalocean/netbox/issues/2910) - Fix config context list and edit forms to use Select2 elements
* [#2912](https://github.com/digitalocean/netbox/issues/2912) - Cable type in filter form should be blank by default
* [#2913](https://github.com/digitalocean/netbox/issues/2913) - Fix assigned prefixes link on VRF view
* [#2914](https://github.com/digitalocean/netbox/issues/2914) - Fix empty connected circuit link on device interfaces list
* [#2915](https://github.com/digitalocean/netbox/issues/2915) - Fix bulk editing of pass-through ports
---
v2.5.6 (2019-02-13) v2.5.6 (2019-02-13)

View File

@ -45,13 +45,13 @@ and run `upgrade.sh`.
## Supported SDK ## Supported SDK
- [pynetbox](https://github.com/digitalocean/pynetbox) Python API client library for Netbox. - [pynetbox](https://github.com/digitalocean/pynetbox) - A Python API client library for Netbox
## Community SDK ## Community SDK
- [netbox-client-ruby](https://github.com/ninech/netbox-client-ruby) A ruby client library for Netbox v2. - [netbox-client-ruby](https://github.com/ninech/netbox-client-ruby) - A Ruby client library for Netbox
- [powerbox](https://github.com/BatmanAMA/powerbox) - A PowerShell library for Netbox
## Ansible Inventory ## Ansible Inventory
- [netbox-as-ansible-inventory](https://github.com/AAbouZaid/netbox-as-ansible-inventory) Ansible dynamic inventory script for Netbox. - [netbox-as-ansible-inventory](https://github.com/AAbouZaid/netbox-as-ansible-inventory) - Ansible dynamic inventory script for Netbox

View File

@ -128,4 +128,4 @@ Reports can be run on the CLI by invoking the management command:
python3 manage.py runreport <module> python3 manage.py runreport <module>
``` ```
One or more report modules may be specified. where ``<module>`` is the name of the python file in the ``reports`` directory without the ``.py`` extension. One or more report modules may be specified.

View File

@ -261,7 +261,7 @@ A list of objects retrieved via the API can be filtered by passing one or more q
GET /api/ipam/prefixes/?status=1 GET /api/ipam/prefixes/?status=1
``` ```
The same filter can be incldued multiple times. These will effect a logical OR and return objects matching any of the given values. For example, the following will return all active and reserved prefixes: Certain filters can be included multiple times within a single request. These will effect a logical OR and return objects matching any of the given values. For example, the following will return all active and reserved prefixes:
``` ```
GET /api/ipam/prefixes/?status=1&status=2 GET /api/ipam/prefixes/?status=1&status=2

View File

@ -283,6 +283,7 @@ REDIS = {
'PASSWORD': '', 'PASSWORD': '',
'DATABASE': 0, 'DATABASE': 0,
'DEFAULT_TIMEOUT': 300, 'DEFAULT_TIMEOUT': 300,
'SSL': False,
} }
``` ```
@ -315,3 +316,9 @@ The TCP port to use when connecting to the Redis server.
Default: None Default: None
The password to use when authenticating to the Redis server (optional). The password to use when authenticating to the Redis server (optional).
### SSL
Default: False
Use secure sockets layer to encrypt the connections to the Redis server.

View File

@ -107,7 +107,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
widget=APISelect( widget=APISelectMultiple(
api_url="/api/dcim/sites/", api_url="/api/dcim/sites/",
value_field="slug", value_field="slug",
) )

View File

@ -59,7 +59,7 @@ class CircuitTypeTable(BaseTable):
name = tables.LinkColumn() name = tables.LinkColumn()
circuit_count = tables.Column(verbose_name='Circuits') circuit_count = tables.Column(verbose_name='Circuits')
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='' template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):

View File

@ -0,0 +1,91 @@
import urllib.parse
from django.test import Client, TestCase
from django.urls import reverse
from circuits.models import Circuit, CircuitType, Provider
class ProviderTestCase(TestCase):
def setUp(self):
self.client = Client()
Provider.objects.bulk_create([
Provider(name='Provider 1', slug='provider-1', asn=65001),
Provider(name='Provider 2', slug='provider-2', asn=65002),
Provider(name='Provider 3', slug='provider-3', asn=65003),
])
def test_provider_list(self):
url = reverse('circuits:provider_list')
params = {
"q": "test",
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_provider(self):
provider = Provider.objects.first()
response = self.client.get(provider.get_absolute_url())
self.assertEqual(response.status_code, 200)
class CircuitTypeTestCase(TestCase):
def setUp(self):
self.client = Client()
CircuitType.objects.bulk_create([
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
])
def test_circuittype_list(self):
url = reverse('circuits:circuittype_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class CircuitTestCase(TestCase):
def setUp(self):
self.client = Client()
provider = Provider(name='Provider 1', slug='provider-1', asn=65001)
provider.save()
circuittype = CircuitType(name='Circuit Type 1', slug='circuit-type-1')
circuittype.save()
Circuit.objects.bulk_create([
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
])
def test_circuit_list(self):
url = reverse('circuits:circuit_list')
params = {
"provider": Provider.objects.first().slug,
"type": CircuitType.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_provider(self):
provider = Provider.objects.first()
response = self.client.get(provider.get_absolute_url())
self.assertEqual(response.status_code, 200)

View File

@ -507,7 +507,7 @@ class CableSerializer(ValidatedModelSerializer):
termination_a = serializers.SerializerMethodField(read_only=True) termination_a = serializers.SerializerMethodField(read_only=True)
termination_b = serializers.SerializerMethodField(read_only=True) termination_b = serializers.SerializerMethodField(read_only=True)
status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False) status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False) length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False, allow_null=True)
class Meta: class Meta:
model = Cable model = Cable

View File

@ -496,11 +496,11 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = Interface.objects.select_related( queryset = Interface.objects.select_related(
'device', '_connected_interface', '_connected_circuittermination' 'device', '_connected_interface__device'
).filter( ).filter(
# Avoid duplicate connections by only selecting the lower PK in a connected pair # Avoid duplicate connections by only selecting the lower PK in a connected pair
Q(_connected_interface__isnull=False, pk__lt=F('_connected_interface')) | _connected_interface__isnull=False,
Q(_connected_circuittermination__isnull=False) pk__lt=F('_connected_interface')
) )
serializer_class = serializers.InterfaceConnectionSerializer serializer_class = serializers.InterfaceConnectionSerializer
filterset_class = filters.InterfaceConnectionFilter filterset_class = filters.InterfaceConnectionFilter

View File

@ -43,6 +43,12 @@ RACK_STATUS_CHOICES = [
[RACK_STATUS_DEPRECATED, 'Deprecated'], [RACK_STATUS_DEPRECATED, 'Deprecated'],
] ]
# Device rack position
DEVICE_POSITION_CHOICES = [
# Rack.u_height is limited to 100
(i, 'Unit {}'.format(i)) for i in range(1, 101)
]
# Parent/child device roles # Parent/child device roles
SUBDEVICE_ROLE_PARENT = True SUBDEVICE_ROLE_PARENT = True
SUBDEVICE_ROLE_CHILD = False SUBDEVICE_ROLE_CHILD = False
@ -77,6 +83,7 @@ IFACE_FF_10GE_XENPAK = 1310
IFACE_FF_10GE_X2 = 1320 IFACE_FF_10GE_X2 = 1320
IFACE_FF_25GE_SFP28 = 1350 IFACE_FF_25GE_SFP28 = 1350
IFACE_FF_40GE_QSFP_PLUS = 1400 IFACE_FF_40GE_QSFP_PLUS = 1400
IFACE_FF_50GE_QSFP28 = 1420
IFACE_FF_100GE_CFP = 1500 IFACE_FF_100GE_CFP = 1500
IFACE_FF_100GE_CFP2 = 1510 IFACE_FF_100GE_CFP2 = 1510
IFACE_FF_100GE_CFP4 = 1520 IFACE_FF_100GE_CFP4 = 1520
@ -158,6 +165,7 @@ IFACE_FF_CHOICES = [
[IFACE_FF_10GE_X2, 'X2 (10GE)'], [IFACE_FF_10GE_X2, 'X2 (10GE)'],
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'], [IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'], [IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_FF_50GE_QSFP28, 'QSFP28 (50GE)'],
[IFACE_FF_100GE_CFP, 'CFP (100GE)'], [IFACE_FF_100GE_CFP, 'CFP (100GE)'],
[IFACE_FF_100GE_CFP2, 'CFP2 (100GE)'], [IFACE_FF_100GE_CFP2, 'CFP2 (100GE)'],
[IFACE_FF_200GE_CFP2, 'CFP2 (200GE)'], [IFACE_FF_200GE_CFP2, 'CFP2 (200GE)'],
@ -270,11 +278,14 @@ PORT_TYPE_8P8C = 1000
PORT_TYPE_110_PUNCH = 1100 PORT_TYPE_110_PUNCH = 1100
PORT_TYPE_ST = 2000 PORT_TYPE_ST = 2000
PORT_TYPE_SC = 2100 PORT_TYPE_SC = 2100
PORT_TYPE_SC_APC = 2110
PORT_TYPE_FC = 2200 PORT_TYPE_FC = 2200
PORT_TYPE_LC = 2300 PORT_TYPE_LC = 2300
PORT_TYPE_LC_APC = 2310
PORT_TYPE_MTRJ = 2400 PORT_TYPE_MTRJ = 2400
PORT_TYPE_MPO = 2500 PORT_TYPE_MPO = 2500
PORT_TYPE_LSH = 2600 PORT_TYPE_LSH = 2600
PORT_TYPE_LSH_APC = 2610
PORT_TYPE_CHOICES = [ PORT_TYPE_CHOICES = [
[ [
'Copper', 'Copper',
@ -288,10 +299,13 @@ PORT_TYPE_CHOICES = [
[ [
[PORT_TYPE_FC, 'FC'], [PORT_TYPE_FC, 'FC'],
[PORT_TYPE_LC, 'LC'], [PORT_TYPE_LC, 'LC'],
[PORT_TYPE_LC_APC, 'LC/APC'],
[PORT_TYPE_LSH, 'LSH'], [PORT_TYPE_LSH, 'LSH'],
[PORT_TYPE_LSH_APC, 'LSH/APC'],
[PORT_TYPE_MPO, 'MPO'], [PORT_TYPE_MPO, 'MPO'],
[PORT_TYPE_MTRJ, 'MTRJ'], [PORT_TYPE_MTRJ, 'MTRJ'],
[PORT_TYPE_SC, 'SC'], [PORT_TYPE_SC, 'SC'],
[PORT_TYPE_SC_APC, 'SC/APC'],
[PORT_TYPE_ST, 'ST'], [PORT_TYPE_ST, 'ST'],
] ]
] ]
@ -355,11 +369,14 @@ CABLE_TYPE_CAT6A = 1610
CABLE_TYPE_CAT7 = 1700 CABLE_TYPE_CAT7 = 1700
CABLE_TYPE_DAC_ACTIVE = 1800 CABLE_TYPE_DAC_ACTIVE = 1800
CABLE_TYPE_DAC_PASSIVE = 1810 CABLE_TYPE_DAC_PASSIVE = 1810
CABLE_TYPE_MMF = 3000
CABLE_TYPE_MMF_OM1 = 3010 CABLE_TYPE_MMF_OM1 = 3010
CABLE_TYPE_MMF_OM2 = 3020 CABLE_TYPE_MMF_OM2 = 3020
CABLE_TYPE_MMF_OM3 = 3030 CABLE_TYPE_MMF_OM3 = 3030
CABLE_TYPE_MMF_OM4 = 3040 CABLE_TYPE_MMF_OM4 = 3040
CABLE_TYPE_SMF = 3500 CABLE_TYPE_SMF = 3500
CABLE_TYPE_SMF_OS1 = 3510
CABLE_TYPE_SMF_OS2 = 3520
CABLE_TYPE_AOC = 3800 CABLE_TYPE_AOC = 3800
CABLE_TYPE_POWER = 5000 CABLE_TYPE_POWER = 5000
CABLE_TYPE_CHOICES = ( CABLE_TYPE_CHOICES = (
@ -377,11 +394,14 @@ CABLE_TYPE_CHOICES = (
), ),
( (
'Fiber', ( 'Fiber', (
(CABLE_TYPE_MMF, 'Multimode Fiber'),
(CABLE_TYPE_MMF_OM1, 'Multimode Fiber (OM1)'), (CABLE_TYPE_MMF_OM1, 'Multimode Fiber (OM1)'),
(CABLE_TYPE_MMF_OM2, 'Multimode Fiber (OM2)'), (CABLE_TYPE_MMF_OM2, 'Multimode Fiber (OM2)'),
(CABLE_TYPE_MMF_OM3, 'Multimode Fiber (OM3)'), (CABLE_TYPE_MMF_OM3, 'Multimode Fiber (OM3)'),
(CABLE_TYPE_MMF_OM4, 'Multimode Fiber (OM4)'), (CABLE_TYPE_MMF_OM4, 'Multimode Fiber (OM4)'),
(CABLE_TYPE_SMF, 'Singlemode Fiber'), (CABLE_TYPE_SMF, 'Singlemode Fiber'),
(CABLE_TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'),
(CABLE_TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'),
(CABLE_TYPE_AOC, 'Active Optical Cabling (AOC)'), (CABLE_TYPE_AOC, 'Active Optical Cabling (AOC)'),
), ),
), ),

View File

@ -1,6 +1,5 @@
import django_filters import django_filters
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q from django.db.models import Q
from netaddr import EUI from netaddr import EUI
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
@ -8,7 +7,9 @@ from netaddr.core import AddrFormatError
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.constants import COLOR_CHOICES from utilities.constants import COLOR_CHOICES
from utilities.filters import NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter from utilities.filters import (
NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
)
from virtualization.models import Cluster from virtualization.models import Cluster
from .constants import * from .constants import *
from .models import ( from .models import (
@ -49,14 +50,15 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
choices=SITE_STATUS_CHOICES, choices=SITE_STATUS_CHOICES,
null_value=None null_value=None
) )
region_id = django_filters.NumberFilter( region_id = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='pk', field_name='region__in',
label='Region (ID)', label='Region (ID)',
) )
region = django_filters.CharFilter( region = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='slug', field_name='region__in',
to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter( tenant_id = django_filters.ModelMultipleChoiceFilter(
@ -95,16 +97,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
pass pass
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
def filter_region(self, queryset, name, value):
try:
region = Region.objects.get(**{name: value})
except ObjectDoesNotExist:
return queryset.none()
return queryset.filter(
Q(region=region) |
Q(region__in=region.get_descendants())
)
class RackGroupFilter(NameSlugSearchFilterSet): class RackGroupFilter(NameSlugSearchFilterSet):
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
@ -513,14 +505,15 @@ class DeviceFilter(CustomFieldFilterSet):
) )
name = NullableCharFieldFilter() name = NullableCharFieldFilter()
asset_tag = NullableCharFieldFilter() asset_tag = NullableCharFieldFilter()
region_id = django_filters.NumberFilter( region_id = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='pk', field_name='site__region__in',
label='Region (ID)', label='Region (ID)',
) )
region = django_filters.CharFilter( region = TreeNodeMultipleChoiceFilter(
method='filter_region', queryset=Region.objects.all(),
field_name='slug', field_name='site__region__in',
to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
@ -543,6 +536,10 @@ class DeviceFilter(CustomFieldFilterSet):
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
label='Rack (ID)', label='Rack (ID)',
) )
position = django_filters.ChoiceFilter(
choices=DEVICE_POSITION_CHOICES,
null_label='Non-racked'
)
cluster_id = django_filters.ModelMultipleChoiceFilter( cluster_id = django_filters.ModelMultipleChoiceFilter(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
label='VM cluster (ID)', label='VM cluster (ID)',
@ -602,7 +599,7 @@ class DeviceFilter(CustomFieldFilterSet):
class Meta: class Meta:
model = Device model = Device
fields = ['serial', 'position', 'face'] fields = ['serial', 'face']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -615,16 +612,6 @@ class DeviceFilter(CustomFieldFilterSet):
Q(comments__icontains=value) Q(comments__icontains=value)
).distinct() ).distinct()
def filter_region(self, queryset, name, value):
try:
region = Region.objects.get(**{name: value})
except ObjectDoesNotExist:
return queryset.none()
return queryset.filter(
Q(site__region=region) |
Q(site__region__in=region.get_descendants())
)
def _mac_address(self, queryset, name, value): def _mac_address(self, queryset, name, value):
value = value.strip() value = value.strip()
if not value: if not value:

View File

@ -1700,7 +1700,6 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/device-roles/", api_url="/api/dcim/device-roles/",
value_field="slug", value_field="slug",
null_option=True,
) )
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
@ -2362,7 +2361,7 @@ class FrontPortCreateForm(ComponentForm):
class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=Interface.objects.all(), queryset=FrontPort.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
) )
type = forms.ChoiceField( type = forms.ChoiceField(
@ -2436,7 +2435,7 @@ class RearPortCreateForm(ComponentForm):
class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=Interface.objects.all(), queryset=RearPort.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
) )
type = forms.ChoiceField( type = forms.ChoiceField(
@ -2753,10 +2752,15 @@ class CableFilterForm(BootstrapMixin, forms.Form):
label='Search' label='Search'
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
choices=CABLE_TYPE_CHOICES, choices=add_blank_choice(CABLE_TYPE_CHOICES),
required=False, required=False,
widget=StaticSelect2() widget=StaticSelect2()
) )
status = forms.ChoiceField(
required=False,
choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
widget=StaticSelect2()
)
color = forms.CharField( color = forms.CharField(
max_length=6, max_length=6,
required=False, required=False,

View File

@ -27,7 +27,7 @@ class DeviceComponentManager(Manager):
select={ select={
'name_padded': sql.format(table_name, table_name), 'name_padded': sql.format(table_name, table_name),
} }
).order_by('name_padded') ).order_by('name_padded', 'pk')
class InterfaceQuerySet(QuerySet): class InterfaceQuerySet(QuerySet):
@ -64,11 +64,15 @@ class InterfaceManager(Manager):
The original `name` field is considered in its entirety to serve as a fallback in the event interfaces do not The original `name` field is considered in its entirety to serve as a fallback in the event interfaces do not
match any of the prescribed fields. match any of the prescribed fields.
The `id` field is included to enforce deterministic ordering of interfaces in similar vein of other device
components.
""" """
sql_col = '{}.name'.format(self.model._meta.db_table) sql_col = '{}.name'.format(self.model._meta.db_table)
ordering = [ ordering = [
'_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name', '_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name', 'pk'
] ]
fields = { fields = {

View File

@ -2423,7 +2423,7 @@ class InventoryItem(ComponentModel):
def to_csv(self): def to_csv(self):
return ( return (
self.device.name or '{' + self.device.pk + '}', self.device.name or '{{{}}}'.format(self.device.pk),
self.name, self.name,
self.manufacturer.name if self.manufacturer else None, self.manufacturer.name if self.manufacturer else None,
self.part_id, self.part_id,
@ -2557,16 +2557,15 @@ class Cable(ChangeLoggedModel):
('termination_b_type', 'termination_b_id'), ('termination_b_type', 'termination_b_id'),
) )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Create an ID string for use by __str__(). We have to save a copy of pk since it's nullified after .delete()
# is called.
self.id_string = '#{}'.format(self.pk)
def __str__(self): def __str__(self):
return self.label or self.id_string if self.label:
return self.label
# Save a copy of the PK on the instance since it's nullified if .delete() is called
if not hasattr(self, 'id_string'):
self.id_string = '#{}'.format(self.pk)
return self.id_string
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:cable', args=[self.pk]) return reverse('dcim:cable', args=[self.pk])
@ -2651,6 +2650,9 @@ class Cable(ChangeLoggedModel):
self.length_unit, self.length_unit,
) )
def get_status_class(self):
return 'success' if self.status else 'info'
def get_path_endpoints(self): def get_path_endpoints(self):
""" """
Traverse both ends of a cable path and return its connected endpoints. Note that one or both endpoints may be Traverse both ends of a cable path and return its connected endpoints. Note that one or both endpoints may be

View File

@ -196,7 +196,7 @@ class RegionTable(BaseTable):
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=REGION_ACTIONS, template_code=REGION_ACTIONS,
attrs={'td': {'class': 'text-right'}}, attrs={'td': {'class': 'text-right noprint'}},
verbose_name='' verbose_name=''
) )
@ -239,7 +239,7 @@ class RackGroupTable(BaseTable):
slug = tables.Column() slug = tables.Column()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=RACKGROUP_ACTIONS, template_code=RACKGROUP_ACTIONS,
attrs={'td': {'class': 'text-right'}}, attrs={'td': {'class': 'text-right noprint'}},
verbose_name='' verbose_name=''
) )
@ -258,7 +258,7 @@ class RackRoleTable(BaseTable):
rack_count = tables.Column(verbose_name='Racks') rack_count = tables.Column(verbose_name='Racks')
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color') color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color')
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}},
verbose_name='') verbose_name='')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -309,7 +309,7 @@ class RackReservationTable(BaseTable):
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
unit_list = tables.Column(orderable=False, verbose_name='Units') unit_list = tables.Column(orderable=False, verbose_name='Units')
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='' template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -327,7 +327,7 @@ class ManufacturerTable(BaseTable):
devicetype_count = tables.Column(verbose_name='Device Types') devicetype_count = tables.Column(verbose_name='Device Types')
platform_count = tables.Column(verbose_name='Platforms') platform_count = tables.Column(verbose_name='Platforms')
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}}, actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right noprint'}},
verbose_name='') verbose_name='')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -463,7 +463,7 @@ class DeviceRoleTable(BaseTable):
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=DEVICEROLE_ACTIONS, template_code=DEVICEROLE_ACTIONS,
attrs={'td': {'class': 'text-right'}}, attrs={'td': {'class': 'text-right noprint'}},
verbose_name='' verbose_name=''
) )
@ -492,7 +492,7 @@ class PlatformTable(BaseTable):
) )
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=PLATFORM_ACTIONS, template_code=PLATFORM_ACTIONS,
attrs={'td': {'class': 'text-right'}}, attrs={'td': {'class': 'text-right noprint'}},
verbose_name='' verbose_name=''
) )
@ -647,6 +647,9 @@ class CableTable(BaseTable):
orderable=False, orderable=False,
verbose_name='' verbose_name=''
) )
status = tables.TemplateColumn(
template_code=STATUS_LABEL
)
length = tables.TemplateColumn( length = tables.TemplateColumn(
template_code=CABLE_LENGTH, template_code=CABLE_LENGTH,
order_by='_abs_length' order_by='_abs_length'
@ -776,7 +779,7 @@ class VirtualChassisTable(BaseTable):
member_count = tables.Column(verbose_name='Members') member_count = tables.Column(verbose_name='Members')
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=VIRTUALCHASSIS_ACTIONS, template_code=VIRTUALCHASSIS_ACTIONS,
attrs={'td': {'class': 'text-right'}}, attrs={'td': {'class': 'text-right noprint'}},
verbose_name='' verbose_name=''
) )

View File

@ -0,0 +1,458 @@
import urllib.parse
from django.contrib.auth import get_user_model
from django.test import Client, TestCase
from django.urls import reverse
from dcim.constants import CABLE_TYPE_CAT6, IFACE_FF_1GE_FIXED
from dcim.models import (
Cable, Device, DeviceRole, DeviceType, Interface, InventoryItem, Manufacturer, Platform, Rack, RackGroup,
RackReservation, RackRole, Site, Region, VirtualChassis,
)
class RegionTestCase(TestCase):
def setUp(self):
self.client = Client()
# Create three Regions
for i in range(1, 4):
Region(name='Region {}'.format(i), slug='region-{}'.format(i)).save()
def test_region_list(self):
url = reverse('dcim:region_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class SiteTestCase(TestCase):
def setUp(self):
self.client = Client()
region = Region(name='Region 1', slug='region-1')
region.save()
Site.objects.bulk_create([
Site(name='Site 1', slug='site-1', region=region),
Site(name='Site 2', slug='site-2', region=region),
Site(name='Site 3', slug='site-3', region=region),
])
def test_site_list(self):
url = reverse('dcim:site_list')
params = {
"region": Region.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_site(self):
site = Site.objects.first()
response = self.client.get(site.get_absolute_url())
self.assertEqual(response.status_code, 200)
class RackGroupTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
RackGroup.objects.bulk_create([
RackGroup(name='Rack Group 1', slug='rack-group-1', site=site),
RackGroup(name='Rack Group 2', slug='rack-group-2', site=site),
RackGroup(name='Rack Group 3', slug='rack-group-3', site=site),
])
def test_rackgroup_list(self):
url = reverse('dcim:rackgroup_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class RackTypeTestCase(TestCase):
def setUp(self):
self.client = Client()
RackRole.objects.bulk_create([
RackRole(name='Rack Role 1', slug='rack-role-1'),
RackRole(name='Rack Role 2', slug='rack-role-2'),
RackRole(name='Rack Role 3', slug='rack-role-3'),
])
def test_rackrole_list(self):
url = reverse('dcim:rackrole_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class RackReservationTestCase(TestCase):
def setUp(self):
self.client = Client()
User = get_user_model()
user = User(username='testuser', email='testuser@example.com')
user.save()
site = Site(name='Site 1', slug='site-1')
site.save()
rack = Rack(name='Rack 1', site=site)
rack.save()
RackReservation.objects.bulk_create([
RackReservation(rack=rack, user=user, units=[1, 2, 3], description='Reservation 1'),
RackReservation(rack=rack, user=user, units=[4, 5, 6], description='Reservation 2'),
RackReservation(rack=rack, user=user, units=[7, 8, 9], description='Reservation 3'),
])
def test_rackreservation_list(self):
url = reverse('dcim:rackreservation_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class RackTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
Rack.objects.bulk_create([
Rack(name='Rack 1', site=site),
Rack(name='Rack 2', site=site),
Rack(name='Rack 3', site=site),
])
def test_rack_list(self):
url = reverse('dcim:rack_list')
params = {
"site": Site.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_rack(self):
rack = Rack.objects.first()
response = self.client.get(rack.get_absolute_url())
self.assertEqual(response.status_code, 200)
class ManufacturerTypeTestCase(TestCase):
def setUp(self):
self.client = Client()
Manufacturer.objects.bulk_create([
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
])
def test_manufacturer_list(self):
url = reverse('dcim:manufacturer_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class DeviceTypeTestCase(TestCase):
def setUp(self):
self.client = Client()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
DeviceType.objects.bulk_create([
DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturer),
DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturer),
DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturer),
])
def test_devicetype_list(self):
url = reverse('dcim:devicetype_list')
params = {
"manufacturer": Manufacturer.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_devicetype(self):
devicetype = DeviceType.objects.first()
response = self.client.get(devicetype.get_absolute_url())
self.assertEqual(response.status_code, 200)
class DeviceRoleTestCase(TestCase):
def setUp(self):
self.client = Client()
DeviceRole.objects.bulk_create([
DeviceRole(name='Device Role 1', slug='device-role-1'),
DeviceRole(name='Device Role 2', slug='device-role-2'),
DeviceRole(name='Device Role 3', slug='device-role-3'),
])
def test_devicerole_list(self):
url = reverse('dcim:devicerole_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class PlatformTestCase(TestCase):
def setUp(self):
self.client = Client()
Platform.objects.bulk_create([
Platform(name='Platform 1', slug='platform-1'),
Platform(name='Platform 2', slug='platform-2'),
Platform(name='Platform 3', slug='platform-3'),
])
def test_platform_list(self):
url = reverse('dcim:platform_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class DeviceTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
devicetype.save()
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
devicerole.save()
Device.objects.bulk_create([
Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole),
Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole),
Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole),
])
def test_device_list(self):
url = reverse('dcim:device_list')
params = {
"device_type_id": DeviceType.objects.first().pk,
"role": DeviceRole.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_device(self):
device = Device.objects.first()
response = self.client.get(device.get_absolute_url())
self.assertEqual(response.status_code, 200)
class InventoryItemTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
devicetype.save()
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
devicerole.save()
device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
device.save()
InventoryItem.objects.bulk_create([
InventoryItem(device=device, name='Inventory Item 1'),
InventoryItem(device=device, name='Inventory Item 2'),
InventoryItem(device=device, name='Inventory Item 3'),
])
def test_inventoryitem_list(self):
url = reverse('dcim:inventoryitem_list')
params = {
"device_id": Device.objects.first().pk,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_inventoryitem(self):
inventoryitem = InventoryItem.objects.first()
response = self.client.get(inventoryitem.get_absolute_url())
self.assertEqual(response.status_code, 200)
class CableTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
devicetype.save()
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
devicerole.save()
device1 = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
device1.save()
device2 = Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole)
device2.save()
iface1 = Interface(device=device1, name='Interface 1', form_factor=IFACE_FF_1GE_FIXED)
iface1.save()
iface2 = Interface(device=device1, name='Interface 2', form_factor=IFACE_FF_1GE_FIXED)
iface2.save()
iface3 = Interface(device=device1, name='Interface 3', form_factor=IFACE_FF_1GE_FIXED)
iface3.save()
iface4 = Interface(device=device2, name='Interface 1', form_factor=IFACE_FF_1GE_FIXED)
iface4.save()
iface5 = Interface(device=device2, name='Interface 2', form_factor=IFACE_FF_1GE_FIXED)
iface5.save()
iface6 = Interface(device=device2, name='Interface 3', form_factor=IFACE_FF_1GE_FIXED)
iface6.save()
Cable(termination_a=iface1, termination_b=iface4, type=CABLE_TYPE_CAT6).save()
Cable(termination_a=iface2, termination_b=iface5, type=CABLE_TYPE_CAT6).save()
Cable(termination_a=iface3, termination_b=iface6, type=CABLE_TYPE_CAT6).save()
def test_cable_list(self):
url = reverse('dcim:cable_list')
params = {
"type": CABLE_TYPE_CAT6,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_cable(self):
cable = Cable.objects.first()
response = self.client.get(cable.get_absolute_url())
self.assertEqual(response.status_code, 200)
class VirtualMachineTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site.objects.create(name='Site 1', slug='site-1')
manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1')
device_type = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
device_role = DeviceRole.objects.create(
name='Device Role', slug='device-role-1'
)
# Create 9 member Devices
device1 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 1', site=site
)
device2 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 2', site=site
)
device3 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 3', site=site
)
device4 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 4', site=site
)
device5 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 5', site=site
)
device6 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 6', site=site
)
# Create three VirtualChassis with two members each
vc1 = VirtualChassis.objects.create(master=device1, domain='test-domain-1')
Device.objects.filter(pk=device2.pk).update(virtual_chassis=vc1, vc_position=2)
vc2 = VirtualChassis.objects.create(master=device3, domain='test-domain-2')
Device.objects.filter(pk=device4.pk).update(virtual_chassis=vc2, vc_position=2)
vc3 = VirtualChassis.objects.create(master=device5, domain='test-domain-3')
Device.objects.filter(pk=device6.pk).update(virtual_chassis=vc3, vc_position=2)
def test_virtualchassis_list(self):
url = reverse('dcim:virtualchassis_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_virtualchassis(self):
virtualchassis = VirtualChassis.objects.first()
response = self.client.get(virtualchassis.get_absolute_url())
self.assertEqual(response.status_code, 200)

View File

@ -1,5 +1,6 @@
import re import re
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.paginator import EmptyPage, PageNotAnInteger from django.core.paginator import EmptyPage, PageNotAnInteger
@ -353,8 +354,9 @@ class RackElevationListView(View):
total_count = racks.count() total_count = racks.count()
# Pagination # Pagination
paginator = EnhancedPaginator(racks, 25) per_page = request.GET.get('per_page', settings.PAGINATE_COUNT)
page_number = request.GET.get('page', 1) page_number = request.GET.get('page', 1)
paginator = EnhancedPaginator(racks, per_page)
try: try:
page = paginator.page(page_number) page = paginator.page(page_number)
except PageNotAnInteger: except PageNotAnInteger:

View File

@ -22,6 +22,7 @@ class ExtrasConfig(AppConfig):
port=settings.REDIS_PORT, port=settings.REDIS_PORT,
db=settings.REDIS_DATABASE, db=settings.REDIS_DATABASE,
password=settings.REDIS_PASSWORD or None, password=settings.REDIS_PASSWORD or None,
ssl=settings.REDIS_SSL,
) )
rs.ping() rs.ping()
except redis.exceptions.ConnectionError: except redis.exceptions.ConnectionError:

View File

@ -11,8 +11,8 @@ from taggit.models import Tag
from dcim.models import DeviceRole, Platform, Region, Site from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from utilities.forms import ( from utilities.forms import (
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect, FilterChoiceField, add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect,
FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField, FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
) )
from .constants import ( from .constants import (
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
@ -221,10 +221,6 @@ class TagFilterForm(BootstrapMixin, forms.Form):
# #
class ConfigContextForm(BootstrapMixin, forms.ModelForm): class ConfigContextForm(BootstrapMixin, forms.ModelForm):
regions = TreeNodeMultipleChoiceField(
queryset=Region.objects.all(),
required=False
)
data = JSONField() data = JSONField()
class Meta: class Meta:
@ -233,6 +229,26 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups',
'tenants', 'data', 'tenants', 'data',
] ]
widgets = {
'regions': APISelectMultiple(
api_url="/api/dcim/regions/"
),
'sites': APISelectMultiple(
api_url="/api/dcim/sites/"
),
'roles': APISelectMultiple(
api_url="/api/dcim/device-roles/"
),
'platforms': APISelectMultiple(
api_url="/api/dcim/platforms/"
),
'tenant_groups': APISelectMultiple(
api_url="/api/tenancy/tenant-groups/"
),
'tenants': APISelectMultiple(
api_url="/api/tenancy/tenants/"
)
}
class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm): class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
@ -264,29 +280,53 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
required=False, required=False,
label='Search' label='Search'
) )
region = FilterTreeNodeMultipleChoiceField( region = FilterChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug' to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
)
) )
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug' to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
) )
role = FilterChoiceField( role = FilterChoiceField(
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
to_field_name='slug' to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/device-roles/",
value_field="slug",
)
) )
platform = FilterChoiceField( platform = FilterChoiceField(
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
to_field_name='slug' to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/platforms/",
value_field="slug",
)
) )
tenant_group = FilterChoiceField( tenant_group = FilterChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
to_field_name='slug' to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/tenancy/tenant-groups/",
value_field="slug",
)
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug' to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
)
) )

View File

@ -29,7 +29,11 @@ def cache_changed_object(instance, **kwargs):
def _record_object_deleted(request, instance, **kwargs): def _record_object_deleted(request, instance, **kwargs):
# Record that the object was deleted. # Force resolution of request.user in case it's still a SimpleLazyObject. This seems to happen
# occasionally during tests, but haven't been able to determine why.
assert request.user.is_authenticated
# Record that the object was deleted
if hasattr(instance, 'log_change'): if hasattr(instance, 'log_change'):
instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE) instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE)

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1.7 on 2019-03-05 18:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('extras', '0016_exporttemplate_add_cable'),
]
operations = [
migrations.AlterField(
model_name='exporttemplate',
name='mime_type',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@ -357,7 +357,7 @@ class ExportTemplate(models.Model):
) )
template_code = models.TextField() template_code = models.TextField()
mime_type = models.CharField( mime_type = models.CharField(
max_length=15, max_length=50,
blank=True blank=True
) )
file_extension = models.CharField( file_extension = models.CharField(

View File

@ -68,7 +68,7 @@ class TagTable(BaseTable):
) )
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=TAG_ACTIONS, template_code=TAG_ACTIONS,
attrs={'td': {'class': 'text-right'}}, attrs={'td': {'class': 'text-right noprint'}},
verbose_name='' verbose_name=''
) )

View File

@ -0,0 +1,105 @@
import urllib.parse
import uuid
from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.urls import reverse
from taggit.models import Tag
from dcim.models import Site
from extras.models import ConfigContext, ObjectChange
class TagTestCase(TestCase):
def setUp(self):
self.client = Client()
Tag.objects.bulk_create([
Tag(name='Tag 1', slug='tag-1'),
Tag(name='Tag 2', slug='tag-2'),
Tag(name='Tag 3', slug='tag-3'),
])
def test_tag_list(self):
url = reverse('extras:tag_list')
params = {
"q": "tag",
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
class ConfigContextTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
# Create three ConfigContexts
for i in range(1, 4):
configcontext = ConfigContext(
name='Config Context {}'.format(i),
data='{{"foo": {}}}'.format(i)
)
configcontext.save()
configcontext.sites.add(site)
def test_configcontext_list(self):
url = reverse('extras:configcontext_list')
params = {
"q": "foo",
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_configcontext(self):
configcontext = ConfigContext.objects.first()
response = self.client.get(configcontext.get_absolute_url())
self.assertEqual(response.status_code, 200)
class ObjectChangeTestCase(TestCase):
def setUp(self):
self.client = Client()
user = User(username='testuser', email='testuser@example.com')
user.save()
site = Site(name='Site 1', slug='site-1')
site.save()
# Create three ObjectChanges
for i in range(1, 4):
site.log_change(
user=user,
request_id=uuid.uuid4(),
action=2
)
def test_objectchange_list(self):
url = reverse('extras:objectchange_list')
params = {
"user": User.objects.first(),
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_objectchange(self):
objectchange = ObjectChange.objects.first()
response = self.client.get(objectchange.get_absolute_url())
self.assertEqual(response.status_code, 200)

View File

@ -349,11 +349,11 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class PrefixCSVForm(forms.ModelForm): class PrefixCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField( vrf = FlexibleModelChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False,
to_field_name='rd', to_field_name='rd',
help_text='Route distinguisher of parent VRF', required=False,
help_text='Route distinguisher of parent VRF (or {ID})',
error_messages={ error_messages={
'invalid_choice': 'VRF not found.', 'invalid_choice': 'VRF not found.',
} }
@ -764,11 +764,11 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class IPAddressCSVForm(forms.ModelForm): class IPAddressCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField( vrf = FlexibleModelChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False,
to_field_name='rd', to_field_name='rd',
help_text='Route distinguisher of the assigned VRF', required=False,
help_text='Route distinguisher of parent VRF (or {ID})',
error_messages={ error_messages={
'invalid_choice': 'VRF not found.', 'invalid_choice': 'VRF not found.',
} }

View File

@ -1,7 +1,7 @@
import netaddr import netaddr
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
@ -10,8 +10,9 @@ from django.urls import reverse
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from dcim.models import Interface from dcim.models import Interface
from extras.models import CustomFieldModel from extras.models import CustomFieldModel, ObjectChange
from utilities.models import ChangeLoggedModel from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object
from .constants import * from .constants import *
from .fields import IPNetworkField, IPAddressField from .fields import IPNetworkField, IPAddressField
from .querysets import PrefixQuerySet from .querysets import PrefixQuerySet
@ -629,6 +630,27 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
self.family = self.address.version self.family = self.address.version
super().save(*args, **kwargs) super().save(*args, **kwargs)
def log_change(self, user, request_id, action):
"""
Include the connected Interface (if any).
"""
# It's possible that an IPAddress can be deleted _after_ its parent Interface, in which case trying to resolve
# the interface will raise DoesNotExist.
try:
parent_obj = self.interface
except ObjectDoesNotExist:
parent_obj = None
ObjectChange(
user=user,
request_id=request_id,
changed_object=self,
related_object=parent_obj,
action=action,
object_data=serialize_object(self)
).save()
def to_csv(self): def to_csv(self):
# Determine if this IP is primary for a Device # Determine if this IP is primary for a Device

View File

@ -203,7 +203,7 @@ class RIRTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name') name = tables.LinkColumn(verbose_name='Name')
is_private = BooleanColumn(verbose_name='Private') is_private = BooleanColumn(verbose_name='Private')
aggregate_count = tables.Column(verbose_name='Aggregates') aggregate_count = tables.Column(verbose_name='Aggregates')
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='') actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RIR model = RIR
@ -288,7 +288,7 @@ class RoleTable(BaseTable):
orderable=False, orderable=False,
verbose_name='VLANs' verbose_name='VLANs'
) )
actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='') actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Role model = Role
@ -392,7 +392,7 @@ class VLANGroupTable(BaseTable):
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
vlan_count = tables.Column(verbose_name='VLANs') vlan_count = tables.Column(verbose_name='VLANs')
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}}, actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right noprint'}},
verbose_name='') verbose_name='')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -437,7 +437,7 @@ class VLANMemberTable(BaseTable):
) )
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=VLAN_MEMBER_ACTIONS, template_code=VLAN_MEMBER_ACTIONS,
attrs={'td': {'class': 'text-right'}}, attrs={'td': {'class': 'text-right noprint'}},
verbose_name='' verbose_name=''
) )

View File

@ -0,0 +1,282 @@
from netaddr import IPNetwork
import urllib.parse
from django.test import Client, TestCase
from django.urls import reverse
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from ipam.constants import IP_PROTOCOL_TCP
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
class VRFTestCase(TestCase):
def setUp(self):
self.client = Client()
VRF.objects.bulk_create([
VRF(name='VRF 1', rd='65000:1'),
VRF(name='VRF 2', rd='65000:2'),
VRF(name='VRF 3', rd='65000:3'),
])
def test_vrf_list(self):
url = reverse('ipam:vrf_list')
params = {
"q": "65000",
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_configcontext(self):
vrf = VRF.objects.first()
response = self.client.get(vrf.get_absolute_url())
self.assertEqual(response.status_code, 200)
class RIRTestCase(TestCase):
def setUp(self):
self.client = Client()
RIR.objects.bulk_create([
RIR(name='RIR 1', slug='rir-1'),
RIR(name='RIR 2', slug='rir-2'),
RIR(name='RIR 3', slug='rir-3'),
])
def test_rir_list(self):
url = reverse('ipam:rir_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_rir(self):
rir = RIR.objects.first()
response = self.client.get(rir.get_absolute_url())
self.assertEqual(response.status_code, 200)
class AggregateTestCase(TestCase):
def setUp(self):
self.client = Client()
rir = RIR(name='RIR 1', slug='rir-1')
rir.save()
Aggregate.objects.bulk_create([
Aggregate(family=4, prefix=IPNetwork('10.1.0.0/16'), rir=rir),
Aggregate(family=4, prefix=IPNetwork('10.2.0.0/16'), rir=rir),
Aggregate(family=4, prefix=IPNetwork('10.3.0.0/16'), rir=rir),
])
def test_aggregate_list(self):
url = reverse('ipam:aggregate_list')
params = {
"rir": RIR.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_aggregate(self):
aggregate = Aggregate.objects.first()
response = self.client.get(aggregate.get_absolute_url())
self.assertEqual(response.status_code, 200)
class RoleTestCase(TestCase):
def setUp(self):
self.client = Client()
Role.objects.bulk_create([
Role(name='Role 1', slug='role-1'),
Role(name='Role 2', slug='role-2'),
Role(name='Role 3', slug='role-3'),
])
def test_role_list(self):
url = reverse('ipam:role_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class PrefixTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
Prefix.objects.bulk_create([
Prefix(family=4, prefix=IPNetwork('10.1.0.0/16'), site=site),
Prefix(family=4, prefix=IPNetwork('10.2.0.0/16'), site=site),
Prefix(family=4, prefix=IPNetwork('10.3.0.0/16'), site=site),
])
def test_prefix_list(self):
url = reverse('ipam:prefix_list')
params = {
"site": Site.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_prefix(self):
prefix = Prefix.objects.first()
response = self.client.get(prefix.get_absolute_url())
self.assertEqual(response.status_code, 200)
class IPAddressTestCase(TestCase):
def setUp(self):
self.client = Client()
vrf = VRF(name='VRF 1', rd='65000:1')
vrf.save()
IPAddress.objects.bulk_create([
IPAddress(family=4, address=IPNetwork('10.1.0.0/16'), vrf=vrf),
IPAddress(family=4, address=IPNetwork('10.2.0.0/16'), vrf=vrf),
IPAddress(family=4, address=IPNetwork('10.3.0.0/16'), vrf=vrf),
])
def test_ipaddress_list(self):
url = reverse('ipam:ipaddress_list')
params = {
"vrf": VRF.objects.first().rd,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_ipaddress(self):
ipaddress = IPAddress.objects.first()
response = self.client.get(ipaddress.get_absolute_url())
self.assertEqual(response.status_code, 200)
class VLANGroupTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
VLANGroup.objects.bulk_create([
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=site),
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=site),
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=site),
])
def test_vlangroup_list(self):
url = reverse('ipam:vlangroup_list')
params = {
"site": Site.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
class VLANTestCase(TestCase):
def setUp(self):
self.client = Client()
vlangroup = VLANGroup(name='VLAN Group 1', slug='vlan-group-1')
vlangroup.save()
VLAN.objects.bulk_create([
VLAN(group=vlangroup, vid=101, name='VLAN101'),
VLAN(group=vlangroup, vid=102, name='VLAN102'),
VLAN(group=vlangroup, vid=103, name='VLAN103'),
])
def test_vlan_list(self):
url = reverse('ipam:vlan_list')
params = {
"group": VLANGroup.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_vlan(self):
vlan = VLAN.objects.first()
response = self.client.get(vlan.get_absolute_url())
self.assertEqual(response.status_code, 200)
class ServiceTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1')
devicetype.save()
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
devicerole.save()
device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
device.save()
Service.objects.bulk_create([
Service(device=device, name='Service 1', protocol=IP_PROTOCOL_TCP, port=101),
Service(device=device, name='Service 2', protocol=IP_PROTOCOL_TCP, port=102),
Service(device=device, name='Service 3', protocol=IP_PROTOCOL_TCP, port=103),
])
def test_service_list(self):
url = reverse('ipam:service_list')
params = {
"device_id": Device.objects.first(),
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_service(self):
service = Service.objects.first()
response = self.client.get(service.get_absolute_url())
self.assertEqual(response.status_code, 200)

View File

@ -132,6 +132,7 @@ REDIS = {
'PASSWORD': '', 'PASSWORD': '',
'DATABASE': 0, 'DATABASE': 0,
'DEFAULT_TIMEOUT': 300, 'DEFAULT_TIMEOUT': 300,
'SSL': False,
} }
# The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of # The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of

View File

@ -22,7 +22,7 @@ except ImportError:
) )
VERSION = '2.5.7-dev' VERSION = '2.5.9-dev'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -131,6 +131,7 @@ REDIS_PORT = REDIS.get('PORT', 6379)
REDIS_PASSWORD = REDIS.get('PASSWORD', '') REDIS_PASSWORD = REDIS.get('PASSWORD', '')
REDIS_DATABASE = REDIS.get('DATABASE', 0) REDIS_DATABASE = REDIS.get('DATABASE', 0)
REDIS_DEFAULT_TIMEOUT = REDIS.get('DEFAULT_TIMEOUT', 300) REDIS_DEFAULT_TIMEOUT = REDIS.get('DEFAULT_TIMEOUT', 300)
REDIS_SSL = REDIS.get('SSL', False)
# Email # Email
EMAIL_HOST = EMAIL.get('SERVER') EMAIL_HOST = EMAIL.get('SERVER')
@ -197,7 +198,7 @@ ROOT_URLCONF = 'netbox.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR + '/templates/'], 'DIRS': [BASE_DIR + '/templates'],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@ -223,7 +224,7 @@ USE_I18N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
STATIC_ROOT = BASE_DIR + '/static/' STATIC_ROOT = BASE_DIR + '/static'
STATIC_URL = '/{}static/'.format(BASE_PATH) STATIC_URL = '/{}static/'.format(BASE_PATH)
STATICFILES_DIRS = ( STATICFILES_DIRS = (
os.path.join(BASE_DIR, "project-static"), os.path.join(BASE_DIR, "project-static"),
@ -291,6 +292,7 @@ RQ_QUEUES = {
'DB': REDIS_DATABASE, 'DB': REDIS_DATABASE,
'PASSWORD': REDIS_PASSWORD, 'PASSWORD': REDIS_PASSWORD,
'DEFAULT_TIMEOUT': REDIS_DEFAULT_TIMEOUT, 'DEFAULT_TIMEOUT': REDIS_DEFAULT_TIMEOUT,
'SSL': REDIS_SSL,
} }
} }
@ -315,6 +317,7 @@ SWAGGER_SETTINGS = {
'utilities.custom_inspectors.IdInFilterInspector', 'utilities.custom_inspectors.IdInFilterInspector',
'drf_yasg.inspectors.CoreAPICompatInspector', 'drf_yasg.inspectors.CoreAPICompatInspector',
], ],
'DEFAULT_MODEL_DEPTH': 1,
'DEFAULT_PAGINATOR_INSPECTORS': [ 'DEFAULT_PAGINATOR_INSPECTORS': [
'utilities.custom_inspectors.NullablePaginatorInspector', 'utilities.custom_inspectors.NullablePaginatorInspector',
'drf_yasg.inspectors.DjangoRestResponsePagination', 'drf_yasg.inspectors.DjangoRestResponsePagination',

View File

@ -267,6 +267,7 @@ class SearchView(View):
class APIRootView(APIView): class APIRootView(APIView):
_ignore_model_permissions = True _ignore_model_permissions = True
exclude_from_schema = True exclude_from_schema = True
swagger_schema = None
def get_view_name(self): def get_view_name(self):
return "API Root" return "API Root"

File diff suppressed because one or more lines are too long

View File

@ -49,6 +49,19 @@ footer p {
} }
} }
/* Printer friendly CSS class and various fixes for printing. */
@media print {
body {
padding-top: 0px;
}
a[href]:after {
content: none !important;
}
.noprint {
display: none !important;
}
}
/* Collapse the nav menu on displays less than 960px wide */ /* Collapse the nav menu on displays less than 960px wide */
@media (max-width: 959px) { @media (max-width: 959px) {
.navbar-header { .navbar-header {
@ -575,4 +588,4 @@ td .progress {
} }
textarea { textarea {
font-family: Consolas, Lucida Console, monospace; font-family: Consolas, Lucida Console, monospace;
} }

View File

@ -90,6 +90,10 @@ $(document).ready(function() {
// Assign color picker selection classes // Assign color picker selection classes
function colorPickerClassCopy(data, container) { function colorPickerClassCopy(data, container) {
if (data.element) { if (data.element) {
// Remove any existing color-selection classes
$(container).attr('class', function(i, c) {
return c.replace(/(^|\s)color-selection-\S+/g, '');
});
$(container).addClass($(data.element).attr("class")); $(container).addClass($(data.element).attr("class"));
} }
return data.text; return data.text;
@ -151,10 +155,13 @@ $(document).ready(function() {
filter_for_elements.each(function(index, filter_for_element) { filter_for_elements.each(function(index, filter_for_element) {
var param_name = $(filter_for_element).attr(attr_name); var param_name = $(filter_for_element).attr(attr_name);
var is_nullable = $(filter_for_element).attr("nullable");
var value = $(filter_for_element).val(); var value = $(filter_for_element).val();
if (param_name && value) { if (param_name && value) {
parameters[param_name] = value; parameters[param_name] = value;
} else if (param_name && is_nullable) {
parameters[param_name] = "null";
} }
}); });

View File

@ -1,4 +1,6 @@
$(document).ready(function() { $(document).ready(function() {
// Instantiate ClipboardJS on all copy buttons
new ClipboardJS('button.copy-secret');
// Unlocking a secret // Unlocking a secret
$('button.unlock-secret').click(function(event) { $('button.unlock-secret').click(function(event) {
@ -45,6 +47,7 @@ $(document).ready(function() {
console.log("Secret retrieved successfully"); console.log("Secret retrieved successfully");
$('#secret_' + secret_id).text(response.plaintext); $('#secret_' + secret_id).text(response.plaintext);
$('button.unlock-secret[secret-id=' + secret_id + ']').hide(); $('button.unlock-secret[secret-id=' + secret_id + ']').hide();
$('button.copy-secret[secret-id=' + secret_id + ']').show();
$('button.lock-secret[secret-id=' + secret_id + ']').show(); $('button.lock-secret[secret-id=' + secret_id + ']').show();
} else { } else {
console.log("Secret was not decrypted. Prompt user for private key."); console.log("Secret was not decrypted. Prompt user for private key.");
@ -67,6 +70,7 @@ $(document).ready(function() {
var secret_div = $('#secret_' + secret_id); var secret_div = $('#secret_' + secret_id);
secret_div.html('********'); secret_div.html('********');
$('button.lock-secret[secret-id=' + secret_id + ']').hide(); $('button.lock-secret[secret-id=' + secret_id + ']').hide();
$('button.copy-secret[secret-id=' + secret_id + ']').hide();
$('button.unlock-secret[secret-id=' + secret_id + ']').show(); $('button.unlock-secret[secret-id=' + secret_id + ']').show();
} }

View File

@ -23,7 +23,7 @@ class SecretRoleTable(BaseTable):
secret_count = tables.Column(verbose_name='Secrets') secret_count = tables.Column(verbose_name='Secrets')
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='' template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):

View File

@ -0,0 +1,82 @@
import urllib.parse
from django.contrib.auth import get_user_model
from django.test import Client, TestCase
from django.urls import reverse
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from secrets.models import Secret, SecretRole
class SecretRoleTestCase(TestCase):
def setUp(self):
TEST_USERNAME = 'testuser'
TEST_PASSWORD = 'testpassword'
User = get_user_model()
User.objects.create(username=TEST_USERNAME, email='testuser@example.com', password=TEST_PASSWORD)
self.client = Client()
self.client.login(username=TEST_USERNAME, password=TEST_PASSWORD)
SecretRole.objects.bulk_create([
SecretRole(name='Secret Role 1', slug='secret-role-1'),
SecretRole(name='Secret Role 2', slug='secret-role-2'),
SecretRole(name='Secret Role 3', slug='secret-role-3'),
])
def test_secretrole_list(self):
url = reverse('secrets:secret_list')
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200)
class SecretTestCase(TestCase):
def setUp(self):
self.client = Client()
site = Site(name='Site 1', slug='site-1')
site.save()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1')
devicetype.save()
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
devicerole.save()
device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
device.save()
secretrole = SecretRole(name='Secret Role 1', slug='secret-role-1')
secretrole.save()
Secret.objects.bulk_create([
Secret(device=device, role=secretrole, name='Secret 1', ciphertext=b'1234567890'),
Secret(device=device, role=secretrole, name='Secret 2', ciphertext=b'1234567890'),
Secret(device=device, role=secretrole, name='Secret 3', ciphertext=b'1234567890'),
])
def test_secret_list(self):
url = reverse('secrets:secret_list')
params = {
"role": SecretRole.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True)
self.assertEqual(response.status_code, 200)
def test_configcontext(self):
secret = Secret.objects.first()
response = self.client.get(secret.get_absolute_url(), follow=True)
self.assertEqual(response.status_code, 200)

View File

@ -54,7 +54,7 @@
<div class="col-xs-4 text-center"> <div class="col-xs-4 text-center">
<p class="text-muted">{% now 'Y-m-d H:i:s T' %}</p> <p class="text-muted">{% now 'Y-m-d H:i:s T' %}</p>
</div> </div>
<div class="col-xs-4 text-right"> <div class="col-xs-4 text-right noprint">
<p class="text-muted"> <p class="text-muted">
<i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/">Docs</a> &middot; <i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/">Docs</a> &middot;
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'api_docs' %}">API</a> &middot; <i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'api_docs' %}">API</a> &middot;
@ -69,6 +69,7 @@
<script src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script> <script src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
<script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script> <script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script>
<script src="{% static 'select2-4.0.5/js/select2.min.js' %}"></script> <script src="{% static 'select2-4.0.5/js/select2.min.js' %}"></script>
<script src="{% static 'clipboard-2.0.4.min.js' %}"></script>
<script src="{% static 'js/forms.js' %}?v{{ settings.VERSION }}"></script> <script src="{% static 'js/forms.js' %}?v{{ settings.VERSION }}"></script>
<script type="text/javascript"> <script type="text/javascript">
var netbox_api_path = "/{{ settings.BASE_PATH }}api/"; var netbox_api_path = "/{{ settings.BASE_PATH }}api/";

View File

@ -4,7 +4,7 @@
{% block title %}{{ circuit }}{% endblock %} {% block title %}{{ circuit }}{% endblock %}
{% block header %} {% block header %}
<div class="row"> <div class="row noprint">
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'circuits:circuit_list' %}">Circuits</a></li> <li><a href="{% url 'circuits:circuit_list' %}">Circuits</a></li>
@ -25,7 +25,7 @@
</form> </form>
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.circuits.change_circuit %} {% if perms.circuits.change_circuit %}
<a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning"> <a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span> <span class="fa fa-pencil" aria-hidden="true"></span>

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.circuits.add_circuit %} {% if perms.circuits.add_circuit %}
{% add_button 'circuits:circuit_add' %} {% add_button 'circuits:circuit_add' %}
{% import_button 'circuits:circuit_import' %} {% import_button 'circuits:circuit_import' %}
@ -14,7 +14,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %} {% include 'inc/tags_panel.html' %}
</div> </div>

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.circuits.add_circuittype %} {% if perms.circuits.add_circuittype %}
{% add_button 'circuits:circuittype_add' %} {% add_button 'circuits:circuittype_add' %}
{% import_button 'circuits:circuittype_import' %} {% import_button 'circuits:circuittype_import' %}

View File

@ -5,7 +5,7 @@
{% block title %}{{ provider }}{% endblock %} {% block title %}{{ provider }}{% endblock %}
{% block header %} {% block header %}
<div class="row"> <div class="row noprint">
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'circuits:provider_list' %}">Providers</a></li> <li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
@ -25,7 +25,7 @@
</form> </form>
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right noprint">
{% if show_graphs %} {% if show_graphs %}
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider-graphs' pk=provider.pk %}" title="Show graphs"> <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider-graphs' pk=provider.pk %}" title="Show graphs">
<i class="fa fa-signal" aria-hidden="true"></i> <i class="fa fa-signal" aria-hidden="true"></i>
@ -85,11 +85,11 @@
</tr> </tr>
<tr> <tr>
<td>NOC Contact</td> <td>NOC Contact</td>
<td>{{ provider.noc_contact|linebreaksbr|placeholder }}</td> <td class="rendered-markdown">{{ provider.noc_contact|gfm|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<td>Admin Contact</td> <td>Admin Contact</td>
<td>{{ provider.admin_contact|linebreaksbr|placeholder }}</td> <td class="rendered-markdown">{{ provider.admin_contact|gfm|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<td>Circuits</td> <td>Circuits</td>
@ -172,7 +172,7 @@
{% endfor %} {% endfor %}
</table> </table>
{% if perms.circuits.add_circuit %} {% if perms.circuits.add_circuit %}
<div class="panel-footer text-right"> <div class="panel-footer text-right noprint">
<a href="{% url 'circuits:circuit_add' %}?provider={{ provider.pk }}" class="btn btn-xs btn-primary"> <a href="{% url 'circuits:circuit_add' %}?provider={{ provider.pk }}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add circuit <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add circuit
</a> </a>

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.circuits.add_provider %} {% if perms.circuits.add_provider %}
{% add_button 'circuits:provider_add' %} {% add_button 'circuits:provider_add' %}
{% import_button 'circuits:provider_import' %} {% import_button 'circuits:provider_import' %}
@ -14,7 +14,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %} {% include 'inc/tags_panel.html' %}
</div> </div>

View File

@ -2,7 +2,7 @@
{% load helpers %} {% load helpers %}
{% block header %} {% block header %}
<div class="row"> <div class="row noprint">
<div class="col-md-12"> <div class="col-md-12">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'dcim:cable_list' %}">Cables</a></li> <li><a href="{% url 'dcim:cable_list' %}">Cables</a></li>
@ -10,7 +10,7 @@
</ol> </ol>
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.change_cable %} {% if perms.dcim.change_cable %}
<a href="{% url 'dcim:cable_edit' pk=cable.pk %}" class="btn btn-warning"> <a href="{% url 'dcim:cable_edit' pk=cable.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span> Edit this cable <span class="fa fa-pencil" aria-hidden="true"></span> Edit this cable

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.add_cable %} {% if perms.dcim.add_cable %}
{% import_button 'dcim:cable_import' %} {% import_button 'dcim:cable_import' %}
{% endif %} {% endif %}
@ -13,7 +13,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:cable_bulk_edit' bulk_delete_url='dcim:cable_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:cable_bulk_edit' bulk_delete_url='dcim:cable_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% export_button content_type %} {% export_button content_type %}
</div> </div>
<h1>{% block title %}Console Connections{% endblock %}</h1> <h1>{% block title %}Console Connections{% endblock %}</h1>
@ -11,7 +11,7 @@
{% include 'responsive_table.html' %} {% include 'responsive_table.html' %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>

View File

@ -5,7 +5,7 @@
{% block title %}{{ device }}{% endblock %} {% block title %}{{ device }}{% endblock %}
{% block header %} {% block header %}
<div class="row"> <div class="row noprint">
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a></li> <li><a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a></li>
@ -33,7 +33,7 @@
</form> </form>
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.change_device %} {% if perms.dcim.change_device %}
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@ -199,7 +199,7 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
<div class="panel-footer text-right"> <div class="panel-footer text-right noprint">
{% if perms.dcim.change_virtualchassis %} {% if perms.dcim.change_virtualchassis %}
<a href="{% url 'dcim:virtualchassis_add_member' pk=device.virtual_chassis.pk %}?site={{ device.site.pk }}&rack={{ device.rack.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:virtualchassis_add_member' pk=device.virtual_chassis.pk %}?site={{ device.site.pk }}&rack={{ device.rack.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Member <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Member
@ -317,7 +317,7 @@
{% endfor %} {% endfor %}
</table> </table>
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %} {% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
<div class="panel-footer text-right"> <div class="panel-footer text-right noprint">
{% if perms.dcim.add_consoleport %} {% if perms.dcim.add_consoleport %}
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary"> <a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
@ -352,7 +352,7 @@
<form id="secret_form"> <form id="secret_form">
{% csrf_token %} {% csrf_token %}
</form> </form>
<div class="panel-footer text-right"> <div class="panel-footer text-right noprint">
<a href="{% url 'dcim:device_addsecret' pk=device.pk %}" class="btn btn-xs btn-primary"> <a href="{% url 'dcim:device_addsecret' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Add secret Add secret
@ -377,7 +377,7 @@
</div> </div>
{% endif %} {% endif %}
{% if perms.ipam.add_service %} {% if perms.ipam.add_service %}
<div class="panel-footer text-right"> <div class="panel-footer text-right noprint">
<a href="{% url 'dcim:device_service_assign' device=device.pk %}" class="btn btn-xs btn-primary"> <a href="{% url 'dcim:device_service_assign' device=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
</a> </a>
@ -390,7 +390,7 @@
</div> </div>
{% include 'inc/image_attachments.html' with images=device.images.all %} {% include 'inc/image_attachments.html' with images=device.images.all %}
{% if perms.extras.add_imageattachment %} {% if perms.extras.add_imageattachment %}
<div class="panel-footer text-right"> <div class="panel-footer text-right noprint">
<a href="{% url 'dcim:device_add_image' object_id=device.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:device_add_image' object_id=device.pk %}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Attach an image Attach an image
@ -398,7 +398,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default noprint">
<div class="panel-heading"> <div class="panel-heading">
<strong>Related Devices</strong> <strong>Related Devices</strong>
</div> </div>
@ -459,7 +459,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<div class="panel-footer"> <div class="panel-footer noprint">
{% if device_bays and perms.dcim.change_devicebay %} {% if device_bays and perms.dcim.change_devicebay %}
<button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
@ -493,7 +493,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Interfaces</strong> <strong>Interfaces</strong>
<div class="pull-right"> <div class="pull-right noprint">
<button class="btn btn-default btn-xs toggle-ips" selected="selected"> <button class="btn btn-default btn-xs toggle-ips" selected="selected">
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs <span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
</button> </button>
@ -521,7 +521,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<div class="panel-footer"> <div class="panel-footer noprint">
{% if interfaces and perms.dcim.change_interface %} {% if interfaces and perms.dcim.change_interface %}
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
@ -581,7 +581,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<div class="panel-footer"> <div class="panel-footer noprint">
{% if consoleserverports and perms.dcim.change_consoleport %} {% if consoleserverports and perms.dcim.change_consoleport %}
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
@ -636,7 +636,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<div class="panel-footer"> <div class="panel-footer noprint">
{% if poweroutlets and perms.dcim.change_powerport %} {% if poweroutlets and perms.dcim.change_powerport %}
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
@ -693,7 +693,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<div class="panel-footer"> <div class="panel-footer noprint">
{% if front_ports and perms.dcim.change_frontport %} {% if front_ports and perms.dcim.change_frontport %}
<button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
@ -750,7 +750,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<div class="panel-footer"> <div class="panel-footer noprint">
{% if rear_ports and perms.dcim.change_rearport %} {% if rear_ports and perms.dcim.change_rearport %}
<button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename

View File

@ -53,7 +53,7 @@
</tbody> </tbody>
</table> </table>
{% if perms.dcim.add_inventoryitem %} {% if perms.dcim.add_inventoryitem %}
<div class="panel-footer text-right"> <div class="panel-footer text-right noprint">
<a href="{% url 'dcim:inventoryitem_add' device=device.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:inventoryitem_add' device=device.pk %}" class="btn btn-primary btn-xs">
<span class="fa fa-plus" aria-hidden="true"></span> Add Inventory Item <span class="fa fa-plus" aria-hidden="true"></span> Add Inventory Item
</a> </a>

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.add_device %} {% if perms.dcim.add_device %}
{% add_button 'dcim:device_add' %} {% add_button 'dcim:device_add' %}
{% import_button 'dcim:device_import' %} {% import_button 'dcim:device_import' %}
@ -14,7 +14,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %} {% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %} {% include 'inc/tags_panel.html' %}
</div> </div>

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.add_devicerole %} {% if perms.dcim.add_devicerole %}
{% add_button 'dcim:devicerole_add' %} {% add_button 'dcim:devicerole_add' %}
{% import_button 'dcim:devicerole_import' %} {% import_button 'dcim:devicerole_import' %}

View File

@ -4,7 +4,7 @@
{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %} {% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}
{% block header %} {% block header %}
<div class="row"> <div class="row noprint">
<div class="col-md-12"> <div class="col-md-12">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'dcim:devicetype_list' %}">Device Types</a></li> <li><a href="{% url 'dcim:devicetype_list' %}">Device Types</a></li>
@ -14,7 +14,7 @@
</div> </div>
</div> </div>
{% if perms.dcim.change_devicetype or perms.dcim.delete_devicetype %} {% if perms.dcim.change_devicetype or perms.dcim.delete_devicetype %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.change_devicetype %} {% if perms.dcim.change_devicetype %}
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.add_devicetype %} {% if perms.dcim.add_devicetype %}
{% add_button 'dcim:devicetype_add' %} {% add_button 'dcim:devicetype_add' %}
{% import_button 'dcim:devicetype_import' %} {% import_button 'dcim:devicetype_import' %}
@ -14,7 +14,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:devicetype_bulk_edit' bulk_delete_url='dcim:devicetype_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:devicetype_bulk_edit' bulk_delete_url='dcim:devicetype_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %} {% include 'inc/tags_panel.html' %}
</div> </div>

View File

@ -29,7 +29,7 @@
{% endif %} {% endif %}
{# Actions #} {# Actions #}
<td class="text-right"> <td class="text-right noprint">
{% if cp.cable %} {% if cp.cable %}
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=cp.cable %} {% include 'dcim/inc/cable_toggle_buttons.html' with cable=cp.cable %}
{% elif perms.dcim.add_cable %} {% elif perms.dcim.add_cable %}

View File

@ -36,7 +36,7 @@
{% endif %} {% endif %}
{# Actions #} {# Actions #}
<td class="text-right"> <td class="text-right noprint">
{% if csp.cable %} {% if csp.cable %}
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=csp.cable %} {% include 'dcim/inc/cable_toggle_buttons.html' with cable=csp.cable %}
{% elif perms.dcim.add_cable %} {% elif perms.dcim.add_cable %}

View File

@ -23,7 +23,7 @@
<span class="text-muted">Vacant</span> <span class="text-muted">Vacant</span>
</td> </td>
{% endif %} {% endif %}
<td class="text-right"> <td class="text-right noprint">
{% if perms.dcim.change_devicebay %} {% if perms.dcim.change_devicebay %}
{% if devicebay.installed_device %} {% if devicebay.installed_device %}
<a href="{% url 'dcim:devicebay_depopulate' pk=devicebay.pk %}" class="btn btn-danger btn-xs"> <a href="{% url 'dcim:devicebay_depopulate' pk=devicebay.pk %}" class="btn btn-danger btn-xs">

View File

@ -6,7 +6,7 @@
<strong>{{ title }}</strong> <strong>{{ title }}</strong>
</div> </div>
{% include 'responsive_table.html' %} {% include 'responsive_table.html' %}
<div class="panel-footer"> <div class="panel-footer noprint">
{% if table.rows %} {% if table.rows %}
{% if edit_url %} {% if edit_url %}
<button type="submit" name="_edit" formaction="{% url edit_url pk=devicetype.pk %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning"> <button type="submit" name="_edit" formaction="{% url edit_url pk=devicetype.pk %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning">

View File

@ -54,7 +54,7 @@
{% endif %} {% endif %}
{# Actions #} {# Actions #}
<td class="text-right"> <td class="text-right noprint">
{% if frontport.cable %} {% if frontport.cable %}
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=frontport.cable %} {% include 'dcim/inc/cable_toggle_buttons.html' with cable=frontport.cable %}
{% elif perms.dcim.add_cable %} {% elif perms.dcim.add_cable %}

View File

@ -96,7 +96,7 @@
{{ peer_termination.connected_endpoint.device }} {{ peer_termination.connected_endpoint.device }}
</a><br/> </a><br/>
<small>via <i class="fa fa-fw fa-globe" title="Circuit"></i> <small>via <i class="fa fa-fw fa-globe" title="Circuit"></i>
<a href="{{ iface.connected_endpoint.circuit.get_absolure_url }}"> <a href="{{ iface.connected_endpoint.circuit.get_absolute_url }}">
{{ iface.connected_endpoint.circuit.provider }} {{ iface.connected_endpoint.circuit.provider }}
{{ iface.connected_endpoint.circuit }} {{ iface.connected_endpoint.circuit }}
</a> </a>
@ -134,7 +134,7 @@
{% endif %} {% endif %}
{# Buttons #} {# Buttons #}
<td class="text-right text-nowrap"> <td class="text-right text-nowrap noprint">
{% if show_graphs %} {% if show_graphs %}
{% if iface.connected_endpoint %} {% if iface.connected_endpoint %}
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs"> <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs">
@ -231,7 +231,7 @@
</td> </td>
{# Buttons #} {# Buttons #}
<td class="text-right text-nowrap"> <td class="text-right text-nowrap noprint">
{% if perms.ipam.change_ipaddress %} {% if perms.ipam.change_ipaddress %}
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs"> <a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i> <i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>

View File

@ -6,7 +6,7 @@
<td>{{ item.serial }}</td> <td>{{ item.serial }}</td>
<td>{{ item.asset_tag|default:"" }}</td> <td>{{ item.asset_tag|default:"" }}</td>
<td>{{ item.description }}</td> <td>{{ item.description }}</td>
<td class="text-right"> <td class="text-right noprint">
{% if perms.dcim.change_inventoryitem %} {% if perms.dcim.change_inventoryitem %}
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a> <a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
{% endif %} {% endif %}

View File

@ -36,7 +36,7 @@
{% endif %} {% endif %}
{# Actions #} {# Actions #}
<td class="text-right"> <td class="text-right noprint">
{% if po.cable %} {% if po.cable %}
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=po.cable %} {% include 'dcim/inc/cable_toggle_buttons.html' with cable=po.cable %}
{% elif perms.dcim.add_cable %} {% elif perms.dcim.add_cable %}

View File

@ -29,7 +29,7 @@
{% endif %} {% endif %}
{# Actions #} {# Actions #}
<td class="text-right"> <td class="text-right noprint">
{% if pp.cable %} {% if pp.cable %}
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=pp.cable %} {% include 'dcim/inc/cable_toggle_buttons.html' with cable=pp.cable %}
{% elif perms.dcim.add_cable %} {% elif perms.dcim.add_cable %}

View File

@ -53,7 +53,7 @@
{% endif %} {% endif %}
{# Actions #} {# Actions #}
<td class="text-right"> <td class="text-right noprint">
{% if rearport.cable %} {% if rearport.cable %}
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=rearport.cable %} {% include 'dcim/inc/cable_toggle_buttons.html' with cable=rearport.cable %}
{% elif perms.dcim.add_cable %} {% elif perms.dcim.add_cable %}

View File

@ -2,7 +2,7 @@
{% load helpers %} {% load helpers %}
{% block header %} {% block header %}
<div class="row"> <div class="row noprint">
<div class="col-md-12"> <div class="col-md-12">
<ol class="breadcrumb"> <ol class="breadcrumb">
{% if interface.device %} {% if interface.device %}
@ -15,7 +15,7 @@
</ol> </ol>
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.change_interface %} {% if perms.dcim.change_interface %}
<a href="{% if interface.device %}{% url 'dcim:interface_edit' pk=interface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=interface.pk %}{% endif %}" class="btn btn-warning"> <a href="{% if interface.device %}{% url 'dcim:interface_edit' pk=interface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=interface.pk %}{% endif %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span> Edit this interface <span class="fa fa-pencil" aria-hidden="true"></span> Edit this interface

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% export_button content_type %} {% export_button content_type %}
</div> </div>
<h1>{% block title %}Interface Connections{% endblock %}</h1> <h1>{% block title %}Interface Connections{% endblock %}</h1>
@ -11,7 +11,7 @@
{% include 'responsive_table.html' %} {% include 'responsive_table.html' %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@
{% load helpers %} {% load helpers %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.add_devicetype %} {% if perms.dcim.add_devicetype %}
{% import_button 'dcim:inventoryitem_import' %} {% import_button 'dcim:inventoryitem_import' %}
{% endif %} {% endif %}
@ -14,7 +14,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:inventoryitem_bulk_edit' bulk_delete_url='dcim:inventoryitem_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:inventoryitem_bulk_edit' bulk_delete_url='dcim:inventoryitem_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.add_manufacturer %} {% if perms.dcim.add_manufacturer %}
{% add_button 'dcim:manufacturer_add' %} {% add_button 'dcim:manufacturer_add' %}
{% import_button 'dcim:manufacturer_import' %} {% import_button 'dcim:manufacturer_import' %}

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.add_platform %} {% if perms.dcim.add_platform %}
{% add_button 'dcim:platform_add' %} {% add_button 'dcim:platform_add' %}
{% import_button 'dcim:platform_import' %} {% import_button 'dcim:platform_import' %}

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% export_button content_type %} {% export_button content_type %}
</div> </div>
<h1>{% block title %}Power Connections{% endblock %}</h1> <h1>{% block title %}Power Connections{% endblock %}</h1>
@ -11,7 +11,7 @@
{% include 'responsive_table.html' %} {% include 'responsive_table.html' %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
{% load helpers %} {% load helpers %}
{% block header %} {% block header %}
<div class="row"> <div class="row noprint">
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'dcim:rack_list' %}">Racks</a></li> <li><a href="{% url 'dcim:rack_list' %}">Racks</a></li>
@ -23,7 +23,7 @@
</form> </form>
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right noprint">
<a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}"{% else %}disabled="disabled"{% endif %} class="btn btn-primary"> <a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}"{% else %}disabled="disabled"{% endif %} class="btn btn-primary">
<span class="fa fa-chevron-left" aria-hidden="true"></span> Previous Rack <span class="fa fa-chevron-left" aria-hidden="true"></span> Previous Rack
</a> </a>
@ -223,7 +223,7 @@
<div class="panel-body text-muted">None</div> <div class="panel-body text-muted">None</div>
{% endif %} {% endif %}
{% if perms.dcim.add_device %} {% if perms.dcim.add_device %}
<div class="panel-footer text-right"> <div class="panel-footer text-right noprint">
<a href="{% url 'dcim:device_add' %}?site={{ rack.site.pk }}&rack={{ rack.pk }}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:device_add' %}?site={{ rack.site.pk }}&rack={{ rack.pk }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Add a non-racked device Add a non-racked device
@ -237,7 +237,7 @@
</div> </div>
{% include 'inc/image_attachments.html' with images=rack.images.all %} {% include 'inc/image_attachments.html' with images=rack.images.all %}
{% if perms.extras.add_imageattachment %} {% if perms.extras.add_imageattachment %}
<div class="panel-footer text-right"> <div class="panel-footer text-right noprint">
<a href="{% url 'dcim:rack_add_image' object_id=rack.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:rack_add_image' object_id=rack.pk %}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Attach an image Attach an image
@ -271,7 +271,7 @@
{{ resv.description }}<br /> {{ resv.description }}<br />
<small>{{ resv.user }} &middot; {{ resv.created }}</small> <small>{{ resv.user }} &middot; {{ resv.created }}</small>
</td> </td>
<td class="text-right"> <td class="text-right noprint">
{% if perms.dcim.change_rackreservation %} {% if perms.dcim.change_rackreservation %}
<a href="{% url 'dcim:rackreservation_edit' pk=resv.pk %}" class="btn btn-warning btn-xs" title="Edit reservation"> <a href="{% url 'dcim:rackreservation_edit' pk=resv.pk %}" class="btn btn-warning btn-xs" title="Edit reservation">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i> <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
@ -290,7 +290,7 @@
<div class="panel-body text-muted">None</div> <div class="panel-body text-muted">None</div>
{% endif %} {% endif %}
{% if perms.dcim.add_rackreservation %} {% if perms.dcim.add_rackreservation %}
<div class="panel-footer text-right"> <div class="panel-footer text-right noprint">
<a href="{% url 'dcim:rack_add_reservation' rack=rack.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:rack_add_reservation' rack=rack.pk %}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Add a reservation Add a reservation

View File

@ -2,7 +2,7 @@
{% load helpers %} {% load helpers %}
{% block content %} {% block content %}
<div class="btn-group pull-right" role="group"> <div class="btn-group pull-right noprint" role="group">
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face=0 %}" class="btn btn-default{% if request.GET.face != '1' %} active{% endif %}">Front</a> <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face=0 %}" class="btn btn-default{% if request.GET.face != '1' %} active{% endif %}">Front</a>
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face=1 %}" class="btn btn-default{% if request.GET.face == '1' %} active{% endif %}">Rear</a> <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face=1 %}" class="btn btn-default{% if request.GET.face == '1' %} active{% endif %}">Rear</a>
</div> </div>
@ -38,7 +38,7 @@
<p>No racks found</p> <p>No racks found</p>
</div> </div>
{% endif %} {% endif %}
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.add_rack %} {% if perms.dcim.add_rack %}
{% add_button 'dcim:rack_add' %} {% add_button 'dcim:rack_add' %}
{% import_button 'dcim:rack_import' %} {% import_button 'dcim:rack_import' %}
@ -14,7 +14,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rack_bulk_edit' bulk_delete_url='dcim:rack_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rack_bulk_edit' bulk_delete_url='dcim:rack_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %} {% include 'inc/tags_panel.html' %}
</div> </div>

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.add_rackgroup %} {% if perms.dcim.add_rackgroup %}
{% add_button 'dcim:rackgroup_add' %} {% add_button 'dcim:rackgroup_add' %}
{% import_button 'dcim:rackgroup_import' %} {% import_button 'dcim:rackgroup_import' %}
@ -14,7 +14,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackgroup_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackgroup_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>

View File

@ -7,7 +7,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rackreservation_bulk_edit' bulk_delete_url='dcim:rackreservation_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rackreservation_bulk_edit' bulk_delete_url='dcim:rackreservation_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.add_rackrole %} {% if perms.dcim.add_rackrole %}
{% add_button 'dcim:rackrole_add' %} {% add_button 'dcim:rackrole_add' %}
{% import_button 'dcim:rackrole_import' %} {% import_button 'dcim:rackrole_import' %}

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.add_region %} {% if perms.dcim.add_region %}
{% add_button 'dcim:region_add' %} {% add_button 'dcim:region_add' %}
{% import_button 'dcim:region_import' %} {% import_button 'dcim:region_import' %}
@ -14,7 +14,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@
{% load helpers %} {% load helpers %}
{% block header %} {% block header %}
<div class="row"> <div class="row noprint">
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'dcim:site_list' %}">Sites</a></li> <li><a href="{% url 'dcim:site_list' %}">Sites</a></li>
@ -30,7 +30,7 @@
</form> </form>
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right noprint">
{% if show_graphs %} {% if show_graphs %}
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site-graphs' pk=site.pk %}" title="Show graphs"> <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site-graphs' pk=site.pk %}" title="Show graphs">
<i class="fa fa-signal" aria-hidden="true"></i> <i class="fa fa-signal" aria-hidden="true"></i>
@ -138,7 +138,7 @@
<td>Physical Address</td> <td>Physical Address</td>
<td> <td>
{% if site.physical_address %} {% if site.physical_address %}
<div class="pull-right"> <div class="pull-right noprint">
<a href="http://maps.google.com/?q={{ site.physical_address|oneline|urlencode }}" target="_blank" class="btn btn-primary btn-xs"> <a href="http://maps.google.com/?q={{ site.physical_address|oneline|urlencode }}" target="_blank" class="btn btn-primary btn-xs">
<i class="glyphicon glyphicon-map-marker"></i> Map it <i class="glyphicon glyphicon-map-marker"></i> Map it
</a> </a>
@ -157,7 +157,7 @@
<td>GPS Coordinates</td> <td>GPS Coordinates</td>
<td> <td>
{% if site.latitude and site.longitude %} {% if site.latitude and site.longitude %}
<div class="pull-right"> <div class="pull-right noprint">
<a href="http://maps.google.com/?q={{ site.latitude }},{{ site.longitude }}" target="_blank" class="btn btn-primary btn-xs"> <a href="http://maps.google.com/?q={{ site.latitude }},{{ site.longitude }}" target="_blank" class="btn btn-primary btn-xs">
<i class="glyphicon glyphicon-map-marker"></i> Map it <i class="glyphicon glyphicon-map-marker"></i> Map it
</a> </a>
@ -251,7 +251,7 @@
<tr> <tr>
<td><i class="fa fa-fw fa-folder-o"></i> <a href="{{ rg.get_absolute_url }}">{{ rg }}</a></td> <td><i class="fa fa-fw fa-folder-o"></i> <a href="{{ rg.get_absolute_url }}">{{ rg }}</a></td>
<td>{{ rg.rack_count }}</td> <td>{{ rg.rack_count }}</td>
<td class="text-right"> <td class="text-right noprint">
<a href="{% url 'dcim:rack_elevation_list' %}?group_id={{ rg.pk }}" class="btn btn-xs btn-primary" title="View elevations"> <a href="{% url 'dcim:rack_elevation_list' %}?group_id={{ rg.pk }}" class="btn btn-xs btn-primary" title="View elevations">
<i class="fa fa-eye"></i> <i class="fa fa-eye"></i>
</a> </a>
@ -271,7 +271,7 @@
</div> </div>
{% include 'inc/image_attachments.html' with images=site.images.all %} {% include 'inc/image_attachments.html' with images=site.images.all %}
{% if perms.extras.add_imageattachment %} {% if perms.extras.add_imageattachment %}
<div class="panel-footer text-right"> <div class="panel-footer text-right noprint">
<a href="{% url 'dcim:site_add_image' object_id=site.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:site_add_image' object_id=site.pk %}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Attach an image Attach an image

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.dcim.add_site %} {% if perms.dcim.add_site %}
{% add_button 'dcim:site_add' %} {% add_button 'dcim:site_add' %}
{% import_button 'dcim:site_import' %} {% import_button 'dcim:site_import' %}
@ -14,7 +14,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' %} {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %} {% include 'inc/tags_panel.html' %}
</div> </div>

View File

@ -3,7 +3,7 @@
{% load helpers %} {% load helpers %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% export_button content_type %} {% export_button content_type %}
</div> </div>
<h1>{% block title %}Virtual Chassis{% endblock %}</h1> <h1>{% block title %}Virtual Chassis{% endblock %}</h1>
@ -11,7 +11,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' %} {% include 'utilities/obj_table.html' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %} {% include 'inc/tags_panel.html' %}
</div> </div>

View File

@ -2,7 +2,7 @@
{% load helpers %} {% load helpers %}
{% block header %} {% block header %}
<div class="row"> <div class="row noprint">
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'extras:configcontext_list' %}">Config Contexts</a></li> <li><a href="{% url 'extras:configcontext_list' %}">Config Contexts</a></li>
@ -22,7 +22,7 @@
</form> </form>
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.extras.change_configcontext %} {% if perms.extras.change_configcontext %}
<a href="{% url 'extras:configcontext_edit' pk=configcontext.pk %}" class="btn btn-warning"> <a href="{% url 'extras:configcontext_edit' pk=configcontext.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.extras.add_configcontext %} {% if perms.extras.add_configcontext %}
{% add_button 'extras:configcontext_add' %} {% add_button 'extras:configcontext_add' %}
{% endif %} {% endif %}
@ -12,7 +12,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='extras:configcontext_bulk_edit' bulk_delete_url='extras:configcontext_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='extras:configcontext_bulk_edit' bulk_delete_url='extras:configcontext_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@
{% block title %}{{ objectchange }}{% endblock %} {% block title %}{{ objectchange }}{% endblock %}
{% block header %} {% block header %}
<div class="row"> <div class="row noprint">
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'extras:objectchange_list' %}">Changelog</a></li> <li><a href="{% url 'extras:objectchange_list' %}">Changelog</a></li>
@ -97,7 +97,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% include 'panel_table.html' with table=related_changes_table heading='Related Changes' %} {% include 'panel_table.html' with table=related_changes_table heading='Related Changes' panel_class='noprint' %}
{% if related_changes_count > related_changes_table.rows|length %} {% if related_changes_count > related_changes_table.rows|length %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'extras:objectchange_list' %}?request_id={{ objectchange.request_id }}" class="btn btn-primary">See all {{ related_changes_count|add:"1" }} changes</a> <a href="{% url 'extras:objectchange_list' %}?request_id={{ objectchange.request_id }}" class="btn btn-primary">See all {{ related_changes_count|add:"1" }} changes</a>

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% export_button content_type %} {% export_button content_type %}
</div> </div>
<h1>{% block title %}Changelog{% endblock %}</h1> <h1>{% block title %}Changelog{% endblock %}</h1>
@ -10,7 +10,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' %} {% include 'utilities/obj_table.html' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@
{% block title %}{{ report.name }}{% endblock %} {% block title %}{{ report.name }}{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row noprint">
<div class="col-md-12"> <div class="col-md-12">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'extras:report_list' %}">Reports</a></li> <li><a href="{% url 'extras:report_list' %}">Reports</a></li>
@ -14,7 +14,7 @@
</div> </div>
</div> </div>
{% if perms.extras.add_reportresult %} {% if perms.extras.add_reportresult %}
<div class="pull-right"> <div class="pull-right noprint">
<form action="{% url 'extras:report_run' name=report.full_name %}" method="post"> <form action="{% url 'extras:report_run' name=report.full_name %}" method="post">
{% csrf_token %} {% csrf_token %}
{{ run_form }} {{ run_form }}

View File

@ -29,6 +29,12 @@
Edit this tag Edit this tag
</a> </a>
{% endif %} {% endif %}
{% if perms.taggit.delete_tag %}
<a href="{% url 'extras:tag_delete' slug=tag.slug %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
Delete this tag
</a>
{% endif %}
</div> </div>
<h1>{% block title %}Tag: {{ tag }}{% endblock %}</h1> <h1>{% block title %}Tag: {{ tag }}{% endblock %}</h1>
{% endblock %} {% endblock %}

View File

@ -14,7 +14,7 @@
</td> </td>
<td>{{ attachment.size|filesizeformat }}</td> <td>{{ attachment.size|filesizeformat }}</td>
<td>{{ attachment.created }}</td> <td>{{ attachment.created }}</td>
<td class="text-right"> <td class="text-right noprint">
{% if perms.extras.change_imageattachment %} {% if perms.extras.change_imageattachment %}
<a href="{% url 'extras:imageattachment_edit' pk=attachment.pk %}" class="btn btn-warning btn-xs" title="Edit image"> <a href="{% url 'extras:imageattachment_edit' pk=attachment.pk %}" class="btn btn-warning btn-xs" title="Edit image">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i> <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>

View File

@ -26,7 +26,7 @@
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
<div class="text-right"> <div class="text-right noprint">
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span> Apply <span class="fa fa-search" aria-hidden="true"></span> Apply
</button> </button>

View File

@ -2,7 +2,7 @@
{% load helpers %} {% load helpers %}
{% block header %} {% block header %}
<div class="row"> <div class="row noprint">
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></li> <li><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></li>
@ -23,7 +23,7 @@
</form> </form>
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.ipam.change_aggregate %} {% if perms.ipam.change_aggregate %}
<a href="{% url 'ipam:aggregate_edit' pk=aggregate.pk %}" class="btn btn-warning"> <a href="{% url 'ipam:aggregate_edit' pk=aggregate.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span> <span class="fa fa-pencil" aria-hidden="true"></span>

View File

@ -3,7 +3,7 @@
{% load humanize %} {% load humanize %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.ipam.add_aggregate %} {% if perms.ipam.add_aggregate %}
{% add_button 'ipam:aggregate_add' %} {% add_button 'ipam:aggregate_add' %}
{% import_button 'ipam:aggregate_import' %} {% import_button 'ipam:aggregate_import' %}
@ -15,7 +15,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:aggregate_bulk_edit' bulk_delete_url='ipam:aggregate_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:aggregate_bulk_edit' bulk_delete_url='ipam:aggregate_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %} {% include 'inc/tags_panel.html' %}
<div class="panel panel-default"> <div class="panel panel-default">

View File

@ -13,7 +13,7 @@
{% endfor %} {% endfor %}
</td> </td>
<td>{{ service.description }}</td> <td>{{ service.description }}</td>
<td class="text-right"> <td class="text-right noprint">
<a href="{% url 'ipam:service_changelog' pk=service.pk %}" class="btn btn-default btn-xs" title="Changelog"> <a href="{% url 'ipam:service_changelog' pk=service.pk %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
</a> </a>

View File

@ -2,7 +2,7 @@
{% load helpers %} {% load helpers %}
{% block header %} {% block header %}
<div class="row"> <div class="row noprint">
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li> <li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li>
@ -25,7 +25,7 @@
</form> </form>
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.ipam.change_ipaddress %} {% if perms.ipam.change_ipaddress %}
<a href="{% url 'ipam:ipaddress_edit' pk=ipaddress.pk %}" class="btn btn-warning"> <a href="{% url 'ipam:ipaddress_edit' pk=ipaddress.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span> <span class="fa fa-pencil" aria-hidden="true"></span>
@ -150,7 +150,7 @@
{% if duplicate_ips_table.rows %} {% if duplicate_ips_table.rows %}
{% include 'panel_table.html' with table=duplicate_ips_table heading='Duplicate IP Addresses' panel_class='danger' %} {% include 'panel_table.html' with table=duplicate_ips_table heading='Duplicate IP Addresses' panel_class='danger' %}
{% endif %} {% endif %}
{% include 'panel_table.html' with table=related_ips_table heading='Related IP Addresses' panel_class='default' %} {% include 'panel_table.html' with table=related_ips_table heading='Related IP Addresses' panel_class='default noprint' %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.ipam.add_ipaddress %} {% if perms.ipam.add_ipaddress %}
{% add_button 'ipam:ipaddress_add' %} {% add_button 'ipam:ipaddress_add' %}
{% import_button 'ipam:ipaddress_import' %} {% import_button 'ipam:ipaddress_import' %}
@ -14,7 +14,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %} {% include 'inc/tags_panel.html' %}
</div> </div>

View File

@ -2,7 +2,7 @@
{% load helpers %} {% load helpers %}
{% block header %} {% block header %}
<div class="row"> <div class="row noprint">
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></li> <li><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></li>
@ -25,7 +25,7 @@
</form> </form>
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.ipam.add_prefix and active_tab == 'prefixes' and first_available_prefix %} {% if perms.ipam.add_prefix and active_tab == 'prefixes' and first_available_prefix %}
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}&vrf={{ prefix.vrf.pk }}&site={{ prefix.site.pk }}&tenant_group={{ prefix.tenant.group.pk }}&tenant={{ prefix.tenant.pk }}" class="btn btn-success"> <a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}&vrf={{ prefix.vrf.pk }}&site={{ prefix.site.pk }}&tenant_group={{ prefix.tenant.group.pk }}&tenant={{ prefix.tenant.pk }}" class="btn btn-success">
<i class="fa fa-plus" aria-hidden="true"></i> Add Child Prefix <i class="fa fa-plus" aria-hidden="true"></i> Add Child Prefix

View File

@ -3,7 +3,7 @@
{% load helpers %} {% load helpers %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<a href="{% url 'ipam:prefix_list' %}{% querystring request expand=None page=1 %}" class="btn btn-default{% if not request.GET.expand %} active{% endif %}">Collapse</a> <a href="{% url 'ipam:prefix_list' %}{% querystring request expand=None page=1 %}" class="btn btn-default{% if not request.GET.expand %} active{% endif %}">Collapse</a>
<a href="{% url 'ipam:prefix_list' %}{% querystring request expand='on' page=1 %}" class="btn btn-default{% if request.GET.expand %} active{% endif %}">Expand</a> <a href="{% url 'ipam:prefix_list' %}{% querystring request expand='on' page=1 %}" class="btn btn-default{% if request.GET.expand %} active{% endif %}">Expand</a>
@ -19,7 +19,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %} {% include 'inc/tags_panel.html' %}
</div> </div>

View File

@ -3,7 +3,7 @@
{% load humanize %} {% load humanize %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if request.GET.family == '6' %} {% if request.GET.family == '6' %}
<a href="{% url 'ipam:rir_list' %}" class="btn btn-default"> <a href="{% url 'ipam:rir_list' %}" class="btn btn-default">
<span class="fa fa-table" aria-hidden="true"></span> <span class="fa fa-table" aria-hidden="true"></span>
@ -29,7 +29,7 @@
<div class="alert alert-info pull-right"><strong>Note:</strong> Numbers shown indicate /64 prefixes.</div> <div class="alert alert-info pull-right"><strong>Note:</strong> Numbers shown indicate /64 prefixes.</div>
{% endif %} {% endif %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.ipam.add_role %} {% if perms.ipam.add_role %}
{% add_button 'ipam:role_add' %} {% add_button 'ipam:role_add' %}
{% import_button 'ipam:role_import' %} {% import_button 'ipam:role_import' %}

View File

@ -2,7 +2,7 @@
{% load helpers %} {% load helpers %}
{% block content %} {% block content %}
<div class="row"> <div class="row noprint">
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'ipam:service_list' %}">Services</a></li> <li><a href="{% url 'ipam:service_list' %}">Services</a></li>

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% export_button content_type %} {% export_button content_type %}
</div> </div>
<h1>{% block title %}Services{% endblock %}</h1> <h1>{% block title %}Services{% endblock %}</h1>
@ -10,7 +10,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:service_bulk_edit' bulk_delete_url='ipam:service_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:service_bulk_edit' bulk_delete_url='ipam:service_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %} {% include 'inc/tags_panel.html' %}
</div> </div>

View File

@ -2,7 +2,7 @@
{% load helpers %} {% load helpers %}
{% block header %} {% block header %}
<div class="row"> <div class="row noprint">
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'ipam:vlan_list' %}">VLANs</a></li> <li><a href="{% url 'ipam:vlan_list' %}">VLANs</a></li>
@ -28,7 +28,7 @@
</form> </form>
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.ipam.change_vlan %} {% if perms.ipam.change_vlan %}
<a href="{% url 'ipam:vlan_edit' pk=vlan.pk %}" class="btn btn-warning"> <a href="{% url 'ipam:vlan_edit' pk=vlan.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span> <span class="fa fa-pencil" aria-hidden="true"></span>
@ -143,7 +143,7 @@
</div> </div>
{% include 'responsive_table.html' with table=prefix_table %} {% include 'responsive_table.html' with table=prefix_table %}
{% if perms.ipam.add_prefix %} {% if perms.ipam.add_prefix %}
<div class="panel-footer text-right"> <div class="panel-footer text-right noprint">
<a href="{% url 'ipam:prefix_add' %}?{% if vlan.tenant %}tenant={{ vlan.tenant.pk }}&{% endif %}site={{ vlan.site.pk }}&vlan={{ vlan.pk }}" class="btn btn-primary btn-xs"> <a href="{% url 'ipam:prefix_add' %}?{% if vlan.tenant %}tenant={{ vlan.tenant.pk }}&{% endif %}site={{ vlan.site.pk }}&vlan={{ vlan.pk }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Add a prefix Add a prefix

View File

@ -2,7 +2,7 @@
{% load buttons %} {% load buttons %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right noprint">
{% if perms.ipam.add_vlan %} {% if perms.ipam.add_vlan %}
{% add_button 'ipam:vlan_add' %} {% add_button 'ipam:vlan_add' %}
{% import_button 'ipam:vlan_import' %} {% import_button 'ipam:vlan_import' %}
@ -14,7 +14,7 @@
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %}
</div> </div>
<div class="col-md-3"> <div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %} {% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %} {% include 'inc/tags_panel.html' %}
</div> </div>

Some files were not shown because too many files have changed in this diff Show More