Merge branch 'develop' into dev_csvcustomfields

This commit is contained in:
Anthony Eden 2019-05-22 18:29:31 +10:00 committed by GitHub
commit 401370b127
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
149 changed files with 966 additions and 672 deletions

View File

@ -1,11 +1,121 @@
v2.5.8 (FUTURE) 2.5.13 (FUTURE)
## Enhancements
* [#2813](https://github.com/digitalocean/netbox/issues/2813) - Add tenant group filters
* [#3085](https://github.com/digitalocean/netbox/issues/3085) - Catch all exceptions during export template rendering
* [#3138](https://github.com/digitalocean/netbox/issues/3138) - Add 2.5GE and 5GE interface form factors
* [#3183](https://github.com/digitalocean/netbox/issues/3183) - Enable bulk deletion of sites
* [#3186](https://github.com/digitalocean/netbox/issues/3186) - Add interface name filter for IP addresses
## Bug Fixes ## Bug Fixes
* [#3132](https://github.com/digitalocean/netbox/issues/3132) - Circuit termination missing from available cable termination types
* [#3150](https://github.com/digitalocean/netbox/issues/3150) - Fix formatting of cable length during cable trace
* [#3190](https://github.com/digitalocean/netbox/issues/3190) - Fix custom field rendering for Jinja2 export templates
---
2.5.12 (2019-05-01)
## Bug Fixes
* [#3127](https://github.com/digitalocean/netbox/issues/3127) - Fix natural ordering of device components
---
2.5.11 (2019-04-29)
## Notes
This release upgrades the Django framework to version 2.2.
## Enhancements
* [#2986](https://github.com/digitalocean/netbox/issues/2986) - Improve natural ordering of device components
* [#3023](https://github.com/digitalocean/netbox/issues/3023) - Add support for filtering cables by connected device
* [#3070](https://github.com/digitalocean/netbox/issues/3070) - Add decommissioning status for devices
## Bug Fixes
* [#2621](https://github.com/digitalocean/netbox/issues/2621) - Upgrade Django requirement to 2.2 to fix object deletion issue in the changelog middleware
* [#3072](https://github.com/digitalocean/netbox/issues/3072) - Preserve multiselect filter values when updating per-page count for list views
* [#3112](https://github.com/digitalocean/netbox/issues/3112) - Fix ordering of interface connections list by termination B name/device
* [#3116](https://github.com/digitalocean/netbox/issues/3116) - Fix `tagged_items` count in tags API endpoint
* [#3118](https://github.com/digitalocean/netbox/issues/3118) - Disable `last_login` update on login when maintenance mode is enabled
---
v2.5.10 (2019-04-08)
## Enhancements
* [#3052](https://github.com/digitalocean/netbox/issues/3052) - Add Jinja2 support for export templates
## Bug Fixes
* [#2937](https://github.com/digitalocean/netbox/issues/2937) - Redirect to list view after editing an object from list view
* [#3036](https://github.com/digitalocean/netbox/issues/3036) - DCIM interfaces API endpoint should not include VM interfaces
* [#3039](https://github.com/digitalocean/netbox/issues/3039) - Fix exception when retrieving change object for a component template via API
* [#3041](https://github.com/digitalocean/netbox/issues/3041) - Fix form widget for bulk cable label update
* [#3044](https://github.com/digitalocean/netbox/issues/3044) - Ignore site/rack fields when connecting a new cable via device search
* [#3046](https://github.com/digitalocean/netbox/issues/3046) - Fix exception at reports API endpoint
* [#3047](https://github.com/digitalocean/netbox/issues/3047) - Fix exception when writing mac address for an interface via API
---
v2.5.9 (2019-04-01)
## Enhancements
* [#2933](https://github.com/digitalocean/netbox/issues/2933) - Add username to outbound webhook requests
* [#3011](https://github.com/digitalocean/netbox/issues/3011) - Add SSL support for django-rq (requires django-rq v1.3.1+)
* [#3025](https://github.com/digitalocean/netbox/issues/3025) - Add request ID to outbound webhook requests (for correlating all changes part of a single request)
## 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
* [#3001](https://github.com/digitalocean/netbox/issues/3001) - Fix API representation of ObjectChange `action` and add `changed_object_type`
* [#3014](https://github.com/digitalocean/netbox/issues/3014) - Fixes VM Role filtering
* [#3019](https://github.com/digitalocean/netbox/issues/3019) - Fix tag population when running NetBox within a path
* [#3022](https://github.com/digitalocean/netbox/issues/3022) - Add missing cable termination types to DCIM `_choices` endpoint
* [#3026](https://github.com/digitalocean/netbox/issues/3026) - Tweak prefix/IP filter forms to filter using VRF ID rather than route distinguisher
* [#3027](https://github.com/digitalocean/netbox/issues/3027) - Ignore empty local context data when rendering config contexts
* [#3032](https://github.com/digitalocean/netbox/issues/3032) - Save assigned tags when creating a new secret
---
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 * [#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 * [#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 * [#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 * [#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
--- ---

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

@ -30,7 +30,7 @@ psql -c 'create database netbox'
psql netbox < netbox.sql psql netbox < netbox.sql
``` ```
Keep in mind that PostgreSQL user accounts and permissions are not included with the dump: You will need to create those manually if you want to fully replicate the original database (see the [installation docs](installation/1-postgresql.md)). When setting up a development instance of NetBox, it's strongly recommended to use different credentials anyway. Keep in mind that PostgreSQL user accounts and permissions are not included with the dump: You will need to create those manually if you want to fully replicate the original database (see the [installation docs](../installation/1-postgresql.md)). When setting up a development instance of NetBox, it's strongly recommended to use different credentials anyway.
## Export the Database Schema ## Export the Database Schema

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

@ -3,13 +3,13 @@ from django.db.models import Q
from dcim.models import Site from dcim.models import Site
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from .constants import CIRCUIT_STATUS_CHOICES from .constants import CIRCUIT_STATUS_CHOICES
from .models import Provider, Circuit, CircuitTermination, CircuitType from .models import Provider, Circuit, CircuitTermination, CircuitType
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet): class ProviderFilter(CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -54,7 +54,7 @@ class CircuitTypeFilter(NameSlugSearchFilterSet):
fields = ['name', 'slug'] fields = ['name', 'slug']
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -87,16 +87,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
choices=CIRCUIT_STATUS_CHOICES, choices=CIRCUIT_STATUS_CHOICES,
null_value=None null_value=None
) )
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__site', field_name='terminations__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),

View File

@ -4,6 +4,7 @@ from taggit.forms import TagField
from dcim.models import Site from dcim.models import Site
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from tenancy.forms import TenancyFilterForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField,
@ -265,8 +266,9 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
] ]
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Circuit model = Circuit
field_order = ['q', 'type', 'provider', 'status', 'site', 'tenant_group', 'tenant', 'commit_rate']
q = forms.CharField( q = forms.CharField(
required=False, required=False,
label='Search' label='Search'
@ -292,16 +294,6 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
required=False, required=False,
widget=StaticSelect2Multiple() widget=StaticSelect2Multiple()
) )
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
site = FilterChoiceField( site = FilterChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',

View File

@ -11,7 +11,7 @@ CIRCUITTYPE_ACTIONS = """
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
</a> </a>
{% if perms.circuit.change_circuittype %} {% if perms.circuit.change_circuittype %}
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'circuits:circuittype_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %} {% endif %}
""" """
@ -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

@ -1,3 +1,4 @@
from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
@ -502,12 +503,16 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
# #
class CableSerializer(ValidatedModelSerializer): class CableSerializer(ValidatedModelSerializer):
termination_a_type = ContentTypeField() termination_a_type = ContentTypeField(
termination_b_type = ContentTypeField() queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES)
)
termination_b_type = ContentTypeField(
queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES)
)
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

@ -1,7 +1,7 @@
from collections import OrderedDict from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.db.models import F, Q from django.db.models import F
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from drf_yasg import openapi from drf_yasg import openapi
@ -35,7 +35,7 @@ from .exceptions import MissingFilterException
class DCIMFieldChoicesViewSet(FieldChoicesViewSet): class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = ( fields = (
(Cable, ['length_unit', 'status', 'type']), (Cable, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']),
(ConsolePort, ['connection_status']), (ConsolePort, ['connection_status']),
(Device, ['face', 'status']), (Device, ['face', 'status']),
(DeviceType, ['subdevice_role']), (DeviceType, ['subdevice_role']),
@ -419,7 +419,9 @@ class PowerOutletViewSet(CableTraceMixin, ModelViewSet):
class InterfaceViewSet(CableTraceMixin, ModelViewSet): class InterfaceViewSet(CableTraceMixin, ModelViewSet):
queryset = Interface.objects.select_related( queryset = Interface.objects.filter(
device__isnull=False
).select_related(
'device', '_connected_interface', '_connected_circuittermination', 'cable' 'device', '_connected_interface', '_connected_circuittermination', 'cable'
).prefetch_related( ).prefetch_related(
'ip_addresses', 'tags' 'ip_addresses', 'tags'

View File

@ -75,6 +75,8 @@ IFACE_FF_100ME_FIXED = 800
IFACE_FF_1GE_FIXED = 1000 IFACE_FF_1GE_FIXED = 1000
IFACE_FF_1GE_GBIC = 1050 IFACE_FF_1GE_GBIC = 1050
IFACE_FF_1GE_SFP = 1100 IFACE_FF_1GE_SFP = 1100
IFACE_FF_2GE_FIXED = 1120
IFACE_FF_5GE_FIXED = 1130
IFACE_FF_10GE_FIXED = 1150 IFACE_FF_10GE_FIXED = 1150
IFACE_FF_10GE_CX4 = 1170 IFACE_FF_10GE_CX4 = 1170
IFACE_FF_10GE_SFP_PLUS = 1200 IFACE_FF_10GE_SFP_PLUS = 1200
@ -83,6 +85,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
@ -149,6 +152,8 @@ IFACE_FF_CHOICES = [
[ [
[IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'], [IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'], [IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_FF_2GE_FIXED, '2.5GBASE-T (2.5GE)'],
[IFACE_FF_5GE_FIXED, '5GBASE-T (5GE)'],
[IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'], [IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
[IFACE_FF_10GE_CX4, '10GBASE-CX4 (10GE)'], [IFACE_FF_10GE_CX4, '10GBASE-CX4 (10GE)'],
] ]
@ -164,6 +169,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)'],
@ -316,6 +322,7 @@ DEVICE_STATUS_PLANNED = 2
DEVICE_STATUS_STAGED = 3 DEVICE_STATUS_STAGED = 3
DEVICE_STATUS_FAILED = 4 DEVICE_STATUS_FAILED = 4
DEVICE_STATUS_INVENTORY = 5 DEVICE_STATUS_INVENTORY = 5
DEVICE_STATUS_DECOMMISSIONING = 6
DEVICE_STATUS_CHOICES = [ DEVICE_STATUS_CHOICES = [
[DEVICE_STATUS_ACTIVE, 'Active'], [DEVICE_STATUS_ACTIVE, 'Active'],
[DEVICE_STATUS_OFFLINE, 'Offline'], [DEVICE_STATUS_OFFLINE, 'Offline'],
@ -323,6 +330,7 @@ DEVICE_STATUS_CHOICES = [
[DEVICE_STATUS_STAGED, 'Staged'], [DEVICE_STATUS_STAGED, 'Staged'],
[DEVICE_STATUS_FAILED, 'Failed'], [DEVICE_STATUS_FAILED, 'Failed'],
[DEVICE_STATUS_INVENTORY, 'Inventory'], [DEVICE_STATUS_INVENTORY, 'Inventory'],
[DEVICE_STATUS_DECOMMISSIONING, 'Decommissioning'],
] ]
# Site statuses # Site statuses
@ -343,6 +351,7 @@ STATUS_CLASSES = {
3: 'primary', 3: 'primary',
4: 'danger', 4: 'danger',
5: 'default', 5: 'default',
6: 'warning',
} }
# Console/power/interface connection statuses # Console/power/interface connection statuses
@ -355,7 +364,7 @@ CONNECTION_STATUS_CHOICES = [
# Cable endpoint types # Cable endpoint types
CABLE_TERMINATION_TYPES = [ CABLE_TERMINATION_TYPES = [
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination',
] ]
# Cable types # Cable types

View File

@ -31,7 +31,7 @@ class MACAddressField(models.Field):
try: try:
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase) return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
except AddrFormatError as e: except AddrFormatError as e:
raise ValidationError(e) raise ValidationError("Invalid MAC address format: {}".format(value))
def db_type(self, connection): def db_type(self, connection):
return 'macaddr' return 'macaddr'

View File

@ -6,9 +6,12 @@ from netaddr import EUI
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.filtersets import TenancyFilterSet
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 (
@ -36,7 +39,7 @@ class RegionFilter(NameSlugSearchFilterSet):
fields = ['name', 'slug'] fields = ['name', 'slug']
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): class SiteFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -49,25 +52,16 @@ 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',
label='Region (slug)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Region (slug)',
) )
tag = TagFilter() tag = TagFilter()
@ -95,16 +89,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(
@ -130,7 +114,7 @@ class RackRoleFilter(NameSlugSearchFilterSet):
fields = ['name', 'slug', 'color'] fields = ['name', 'slug', 'color']
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): class RackFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -160,16 +144,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Group', label='Group',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=RACK_STATUS_CHOICES, choices=RACK_STATUS_CHOICES,
null_value=None null_value=None
@ -206,7 +180,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
) )
class RackReservationFilter(django_filters.FilterSet): class RackReservationFilter(TenancyFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -241,16 +215,6 @@ class RackReservationFilter(django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Group', label='Group',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
user_id = django_filters.ModelMultipleChoiceFilter( user_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(), queryset=User.objects.all(),
label='User (ID)', label='User (ID)',
@ -456,7 +420,7 @@ class PlatformFilter(NameSlugSearchFilterSet):
fields = ['name', 'slug'] fields = ['name', 'slug']
class DeviceFilter(CustomFieldFilterSet): class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -491,16 +455,6 @@ class DeviceFilter(CustomFieldFilterSet):
to_field_name='slug', to_field_name='slug',
label='Role (slug)', label='Role (slug)',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
platform_id = django_filters.ModelMultipleChoiceFilter( platform_id = django_filters.ModelMultipleChoiceFilter(
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
label='Platform (ID)', label='Platform (ID)',
@ -513,14 +467,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(
@ -619,16 +574,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:
@ -984,6 +929,14 @@ class CableFilter(django_filters.FilterSet):
color = django_filters.MultipleChoiceFilter( color = django_filters.MultipleChoiceFilter(
choices=COLOR_CHOICES choices=COLOR_CHOICES
) )
device = django_filters.CharFilter(
method='filter_connected_device',
field_name='name'
)
device_id = django_filters.CharFilter(
method='filter_connected_device',
field_name='pk'
)
class Meta: class Meta:
model = Cable model = Cable
@ -994,6 +947,16 @@ class CableFilter(django_filters.FilterSet):
return queryset return queryset
return queryset.filter(label__icontains=value) return queryset.filter(label__icontains=value)
def filter_connected_device(self, queryset, name, value):
if not value.strip():
return queryset
try:
device = Device.objects.get(**{name: value})
except ObjectDoesNotExist:
return queryset.none()
cable_pks = device.get_cables(pk_list=True)
return queryset.filter(pk__in=cable_pks)
class ConsoleConnectionFilter(django_filters.FilterSet): class ConsoleConnectionFilter(django_filters.FilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(

View File

@ -13,7 +13,8 @@ from timezone_field import TimeZoneFormField
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from ipam.models import IPAddress, VLAN, VLANGroup from ipam.models import IPAddress, VLAN, VLANGroup
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from tenancy.models import Tenant from tenancy.forms import TenancyFilterForm
from tenancy.models import Tenant, TenantGroup
from utilities.forms import ( from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField,
@ -256,8 +257,9 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
] ]
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Site model = Site
field_order = ['q', 'status', 'region', 'tenant_group', 'tenant']
q = forms.CharField( q = forms.CharField(
required=False, required=False,
label='Search' label='Search'
@ -276,16 +278,6 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
value_field="slug", value_field="slug",
) )
) )
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
# #
@ -596,8 +588,9 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
] ]
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Rack model = Rack
field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
q = forms.CharField( q = forms.CharField(
required=False, required=False,
label='Search' label='Search'
@ -619,16 +612,6 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
null_option=True, null_option=True,
) )
) )
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
choices=RACK_STATUS_CHOICES, choices=RACK_STATUS_CHOICES,
required=False, required=False,
@ -689,40 +672,6 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
return unit_choices return unit_choices
class RackReservationFilterForm(BootstrapMixin, forms.Form):
q = forms.CharField(
required=False,
label='Search'
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
)
group_id = FilterChoiceField(
queryset=RackGroup.objects.select_related('site'),
label='Rack group',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/dcim/rack-groups/",
null_option=True,
)
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=RackReservation.objects.all(), queryset=RackReservation.objects.all(),
@ -751,6 +700,31 @@ class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
nullable_fields = [] nullable_fields = []
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
field_order = ['q', 'site', 'group_id', 'tenant_group', 'tenant']
q = forms.CharField(
required=False,
label='Search'
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
)
group_id = FilterChoiceField(
queryset=RackGroup.objects.select_related('site'),
label='Rack group',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/dcim/rack-groups/",
null_option=True,
)
)
# #
# Manufacturers # Manufacturers
# #
@ -1643,8 +1617,12 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
] ]
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): class DeviceFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Device model = Device
field_order = [
'q', 'region', 'site', 'rack_group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant',
'manufacturer_id', 'device_type_id', 'mac_address', 'has_primary_ip',
]
q = forms.CharField( q = forms.CharField(
required=False, required=False,
label='Search' label='Search'
@ -1700,17 +1678,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(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
) )
) )
manufacturer_id = FilterChoiceField( manufacturer_id = FilterChoiceField(
@ -2707,12 +2674,12 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
status = forms.ChoiceField( status = forms.ChoiceField(
choices=add_blank_choice(CONNECTION_STATUS_CHOICES), choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
required=False, required=False,
widget=StaticSelect2(),
initial='' initial=''
) )
label = forms.CharField( label = forms.CharField(
max_length=100, max_length=100,
required=False, required=False
widget=StaticSelect2()
) )
color = forms.CharField( color = forms.CharField(
max_length=6, max_length=6,
@ -2767,6 +2734,10 @@ class CableFilterForm(BootstrapMixin, forms.Form):
required=False, required=False,
widget=ColorSelect() widget=ColorSelect()
) )
device = forms.CharField(
required=False,
label='Device name'
)
# #
@ -3102,9 +3073,31 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
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",
)
)
tenant_group = FilterChoiceField(
queryset=TenantGroup.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenant-groups/",
value_field="slug",
null_option=True,
filter_for={
'tenant': 'group'
}
)
) )
tenant = FilterChoiceField( tenant = FilterChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
null_label='-- None --', null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
) )

View File

@ -14,22 +14,6 @@ CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*:(\d{{1,9}})(\.\d{{1,9}})?$')
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*\.(\d{{1,9}})$') AS integer), 0)" VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*\.(\d{{1,9}})$') AS integer), 0)"
class DeviceComponentManager(Manager):
def get_queryset(self):
queryset = super().get_queryset()
table_name = self.model._meta.db_table
sql = r"CONCAT(REGEXP_REPLACE({}.name, '\d+$', ''), LPAD(SUBSTRING({}.name FROM '\d+$'), 8, '0'))"
# Pad any trailing digits to effect natural sorting
return queryset.extra(
select={
'name_padded': sql.format(table_name, table_name),
}
).order_by('name_padded', 'pk')
class InterfaceQuerySet(QuerySet): class InterfaceQuerySet(QuerySet):
def connectable(self): def connectable(self):
@ -64,11 +48,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

@ -23,7 +23,7 @@ from utilities.utils import serialize_object, to_meters
from .constants import * from .constants import *
from .exceptions import LoopDetected from .exceptions import LoopDetected
from .fields import ASNField, MACAddressField from .fields import ASNField, MACAddressField
from .managers import DeviceComponentManager, InterfaceManager from .managers import InterfaceManager
class ComponentTemplateModel(models.Model): class ComponentTemplateModel(models.Model):
@ -1004,7 +1004,7 @@ class ConsolePortTemplate(ComponentTemplateModel):
max_length=50 max_length=50
) )
objects = DeviceComponentManager() objects = NaturalOrderingManager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
@ -1027,7 +1027,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
max_length=50 max_length=50
) )
objects = DeviceComponentManager() objects = NaturalOrderingManager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
@ -1050,7 +1050,7 @@ class PowerPortTemplate(ComponentTemplateModel):
max_length=50 max_length=50
) )
objects = DeviceComponentManager() objects = NaturalOrderingManager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
@ -1073,7 +1073,7 @@ class PowerOutletTemplate(ComponentTemplateModel):
max_length=50 max_length=50
) )
objects = DeviceComponentManager() objects = NaturalOrderingManager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
@ -1139,7 +1139,7 @@ class FrontPortTemplate(ComponentTemplateModel):
validators=[MinValueValidator(1), MaxValueValidator(64)] validators=[MinValueValidator(1), MaxValueValidator(64)]
) )
objects = DeviceComponentManager() objects = NaturalOrderingManager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
@ -1188,7 +1188,7 @@ class RearPortTemplate(ComponentTemplateModel):
validators=[MinValueValidator(1), MaxValueValidator(64)] validators=[MinValueValidator(1), MaxValueValidator(64)]
) )
objects = DeviceComponentManager() objects = NaturalOrderingManager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
@ -1211,7 +1211,7 @@ class DeviceBayTemplate(ComponentTemplateModel):
max_length=50 max_length=50
) )
objects = DeviceComponentManager() objects = NaturalOrderingManager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
@ -1704,6 +1704,21 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False) filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False)
return Interface.objects.filter(filter) return Interface.objects.filter(filter)
def get_cables(self, pk_list=False):
"""
Return a QuerySet or PK list matching all Cables connected to a component of this Device.
"""
cable_pks = []
for component_model in [
ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, FrontPort, RearPort
]:
cable_pks += component_model.objects.filter(
device=self, cable__isnull=False
).values_list('cable', flat=True)
if pk_list:
return cable_pks
return Cable.objects.filter(pk__in=cable_pks)
def get_children(self): def get_children(self):
""" """
Return the set of child Devices installed in DeviceBays within this Device. Return the set of child Devices installed in DeviceBays within this Device.
@ -1742,7 +1757,7 @@ class ConsolePort(CableTermination, ComponentModel):
blank=True blank=True
) )
objects = DeviceComponentManager() objects = NaturalOrderingManager()
tags = TaggableManager() tags = TaggableManager()
csv_headers = ['device', 'name'] csv_headers = ['device', 'name']
@ -1785,7 +1800,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
blank=True blank=True
) )
objects = DeviceComponentManager() objects = NaturalOrderingManager()
tags = TaggableManager() tags = TaggableManager()
csv_headers = ['device', 'name'] csv_headers = ['device', 'name']
@ -1834,7 +1849,7 @@ class PowerPort(CableTermination, ComponentModel):
blank=True blank=True
) )
objects = DeviceComponentManager() objects = NaturalOrderingManager()
tags = TaggableManager() tags = TaggableManager()
csv_headers = ['device', 'name'] csv_headers = ['device', 'name']
@ -1877,7 +1892,7 @@ class PowerOutlet(CableTermination, ComponentModel):
blank=True blank=True
) )
objects = DeviceComponentManager() objects = NaturalOrderingManager()
tags = TaggableManager() tags = TaggableManager()
csv_headers = ['device', 'name'] csv_headers = ['device', 'name']
@ -2198,7 +2213,7 @@ class FrontPort(CableTermination, ComponentModel):
blank=True blank=True
) )
objects = DeviceComponentManager() objects = NaturalOrderingManager()
tags = TaggableManager() tags = TaggableManager()
csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description'] csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
@ -2264,7 +2279,7 @@ class RearPort(CableTermination, ComponentModel):
blank=True blank=True
) )
objects = DeviceComponentManager() objects = NaturalOrderingManager()
tags = TaggableManager() tags = TaggableManager()
csv_headers = ['device', 'name', 'type', 'positions', 'description'] csv_headers = ['device', 'name', 'type', 'positions', 'description']
@ -2311,7 +2326,7 @@ class DeviceBay(ComponentModel):
null=True null=True
) )
objects = DeviceComponentManager() objects = NaturalOrderingManager()
tags = TaggableManager() tags = TaggableManager()
csv_headers = ['device', 'name', 'installed_device'] csv_headers = ['device', 'name', 'installed_device']
@ -2423,7 +2438,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 +2572,15 @@ class Cable(ChangeLoggedModel):
('termination_b_type', 'termination_b_id'), ('termination_b_type', 'termination_b_id'),
) )
def __init__(self, *args, **kwargs): def __str__(self):
if self.label:
return self.label
super().__init__(*args, **kwargs) # Save a copy of the PK on the instance since it's nullified if .delete() is called
if not hasattr(self, 'id_string'):
# 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) self.id_string = '#{}'.format(self.pk)
def __str__(self): return self.id_string
return self.label or 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])

View File

@ -44,7 +44,7 @@ REGION_ACTIONS = """
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
</a> </a>
{% if perms.dcim.change_region %} {% if perms.dcim.change_region %}
<a href="{% url 'dcim:region_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'dcim:region_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %} {% endif %}
""" """
@ -56,7 +56,7 @@ RACKGROUP_ACTIONS = """
<i class="fa fa-eye"></i> <i class="fa fa-eye"></i>
</a> </a>
{% if perms.dcim.change_rackgroup %} {% if perms.dcim.change_rackgroup %}
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning" title="Edit"> <a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning" title="Edit">
<i class="glyphicon glyphicon-pencil"></i> <i class="glyphicon glyphicon-pencil"></i>
</a> </a>
{% endif %} {% endif %}
@ -67,7 +67,7 @@ RACKROLE_ACTIONS = """
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
</a> </a>
{% if perms.dcim.change_rackrole %} {% if perms.dcim.change_rackrole %}
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'dcim:rackrole_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %} {% endif %}
""" """
@ -88,7 +88,7 @@ RACKRESERVATION_ACTIONS = """
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
</a> </a>
{% if perms.dcim.change_rackreservation %} {% if perms.dcim.change_rackreservation %}
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %} {% endif %}
""" """
@ -97,7 +97,7 @@ MANUFACTURER_ACTIONS = """
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
</a> </a>
{% if perms.dcim.change_manufacturer %} {% if perms.dcim.change_manufacturer %}
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %} {% endif %}
""" """
@ -106,7 +106,7 @@ DEVICEROLE_ACTIONS = """
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
</a> </a>
{% if perms.dcim.change_devicerole %} {% if perms.dcim.change_devicerole %}
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'dcim:devicerole_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %} {% endif %}
""" """
@ -131,7 +131,7 @@ PLATFORM_ACTIONS = """
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
</a> </a>
{% if perms.dcim.change_platform %} {% if perms.dcim.change_platform %}
<a href="{% url 'dcim:platform_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'dcim:platform_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %} {% endif %}
""" """
@ -168,7 +168,7 @@ VIRTUALCHASSIS_ACTIONS = """
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
</a> </a>
{% if perms.dcim.change_virtualchassis %} {% if perms.dcim.change_virtualchassis %}
<a href="{% url 'dcim:virtualchassis_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'dcim:virtualchassis_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %} {% endif %}
""" """
@ -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):
@ -305,11 +305,11 @@ class RackDetailTable(RackTable):
class RackReservationTable(BaseTable): class RackReservationTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.TemplateColumn(template_code=COL_TENANT)
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=''
) )
@ -733,18 +733,18 @@ class InterfaceConnectionTable(BaseTable):
) )
device_b = tables.LinkColumn( device_b = tables.LinkColumn(
viewname='dcim:device', viewname='dcim:device',
accessor=Accessor('connected_endpoint.device'), accessor=Accessor('_connected_interface.device'),
args=[Accessor('connected_endpoint.device.pk')], args=[Accessor('_connected_interface.device.pk')],
verbose_name='Device B' verbose_name='Device B'
) )
interface_b = tables.LinkColumn( interface_b = tables.LinkColumn(
viewname='dcim:interface', viewname='dcim:interface',
accessor=Accessor('connected_endpoint.name'), accessor=Accessor('_connected_interface'),
args=[Accessor('connected_endpoint.pk')], args=[Accessor('_connected_interface.pk')],
verbose_name='Interface B' verbose_name='Interface B'
) )
description_b = tables.Column( description_b = tables.Column(
accessor=Accessor('connected_endpoint.description'), accessor=Accessor('_connected_interface.description'),
verbose_name='Description' verbose_name='Description'
) )
@ -779,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

@ -25,6 +25,7 @@ urlpatterns = [
url(r'^sites/add/$', views.SiteCreateView.as_view(), name='site_add'), url(r'^sites/add/$', views.SiteCreateView.as_view(), name='site_add'),
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'), url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'), url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
url(r'^sites/delete/$', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'),
url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'), url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'), url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'), url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),

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
@ -246,6 +247,14 @@ class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
default_return_url = 'dcim:site_list' default_return_url = 'dcim:site_list'
class SiteBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_site'
queryset = Site.objects.select_related('region', 'tenant')
filter = filters.SiteFilter
table = tables.SiteTable
default_return_url = 'dcim:site_list'
# #
# Rack groups # Rack groups
# #
@ -353,8 +362,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

@ -1,3 +1,4 @@
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers from rest_framework import serializers
from taggit.models import Tag from taggit.models import Tag
@ -15,7 +16,8 @@ from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantG
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from users.api.nested_serializers import NestedUserSerializer from users.api.nested_serializers import NestedUserSerializer
from utilities.api import ( from utilities.api import (
ChoiceField, ContentTypeField, get_serializer_for_model, SerializedPKRelatedField, ValidatedModelSerializer, ChoiceField, ContentTypeField, get_serializer_for_model, SerializerNotFound, SerializedPKRelatedField,
ValidatedModelSerializer,
) )
from .nested_serializers import * from .nested_serializers import *
@ -53,10 +55,17 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
# #
class ExportTemplateSerializer(ValidatedModelSerializer): class ExportTemplateSerializer(ValidatedModelSerializer):
template_language = ChoiceField(
choices=TEMPLATE_LANGUAGE_CHOICES,
default=TEMPLATE_LANGUAGE_JINJA2
)
class Meta: class Meta:
model = ExportTemplate model = ExportTemplate
fields = ['id', 'content_type', 'name', 'description', 'template_code', 'mime_type', 'file_extension'] fields = [
'id', 'content_type', 'name', 'description', 'template_language', 'template_code', 'mime_type',
'file_extension',
]
# #
@ -88,7 +97,9 @@ class TagSerializer(ValidatedModelSerializer):
# #
class ImageAttachmentSerializer(ValidatedModelSerializer): class ImageAttachmentSerializer(ValidatedModelSerializer):
content_type = ContentTypeField() content_type = ContentTypeField(
queryset=ContentType.objects.all()
)
parent = serializers.SerializerMethodField(read_only=True) parent = serializers.SerializerMethodField(read_only=True)
class Meta: class Meta:
@ -205,14 +216,25 @@ class ReportDetailSerializer(ReportSerializer):
# #
class ObjectChangeSerializer(serializers.ModelSerializer): class ObjectChangeSerializer(serializers.ModelSerializer):
user = NestedUserSerializer(read_only=True) user = NestedUserSerializer(
content_type = ContentTypeField(read_only=True) read_only=True
changed_object = serializers.SerializerMethodField(read_only=True) )
action = ChoiceField(
choices=OBJECTCHANGE_ACTION_CHOICES,
read_only=True
)
changed_object_type = ContentTypeField(
read_only=True
)
changed_object = serializers.SerializerMethodField(
read_only=True
)
class Meta: class Meta:
model = ObjectChange model = ObjectChange
fields = [ fields = [
'id', 'time', 'user', 'user_name', 'request_id', 'action', 'content_type', 'changed_object', 'object_data', 'id', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object',
'object_data',
] ]
def get_changed_object(self, obj): def get_changed_object(self, obj):
@ -221,9 +243,14 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
""" """
if obj.changed_object is None: if obj.changed_object is None:
return None return None
try:
serializer = get_serializer_for_model(obj.changed_object, prefix='Nested') serializer = get_serializer_for_model(obj.changed_object, prefix='Nested')
if serializer is None: except SerializerNotFound:
return obj.object_repr return obj.object_repr
context = {'request': self.context['request']} context = {
'request': self.context['request']
}
data = serializer(obj.changed_object, context=context).data data = serializer(obj.changed_object, context=context).data
return data return data

View File

@ -10,7 +10,7 @@ from taggit.models import Tag
from extras import filters from extras import filters
from extras.models import ( from extras.models import (
ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
) )
from extras.reports import get_report, get_reports from extras.reports import get_report, get_reports
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
@ -23,8 +23,9 @@ from . import serializers
class ExtrasFieldChoicesViewSet(FieldChoicesViewSet): class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
fields = ( fields = (
(CustomField, ['type']), (ExportTemplate, ['template_language']),
(Graph, ['type']), (Graph, ['type']),
(ObjectChange, ['action']),
) )
@ -115,7 +116,9 @@ class TopologyMapViewSet(ModelViewSet):
# #
class TagViewSet(ModelViewSet): class TagViewSet(ModelViewSet):
queryset = Tag.objects.annotate(tagged_items=Count('taggit_taggeditem_items')) queryset = Tag.objects.annotate(
tagged_items=Count('taggit_taggeditem_items', distinct=True)
)
serializer_class = serializers.TagSerializer serializer_class = serializers.TagSerializer
filterset_class = filters.TagFilter filterset_class = filters.TagFilter

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

@ -56,6 +56,14 @@ EXPORTTEMPLATE_MODELS = [
'cluster', 'virtualmachine', # Virtualization 'cluster', 'virtualmachine', # Virtualization
] ]
# ExportTemplate language choices
TEMPLATE_LANGUAGE_DJANGO = 10
TEMPLATE_LANGUAGE_JINJA2 = 20
TEMPLATE_LANGUAGE_CHOICES = (
(TEMPLATE_LANGUAGE_DJANGO, 'Django'),
(TEMPLATE_LANGUAGE_JINJA2, 'Jinja2'),
)
# Topology map types # Topology map types
TOPOLOGYMAP_TYPE_NETWORK = 1 TOPOLOGYMAP_TYPE_NETWORK = 1
TOPOLOGYMAP_TYPE_CONSOLE = 2 TOPOLOGYMAP_TYPE_CONSOLE = 2

View File

@ -82,7 +82,7 @@ class ExportTemplateFilter(django_filters.FilterSet):
class Meta: class Meta:
model = ExportTemplate model = ExportTemplate
fields = ['content_type', 'name'] fields = ['content_type', 'name', 'template_language']
class TagFilter(django_filters.FilterSet): class TagFilter(django_filters.FilterSet):

View File

@ -4,7 +4,6 @@ from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from mptt.forms import TreeNodeMultipleChoiceField
from taggit.forms import TagField from taggit.forms import TagField
from taggit.models import Tag from taggit.models import Tag

View File

@ -37,7 +37,7 @@ def _record_object_deleted(request, instance, **kwargs):
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)
enqueue_webhooks(instance, OBJECTCHANGE_ACTION_DELETE) enqueue_webhooks(instance, request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
class ObjectChangeMiddleware(object): class ObjectChangeMiddleware(object):
@ -83,7 +83,7 @@ class ObjectChangeMiddleware(object):
obj.log_change(request.user, request.id, action) obj.log_change(request.user, request.id, action)
# Enqueue webhooks # Enqueue webhooks
enqueue_webhooks(obj, action) enqueue_webhooks(obj, request.user, request.id, action)
# Housekeeping: 1% chance of clearing out expired ObjectChanges # Housekeeping: 1% chance of clearing out expired ObjectChanges
if _thread_locals.changed_objects and settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1: if _thread_locals.changed_objects and settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1:

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

@ -0,0 +1,27 @@
# Generated by Django 2.1.7 on 2019-04-08 14:49
from django.db import migrations, models
def set_template_language(apps, schema_editor):
"""
Set the language for all existing ExportTemplates to Django (Jinja2 is the default for new ExportTemplates).
"""
ExportTemplate = apps.get_model('extras', 'ExportTemplate')
ExportTemplate.objects.update(template_language=10)
class Migration(migrations.Migration):
dependencies = [
('extras', '0017_exporttemplate_mime_type_length'),
]
operations = [
migrations.AddField(
model_name='exporttemplate',
name='template_language',
field=models.PositiveSmallIntegerField(default=20),
),
migrations.RunPython(set_template_language),
]

View File

@ -1,7 +1,6 @@
from collections import OrderedDict from collections import OrderedDict
from datetime import date from datetime import date
import graphviz
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -12,6 +11,8 @@ from django.db.models import F, Q
from django.http import HttpResponse from django.http import HttpResponse
from django.template import Template, Context from django.template import Template, Context
from django.urls import reverse from django.urls import reverse
import graphviz
from jinja2 import Environment
from dcim.constants import CONNECTION_STATUS_CONNECTED from dcim.constants import CONNECTION_STATUS_CONNECTED
from utilities.utils import deepmerge, foreground_color from utilities.utils import deepmerge, foreground_color
@ -105,6 +106,7 @@ class CustomFieldModel(models.Model):
class Meta: class Meta:
abstract = True abstract = True
@property
def cf(self): def cf(self):
""" """
Name-based CustomFieldValue accessor for use in templates Name-based CustomFieldValue accessor for use in templates
@ -355,9 +357,13 @@ class ExportTemplate(models.Model):
max_length=200, max_length=200,
blank=True blank=True
) )
template_language = models.PositiveSmallIntegerField(
choices=TEMPLATE_LANGUAGE_CHOICES,
default=TEMPLATE_LANGUAGE_JINJA2
)
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(
@ -374,16 +380,36 @@ class ExportTemplate(models.Model):
def __str__(self): def __str__(self):
return '{}: {}'.format(self.content_type, self.name) return '{}: {}'.format(self.content_type, self.name)
def render(self, queryset):
"""
Render the contents of the template.
"""
context = {
'queryset': queryset
}
if self.template_language == TEMPLATE_LANGUAGE_DJANGO:
template = Template(self.template_code)
output = template.render(Context(context))
elif self.template_language == TEMPLATE_LANGUAGE_JINJA2:
template = Environment().from_string(source=self.template_code)
output = template.render(**context)
else:
return None
# Replace CRLF-style line terminators
output = output.replace('\r\n', '\n')
return output
def render_to_response(self, queryset): def render_to_response(self, queryset):
""" """
Render the template to an HTTP response, delivered as a named file attachment Render the template to an HTTP response, delivered as a named file attachment
""" """
template = Template(self.template_code) output = self.render(queryset)
mime_type = 'text/plain' if not self.mime_type else self.mime_type mime_type = 'text/plain' if not self.mime_type else self.mime_type
output = template.render(Context({'queryset': queryset}))
# Replace CRLF-style line terminators
output = output.replace('\r\n', '\n')
# Build the response # Build the response
response = HttpResponse(output, content_type=mime_type) response = HttpResponse(output, content_type=mime_type)
@ -720,7 +746,7 @@ class ConfigContextModel(models.Model):
data = deepmerge(data, context.data) data = deepmerge(data, context.data)
# If the object has local config context data defined, merge it last # If the object has local config context data defined, merge it last
if self.local_context_data is not None: if self.local_context_data:
data = deepmerge(data, self.local_context_data) data = deepmerge(data, self.local_context_data)
return data return data

View File

@ -1,6 +1,24 @@
from collections import OrderedDict
from django.db.models import Q, QuerySet from django.db.models import Q, QuerySet
class CustomFieldQueryset:
"""
Annotate custom fields on objects within a QuerySet.
"""
def __init__(self, queryset, custom_fields):
self.queryset = queryset
self.model = queryset.model
self.custom_fields = custom_fields
def __iter__(self):
for obj in self.queryset:
values_dict = {cfv.field_id: cfv.value for cfv in obj.custom_field_values.all()}
obj.custom_fields = OrderedDict([(field, values_dict.get(field.pk)) for field in self.custom_fields])
yield obj
class ConfigContextQuerySet(QuerySet): class ConfigContextQuerySet(QuerySet):
def get_for_object(self, obj): def get_for_object(self, obj):

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

@ -30,7 +30,7 @@ from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemT
class TagListView(ObjectListView): class TagListView(ObjectListView):
queryset = Tag.objects.annotate( queryset = Tag.objects.annotate(
items=Count('taggit_taggeditem_items') items=Count('taggit_taggeditem_items', distinct=True)
).order_by( ).order_by(
'name' 'name'
) )

View File

@ -9,7 +9,7 @@ from utilities.api import get_serializer_for_model
from .constants import WEBHOOK_MODELS from .constants import WEBHOOK_MODELS
def enqueue_webhooks(instance, action): def enqueue_webhooks(instance, user, request_id, action):
""" """
Find Webhook(s) assigned to this instance + action and enqueue them Find Webhook(s) assigned to this instance + action and enqueue them
to be processed to be processed
@ -47,5 +47,7 @@ def enqueue_webhooks(instance, action):
serializer.data, serializer.data,
instance._meta.model_name, instance._meta.model_name,
action, action,
str(datetime.datetime.now()) str(datetime.datetime.now()),
user.username,
request_id
) )

View File

@ -10,7 +10,7 @@ from extras.constants import WEBHOOK_CT_JSON, WEBHOOK_CT_X_WWW_FORM_ENCODED, OBJ
@job('default') @job('default')
def process_webhook(webhook, data, model_name, event, timestamp): def process_webhook(webhook, data, model_name, event, timestamp, username, request_id):
""" """
Make a POST request to the defined Webhook Make a POST request to the defined Webhook
""" """
@ -18,6 +18,8 @@ def process_webhook(webhook, data, model_name, event, timestamp):
'event': dict(OBJECTCHANGE_ACTION_CHOICES)[event].lower(), 'event': dict(OBJECTCHANGE_ACTION_CHOICES)[event].lower(),
'timestamp': timestamp, 'timestamp': timestamp,
'model': model_name, 'model': model_name,
'username': username,
'request_id': request_id,
'data': data 'data': data
} }
headers = { headers = {

View File

@ -128,6 +128,7 @@ class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
# #
class PrefixSerializer(TaggitSerializer, CustomFieldModelSerializer): class PrefixSerializer(TaggitSerializer, CustomFieldModelSerializer):
family = ChoiceField(choices=AF_CHOICES, read_only=True)
site = NestedSiteSerializer(required=False, allow_null=True) site = NestedSiteSerializer(required=False, allow_null=True)
vrf = NestedVRFSerializer(required=False, allow_null=True) vrf = NestedVRFSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
@ -189,6 +190,7 @@ class IPAddressInterfaceSerializer(WritableNestedSerializer):
class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer): class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
family = ChoiceField(choices=AF_CHOICES, read_only=True)
vrf = NestedVRFSerializer(required=False, allow_null=True) vrf = NestedVRFSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=IPADDRESS_STATUS_CHOICES, required=False) status = ChoiceField(choices=IPADDRESS_STATUS_CHOICES, required=False)

View File

@ -6,14 +6,14 @@ from netaddr.core import AddrFormatError
from dcim.models import Site, Device, Interface from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): class VRFFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -22,16 +22,6 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
tag = TagFilter() tag = TagFilter()
def search(self, queryset, name, value): def search(self, queryset, name, value):
@ -59,7 +49,7 @@ class RIRFilter(NameSlugSearchFilterSet):
fields = ['name', 'slug', 'is_private'] fields = ['name', 'slug', 'is_private']
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet): class AggregateFilter(CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -107,7 +97,7 @@ class RoleFilter(NameSlugSearchFilterSet):
fields = ['name', 'slug'] fields = ['name', 'slug']
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -146,16 +136,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='rd', to_field_name='rd',
label='VRF (RD)', label='VRF (RD)',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
@ -254,7 +234,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.filter(prefix__net_mask_length=value) return queryset.filter(prefix__net_mask_length=value)
class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -285,16 +265,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='rd', to_field_name='rd',
label='VRF (RD)', label='VRF (RD)',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
device = django_filters.CharFilter( device = django_filters.CharFilter(
method='filter_device', method='filter_device',
field_name='name', field_name='name',
@ -316,6 +286,12 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='name', to_field_name='name',
label='Virtual machine (name)', label='Virtual machine (name)',
) )
interface = django_filters.ModelMultipleChoiceFilter(
field_name='interface__name',
queryset=Interface.objects.all(),
to_field_name='name',
label='Interface (ID)',
)
interface_id = django_filters.ModelMultipleChoiceFilter( interface_id = django_filters.ModelMultipleChoiceFilter(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
label='Interface (ID)', label='Interface (ID)',
@ -394,7 +370,7 @@ class VLANGroupFilter(NameSlugSearchFilterSet):
fields = ['name', 'slug'] fields = ['name', 'slug']
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): class VLANFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -423,16 +399,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Group', label='Group',
) )
tenant_id = django_filters.ModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
field_name='tenant__slug',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
role_id = django_filters.ModelMultipleChoiceFilter( role_id = django_filters.ModelMultipleChoiceFilter(
queryset=Role.objects.all(), queryset=Role.objects.all(),
label='Role (ID)', label='Role (ID)',

View File

@ -6,6 +6,7 @@ from taggit.forms import TagField
from dcim.models import Site, Rack, Device, Interface from dcim.models import Site, Rack, Device, Interface
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from tenancy.forms import TenancyFilterForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField,
@ -97,22 +98,13 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm
] ]
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm): class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = VRF model = VRF
field_order = ['q', 'tenant_group', 'tenant']
q = forms.CharField( q = forms.CharField(
required=False, required=False,
label='Search' label='Search'
) )
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
# #
@ -349,11 +341,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.',
} }
@ -497,8 +489,12 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
] ]
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Prefix model = Prefix
field_order = [
'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'site', 'role', 'tenant_group', 'tenant',
'is_pool', 'expand',
]
q = forms.CharField( q = forms.CharField(
required=False, required=False,
label='Search' label='Search'
@ -524,24 +520,12 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
label='Mask length', label='Mask length',
widget=StaticSelect2() widget=StaticSelect2()
) )
vrf = FilterChoiceField( vrf_id = FilterChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
to_field_name='rd',
label='VRF', label='VRF',
null_label='-- Global --', null_label='-- Global --',
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/ipam/vrfs/", api_url="/api/ipam/vrfs/",
value_field="rd",
null_option=True,
)
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True, null_option=True,
) )
) )
@ -764,11 +748,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.',
} }
@ -946,8 +930,11 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
) )
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = IPAddress model = IPAddress
field_order = [
'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'tenant_group', 'tenant',
]
q = forms.CharField( q = forms.CharField(
required=False, required=False,
label='Search' label='Search'
@ -973,24 +960,12 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
label='Mask length', label='Mask length',
widget=StaticSelect2() widget=StaticSelect2()
) )
vrf = FilterChoiceField( vrf_id = FilterChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
to_field_name='rd',
label='VRF', label='VRF',
null_label='-- Global --', null_label='-- Global --',
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/ipam/vrfs/", api_url="/api/ipam/vrfs/",
value_field="rd",
null_option=True,
)
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True, null_option=True,
) )
) )
@ -1225,8 +1200,9 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
] ]
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = VLAN model = VLAN
field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
q = forms.CharField( q = forms.CharField(
required=False, required=False,
label='Search' label='Search'
@ -1250,16 +1226,6 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
null_option=True, null_option=True,
) )
) )
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
)
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
choices=VLAN_STATUS_CHOICES, choices=VLAN_STATUS_CHOICES,
required=False, required=False,

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

@ -30,7 +30,7 @@ RIR_ACTIONS = """
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
</a> </a>
{% if perms.ipam.change_rir %} {% if perms.ipam.change_rir %}
<a href="{% url 'ipam:rir_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'ipam:rir_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %} {% endif %}
""" """
@ -52,7 +52,7 @@ ROLE_ACTIONS = """
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
</a> </a>
{% if perms.ipam.change_role %} {% if perms.ipam.change_role %}
<a href="{% url 'ipam:role_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'ipam:role_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %} {% endif %}
""" """
@ -152,7 +152,7 @@ VLANGROUP_ACTIONS = """
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% if perms.ipam.change_vlangroup %} {% if perms.ipam.change_vlangroup %}
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %} {% endif %}
""" """
@ -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
@ -319,6 +319,7 @@ class PrefixTable(BaseTable):
class PrefixDetailTable(PrefixTable): class PrefixDetailTable(PrefixTable):
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False) utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False)
tenant = tables.TemplateColumn(template_code=COL_TENANT)
class Meta(PrefixTable.Meta): class Meta(PrefixTable.Meta):
fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description') fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description')
@ -349,6 +350,7 @@ class IPAddressDetailTable(IPAddressTable):
nat_inside = tables.LinkColumn( nat_inside = tables.LinkColumn(
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)' 'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
) )
tenant = tables.TemplateColumn(template_code=COL_TENANT)
class Meta(IPAddressTable.Meta): class Meta(IPAddressTable.Meta):
fields = ( fields = (
@ -392,7 +394,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):
@ -423,6 +425,7 @@ class VLANTable(BaseTable):
class VLANDetailTable(VLANTable): class VLANDetailTable(VLANTable):
prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes') prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes')
tenant = tables.TemplateColumn(template_code=COL_TENANT)
class Meta(VLANTable.Meta): class Meta(VLANTable.Meta):
fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description') fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
@ -437,7 +440,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

@ -147,18 +147,18 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
# Miscellaneous # Miscellaneous
# #
def get_view_name(view_cls, suffix=None): def get_view_name(view, suffix=None):
""" """
Derive the view name from its associated model, if it has one. Fall back to DRF's built-in `get_view_name`. Derive the view name from its associated model, if it has one. Fall back to DRF's built-in `get_view_name`.
""" """
if hasattr(view_cls, 'queryset'): if hasattr(view, 'queryset'):
# Determine the model name from the queryset. # Determine the model name from the queryset.
name = view_cls.queryset.model._meta.verbose_name name = view.queryset.model._meta.verbose_name
name = ' '.join([w[0].upper() + w[1:] for w in name.split()]) # Capitalize each word name = ' '.join([w[0].upper() + w[1:] for w in name.split()]) # Capitalize each word
else: else:
# Replicate DRF's built-in behavior. # Replicate DRF's built-in behavior.
name = view_cls.__name__ name = view.__class__.__name__
name = formatting.remove_trailing_string(name, 'View') name = formatting.remove_trailing_string(name, 'View')
name = formatting.remove_trailing_string(name, 'ViewSet') name = formatting.remove_trailing_string(name, 'ViewSet')
name = formatting.camelcase_to_spaces(name) name = formatting.camelcase_to_spaces(name)

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.8-dev' VERSION = '2.5.13-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

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

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,14 @@ $(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 is_visible = $(filter_for_element).is(":visible");
var value = $(filter_for_element).val(); var value = $(filter_for_element).val();
if (param_name && value) { if (param_name && is_visible && value) {
parameters[param_name] = value; parameters[param_name] = value;
} else if (param_name && is_visible && is_nullable) {
parameters[param_name] = "null";
} }
}); });
@ -243,7 +251,7 @@ $(document).ready(function() {
ajax: { ajax: {
delay: 250, delay: 250,
url: "/api/extras/tags/", url: netbox_api_path + "extras/tags/",
data: function(params) { data: function(params) {
// Paging. Note that `params.page` indexes at 1 // Paging. Note that `params.page` indexes at 1

View File

@ -8,7 +8,7 @@ SECRETROLE_ACTIONS = """
<i class="fa fa-history"></i> <i class="fa fa-history"></i>
</a> </a>
{% if perms.secrets.change_secretrole %} {% if perms.secrets.change_secretrole %}
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'secrets:secretrole_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %} {% endif %}
""" """
@ -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

@ -120,6 +120,8 @@ def secret_add(request, pk):
secret.plaintext = str(form.cleaned_data['plaintext']) secret.plaintext = str(form.cleaned_data['plaintext'])
secret.encrypt(master_key) secret.encrypt(master_key)
secret.save() secret.save()
form.save_m2m()
messages.success(request, "Added new secret: {}.".format(secret)) messages.success(request, "Added new secret: {}.".format(secret))
if '_addanother' in request.POST: if '_addanother' in request.POST:
return redirect('dcim:device_addsecret', pk=device.pk) return redirect('dcim:device_addsecret', pk=device.pk)

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;

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

@ -31,7 +31,7 @@
</h4> </h4>
<p><span class="label label-{% if cable.status %}success{% else %}info{% endif %}">{{ cable.get_status_display }}</span></p> <p><span class="label label-{% if cable.status %}success{% else %}info{% endif %}">{{ cable.get_status_display }}</span></p>
<p>{{ cable.get_type_display|default:"" }}</p> <p>{{ cable.get_type_display|default:"" }}</p>
{% if cable.length %}- {{ cable.length }}{{ cable.get_length_unit_display }}{% endif %} {% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %}
<span class="label color-block center-block" style="background-color: #{{ cable.color }}">&nbsp;</span> <span class="label color-block center-block" style="background-color: #{{ cable.color }}">&nbsp;</span>
{% else %} {% else %}
<h4 class="text-muted">No Cable</h4> <h4 class="text-muted">No Cable</h4>

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

@ -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' %}
@ -12,9 +12,9 @@
<h1>{% block title %}Sites{% endblock %}</h1> <h1>{% block title %}Sites{% endblock %}</h1>
<div class="row"> <div class="row">
<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' bulk_delete_url='dcim:site_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 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

@ -20,9 +20,11 @@
</ul> </ul>
</nav> </nav>
<form method="get"> <form method="get">
{% for k, v in request.GET.items %} {% for k, v_list in request.GET.lists %}
{% if k != 'per_page' %} {% if k != 'per_page' %}
{% for v in v_list %}
<input type="hidden" name="{{ k }}" value="{{ v }}" /> <input type="hidden" name="{{ k }}" value="{{ v }}" />
{% endfor %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<select name="per_page" id="per_page"> <select name="per_page" id="per_page">

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>

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