Просмотр исходного кода

Merge branch 'develop' of https://github.com/digitalocean/netbox into 2813-addtenantgroupfilter

dansheps 6 лет назад
Родитель
Сommit
a91a79681f
100 измененных файлов с 471 добавлено и 229 удалено
  1. 60 1
      CHANGELOG.md
  2. 4 4
      README.md
  3. 1 1
      docs/api/overview.md
  4. 7 0
      docs/configuration/optional-settings.md
  5. 2 2
      netbox/circuits/tables.py
  6. 8 3
      netbox/dcim/api/serializers.py
  7. 5 3
      netbox/dcim/api/views.py
  8. 2 0
      netbox/dcim/constants.py
  9. 1 1
      netbox/dcim/fields.py
  10. 2 3
      netbox/dcim/forms.py
  11. 5 1
      netbox/dcim/managers.py
  12. 8 9
      netbox/dcim/models.py
  13. 16 16
      netbox/dcim/tables.py
  14. 3 1
      netbox/dcim/views.py
  15. 37 10
      netbox/extras/api/serializers.py
  16. 3 2
      netbox/extras/api/views.py
  17. 1 0
      netbox/extras/apps.py
  18. 8 0
      netbox/extras/constants.py
  19. 1 1
      netbox/extras/filters.py
  20. 1 2
      netbox/extras/forms.py
  21. 2 2
      netbox/extras/middleware.py
  22. 18 0
      netbox/extras/migrations/0017_exporttemplate_mime_type_length.py
  23. 27 0
      netbox/extras/migrations/0018_exporttemplate_add_jinja2.py
  24. 33 8
      netbox/extras/models.py
  25. 1 1
      netbox/extras/tables.py
  26. 4 2
      netbox/extras/webhooks.py
  27. 3 1
      netbox/extras/webhooks_worker.py
  28. 2 0
      netbox/ipam/api/serializers.py
  29. 8 12
      netbox/ipam/forms.py
  30. 24 2
      netbox/ipam/models.py
  31. 7 7
      netbox/ipam/tables.py
  32. 4 4
      netbox/netbox/api.py
  33. 1 0
      netbox/netbox/configuration.example.py
  34. 4 1
      netbox/netbox/settings.py
  35. 14 1
      netbox/project-static/css/base.css
  36. 10 2
      netbox/project-static/js/forms.js
  37. 2 2
      netbox/secrets/tables.py
  38. 2 0
      netbox/secrets/views.py
  39. 1 1
      netbox/templates/_base.html
  40. 2 2
      netbox/templates/circuits/circuit.html
  41. 2 2
      netbox/templates/circuits/circuit_list.html
  42. 1 1
      netbox/templates/circuits/circuittype_list.html
  43. 3 3
      netbox/templates/circuits/provider.html
  44. 2 2
      netbox/templates/circuits/provider_list.html
  45. 2 2
      netbox/templates/dcim/cable.html
  46. 2 2
      netbox/templates/dcim/cable_list.html
  47. 2 2
      netbox/templates/dcim/console_connections_list.html
  48. 15 15
      netbox/templates/dcim/device.html
  49. 1 1
      netbox/templates/dcim/device_inventory.html
  50. 2 2
      netbox/templates/dcim/device_list.html
  51. 1 1
      netbox/templates/dcim/devicerole_list.html
  52. 2 2
      netbox/templates/dcim/devicetype.html
  53. 2 2
      netbox/templates/dcim/devicetype_list.html
  54. 1 1
      netbox/templates/dcim/inc/consoleport.html
  55. 1 1
      netbox/templates/dcim/inc/consoleserverport.html
  56. 1 1
      netbox/templates/dcim/inc/devicebay.html
  57. 1 1
      netbox/templates/dcim/inc/devicetype_component_table.html
  58. 1 1
      netbox/templates/dcim/inc/frontport.html
  59. 2 2
      netbox/templates/dcim/inc/interface.html
  60. 1 1
      netbox/templates/dcim/inc/inventoryitem.html
  61. 1 1
      netbox/templates/dcim/inc/poweroutlet.html
  62. 1 1
      netbox/templates/dcim/inc/powerport.html
  63. 1 1
      netbox/templates/dcim/inc/rearport.html
  64. 2 2
      netbox/templates/dcim/interface.html
  65. 2 2
      netbox/templates/dcim/interface_connections_list.html
  66. 2 2
      netbox/templates/dcim/inventoryitem_list.html
  67. 1 1
      netbox/templates/dcim/manufacturer_list.html
  68. 1 1
      netbox/templates/dcim/platform_list.html
  69. 2 2
      netbox/templates/dcim/power_connections_list.html
  70. 6 6
      netbox/templates/dcim/rack.html
  71. 2 2
      netbox/templates/dcim/rack_elevation_list.html
  72. 2 2
      netbox/templates/dcim/rack_list.html
  73. 2 2
      netbox/templates/dcim/rackgroup_list.html
  74. 1 1
      netbox/templates/dcim/rackreservation_list.html
  75. 1 1
      netbox/templates/dcim/rackrole_list.html
  76. 2 2
      netbox/templates/dcim/region_list.html
  77. 6 6
      netbox/templates/dcim/site.html
  78. 2 2
      netbox/templates/dcim/site_list.html
  79. 2 2
      netbox/templates/dcim/virtualchassis_list.html
  80. 2 2
      netbox/templates/extras/configcontext.html
  81. 2 2
      netbox/templates/extras/configcontext_list.html
  82. 2 2
      netbox/templates/extras/objectchange.html
  83. 2 2
      netbox/templates/extras/objectchange_list.html
  84. 2 2
      netbox/templates/extras/report.html
  85. 6 0
      netbox/templates/extras/tag.html
  86. 1 1
      netbox/templates/inc/image_attachments.html
  87. 1 1
      netbox/templates/inc/search_panel.html
  88. 2 2
      netbox/templates/ipam/aggregate.html
  89. 2 2
      netbox/templates/ipam/aggregate_list.html
  90. 1 1
      netbox/templates/ipam/inc/service.html
  91. 3 3
      netbox/templates/ipam/ipaddress.html
  92. 2 2
      netbox/templates/ipam/ipaddress_list.html
  93. 2 2
      netbox/templates/ipam/prefix.html
  94. 2 2
      netbox/templates/ipam/prefix_list.html
  95. 2 2
      netbox/templates/ipam/rir_list.html
  96. 1 1
      netbox/templates/ipam/role_list.html
  97. 1 1
      netbox/templates/ipam/service.html
  98. 2 2
      netbox/templates/ipam/service_list.html
  99. 3 3
      netbox/templates/ipam/vlan.html
  100. 2 2
      netbox/templates/ipam/vlan_list.html

+ 60 - 1
CHANGELOG.md

@@ -1,14 +1,73 @@
-v2.5.8 (FUTURE)
+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
 * [#2781](https://github.com/digitalocean/netbox/issues/2781) - Fix filtering of sites/devices/VMs by multiple regions
 * [#2923](https://github.com/digitalocean/netbox/issues/2923) - Provider filter form's site field should be blank by default
 * [#2938](https://github.com/digitalocean/netbox/issues/2938) - Enforce deterministic ordering of device components returned by API
 * [#2939](https://github.com/digitalocean/netbox/issues/2939) - Exclude circuit terminations from API interface connections endpoint
+* [#2940](https://github.com/digitalocean/netbox/issues/2940) - Allow CSV import of prefixes/IPs to VRF without an RD assigned
+* [#2944](https://github.com/digitalocean/netbox/issues/2944) - Record the deletion of an IP address in the changelog of its parent interface (if any)
 * [#2952](https://github.com/digitalocean/netbox/issues/2952) - Added the `slug` field to the Tenant filter for use in the API and search function
 * [#2954](https://github.com/digitalocean/netbox/issues/2954) - Remove trailing slashes to fix root/template paths on Windows
+* [#2961](https://github.com/digitalocean/netbox/issues/2961) - Prevent exception when exporting inventory items belonging to unnamed devices
+* [#2962](https://github.com/digitalocean/netbox/issues/2962) - Increase ExportTemplate `mime_type` field length
+* [#2966](https://github.com/digitalocean/netbox/issues/2966) - Accept `null` cable length_unit via API
+* [#2972](https://github.com/digitalocean/netbox/issues/2972) - Improve ContentTypeField serializer to elegantly handle invalid data
+* [#2976](https://github.com/digitalocean/netbox/issues/2976) - Add delete button to tag view
+* [#2980](https://github.com/digitalocean/netbox/issues/2980) - Improve rendering time for API docs
+* [#2982](https://github.com/digitalocean/netbox/issues/2982) - Correct CSS class assignment on color picker
+* [#2984](https://github.com/digitalocean/netbox/issues/2984) - Fix logging of unlabeled cable ID on cable deletion
+* [#2985](https://github.com/digitalocean/netbox/issues/2985) - Fix pagination page length for rack elevations
 
 ---
 

+ 4 - 4
README.md

@@ -45,13 +45,13 @@ and run `upgrade.sh`.
 
 ## 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
 
-- [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
 
-- [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

+ 1 - 1
docs/api/overview.md

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

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

@@ -283,6 +283,7 @@ REDIS = {
     'PASSWORD': '',
     'DATABASE': 0,
     'DEFAULT_TIMEOUT': 300,
+    'SSL': False,
 }
 ```
 
@@ -315,3 +316,9 @@ The TCP port to use when connecting to the Redis server.
 Default: None
 
 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.

+ 2 - 2
netbox/circuits/tables.py

@@ -11,7 +11,7 @@ CIRCUITTYPE_ACTIONS = """
     <i class="fa fa-history"></i>
 </a>
 {% 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 %}
 """
 
@@ -59,7 +59,7 @@ class CircuitTypeTable(BaseTable):
     name = tables.LinkColumn()
     circuit_count = tables.Column(verbose_name='Circuits')
     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):

+ 8 - 3
netbox/dcim/api/serializers.py

@@ -1,3 +1,4 @@
+from django.contrib.contenttypes.models import ContentType
 from rest_framework import serializers
 from rest_framework.validators import UniqueTogetherValidator
 from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
@@ -502,12 +503,16 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
 #
 
 class CableSerializer(ValidatedModelSerializer):
-    termination_a_type = ContentTypeField()
-    termination_b_type = ContentTypeField()
+    termination_a_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_b = serializers.SerializerMethodField(read_only=True)
     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:
         model = Cable

+ 5 - 3
netbox/dcim/api/views.py

@@ -1,7 +1,7 @@
 from collections import OrderedDict
 
 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.shortcuts import get_object_or_404
 from drf_yasg import openapi
@@ -35,7 +35,7 @@ from .exceptions import MissingFilterException
 
 class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
     fields = (
-        (Cable, ['length_unit', 'status', 'type']),
+        (Cable, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']),
         (ConsolePort, ['connection_status']),
         (Device, ['face', 'status']),
         (DeviceType, ['subdevice_role']),
@@ -419,7 +419,9 @@ class PowerOutletViewSet(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'
     ).prefetch_related(
         'ip_addresses', 'tags'

+ 2 - 0
netbox/dcim/constants.py

@@ -83,6 +83,7 @@ IFACE_FF_10GE_XENPAK = 1310
 IFACE_FF_10GE_X2 = 1320
 IFACE_FF_25GE_SFP28 = 1350
 IFACE_FF_40GE_QSFP_PLUS = 1400
+IFACE_FF_50GE_QSFP28 = 1420
 IFACE_FF_100GE_CFP = 1500
 IFACE_FF_100GE_CFP2 = 1510
 IFACE_FF_100GE_CFP4 = 1520
@@ -164,6 +165,7 @@ IFACE_FF_CHOICES = [
             [IFACE_FF_10GE_X2, 'X2 (10GE)'],
             [IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
             [IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
+            [IFACE_FF_50GE_QSFP28, 'QSFP28 (50GE)'],
             [IFACE_FF_100GE_CFP, 'CFP (100GE)'],
             [IFACE_FF_100GE_CFP2, 'CFP2 (100GE)'],
             [IFACE_FF_200GE_CFP2, 'CFP2 (200GE)'],

+ 1 - 1
netbox/dcim/fields.py

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

+ 2 - 3
netbox/dcim/forms.py

@@ -1678,7 +1678,6 @@ class DeviceFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
         widget=APISelectMultiple(
             api_url="/api/dcim/device-roles/",
             value_field="slug",
-            null_option=True,
         )
     )
     manufacturer_id = FilterChoiceField(
@@ -2675,12 +2674,12 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
     status = forms.ChoiceField(
         choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
         required=False,
+        widget=StaticSelect2(),
         initial=''
     )
     label = forms.CharField(
         max_length=100,
-        required=False,
-        widget=StaticSelect2()
+        required=False
     )
     color = forms.CharField(
         max_length=6,

+ 5 - 1
netbox/dcim/managers.py

@@ -64,11 +64,15 @@ class InterfaceManager(Manager):
 
         The original `name` field is considered in its entirety to serve as a fallback in the event interfaces do not
         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)
         ordering = [
-            '_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name',
+            '_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name', 'pk'
+
         ]
 
         fields = {

+ 8 - 9
netbox/dcim/models.py

@@ -2423,7 +2423,7 @@ class InventoryItem(ComponentModel):
 
     def to_csv(self):
         return (
-            self.device.name or '{' + self.device.pk + '}',
+            self.device.name or '{{{}}}'.format(self.device.pk),
             self.name,
             self.manufacturer.name if self.manufacturer else None,
             self.part_id,
@@ -2557,16 +2557,15 @@ class Cable(ChangeLoggedModel):
             ('termination_b_type', 'termination_b_id'),
         )
 
-    def __init__(self, *args, **kwargs):
-
-        super().__init__(*args, **kwargs)
+    def __str__(self):
+        if self.label:
+            return self.label
 
-        # 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)
+        # Save a copy of the PK on the instance since it's nullified if .delete() is called
+        if not hasattr(self, 'id_string'):
+            self.id_string = '#{}'.format(self.pk)
 
-    def __str__(self):
-        return self.label or self.id_string
+        return self.id_string
 
     def get_absolute_url(self):
         return reverse('dcim:cable', args=[self.pk])

+ 16 - 16
netbox/dcim/tables.py

@@ -44,7 +44,7 @@ REGION_ACTIONS = """
     <i class="fa fa-history"></i>
 </a>
 {% 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 %}
 """
 
@@ -56,7 +56,7 @@ RACKGROUP_ACTIONS = """
     <i class="fa fa-eye"></i>
 </a>
 {% 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>
     </a>
 {% endif %}
@@ -67,7 +67,7 @@ RACKROLE_ACTIONS = """
     <i class="fa fa-history"></i>
 </a>
 {% 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 %}
 """
 
@@ -88,7 +88,7 @@ RACKRESERVATION_ACTIONS = """
     <i class="fa fa-history"></i>
 </a>
 {% 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 %}
 """
 
@@ -97,7 +97,7 @@ MANUFACTURER_ACTIONS = """
     <i class="fa fa-history"></i>
 </a>
 {% 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 %}
 """
 
@@ -106,7 +106,7 @@ DEVICEROLE_ACTIONS = """
     <i class="fa fa-history"></i>
 </a>
 {% 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 %}
 """
 
@@ -131,7 +131,7 @@ PLATFORM_ACTIONS = """
     <i class="fa fa-history"></i>
 </a>
 {% 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 %}
 """
 
@@ -168,7 +168,7 @@ VIRTUALCHASSIS_ACTIONS = """
     <i class="fa fa-history"></i>
 </a>
 {% 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 %}
 """
 
@@ -196,7 +196,7 @@ class RegionTable(BaseTable):
     slug = tables.Column(verbose_name='Slug')
     actions = tables.TemplateColumn(
         template_code=REGION_ACTIONS,
-        attrs={'td': {'class': 'text-right'}},
+        attrs={'td': {'class': 'text-right noprint'}},
         verbose_name=''
     )
 
@@ -239,7 +239,7 @@ class RackGroupTable(BaseTable):
     slug = tables.Column()
     actions = tables.TemplateColumn(
         template_code=RACKGROUP_ACTIONS,
-        attrs={'td': {'class': 'text-right'}},
+        attrs={'td': {'class': 'text-right noprint'}},
         verbose_name=''
     )
 
@@ -258,7 +258,7 @@ class RackRoleTable(BaseTable):
     rack_count = tables.Column(verbose_name='Racks')
     color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color')
     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='')
 
     class Meta(BaseTable.Meta):
@@ -309,7 +309,7 @@ class RackReservationTable(BaseTable):
     rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
     unit_list = tables.Column(orderable=False, verbose_name='Units')
     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):
@@ -327,7 +327,7 @@ class ManufacturerTable(BaseTable):
     devicetype_count = tables.Column(verbose_name='Device Types')
     platform_count = tables.Column(verbose_name='Platforms')
     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='')
 
     class Meta(BaseTable.Meta):
@@ -463,7 +463,7 @@ class DeviceRoleTable(BaseTable):
     slug = tables.Column(verbose_name='Slug')
     actions = tables.TemplateColumn(
         template_code=DEVICEROLE_ACTIONS,
-        attrs={'td': {'class': 'text-right'}},
+        attrs={'td': {'class': 'text-right noprint'}},
         verbose_name=''
     )
 
@@ -492,7 +492,7 @@ class PlatformTable(BaseTable):
     )
     actions = tables.TemplateColumn(
         template_code=PLATFORM_ACTIONS,
-        attrs={'td': {'class': 'text-right'}},
+        attrs={'td': {'class': 'text-right noprint'}},
         verbose_name=''
     )
 
@@ -779,7 +779,7 @@ class VirtualChassisTable(BaseTable):
     member_count = tables.Column(verbose_name='Members')
     actions = tables.TemplateColumn(
         template_code=VIRTUALCHASSIS_ACTIONS,
-        attrs={'td': {'class': 'text-right'}},
+        attrs={'td': {'class': 'text-right noprint'}},
         verbose_name=''
     )
 

+ 3 - 1
netbox/dcim/views.py

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

+ 37 - 10
netbox/extras/api/serializers.py

@@ -1,3 +1,4 @@
+from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ObjectDoesNotExist
 from rest_framework import serializers
 from taggit.models import Tag
@@ -15,7 +16,8 @@ from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantG
 from tenancy.models import Tenant, TenantGroup
 from users.api.nested_serializers import NestedUserSerializer
 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 *
 
@@ -53,10 +55,17 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
 #
 
 class ExportTemplateSerializer(ValidatedModelSerializer):
+    template_language = ChoiceField(
+        choices=TEMPLATE_LANGUAGE_CHOICES,
+        default=TEMPLATE_LANGUAGE_JINJA2
+    )
 
     class Meta:
         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):
-    content_type = ContentTypeField()
+    content_type = ContentTypeField(
+        queryset=ContentType.objects.all()
+    )
     parent = serializers.SerializerMethodField(read_only=True)
 
     class Meta:
@@ -205,14 +216,25 @@ class ReportDetailSerializer(ReportSerializer):
 #
 
 class ObjectChangeSerializer(serializers.ModelSerializer):
-    user = NestedUserSerializer(read_only=True)
-    content_type = ContentTypeField(read_only=True)
-    changed_object = serializers.SerializerMethodField(read_only=True)
+    user = NestedUserSerializer(
+        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:
         model = ObjectChange
         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):
@@ -221,9 +243,14 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
         """
         if obj.changed_object is None:
             return None
-        serializer = get_serializer_for_model(obj.changed_object, prefix='Nested')
-        if serializer is None:
+
+        try:
+            serializer = get_serializer_for_model(obj.changed_object, prefix='Nested')
+        except SerializerNotFound:
             return obj.object_repr
-        context = {'request': self.context['request']}
+        context = {
+            'request': self.context['request']
+        }
         data = serializer(obj.changed_object, context=context).data
+
         return data

+ 3 - 2
netbox/extras/api/views.py

@@ -10,7 +10,7 @@ from taggit.models import Tag
 
 from extras import filters
 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 utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
@@ -23,8 +23,9 @@ from . import serializers
 
 class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
     fields = (
-        (CustomField, ['type']),
+        (ExportTemplate, ['template_language']),
         (Graph, ['type']),
+        (ObjectChange, ['action']),
     )
 
 

+ 1 - 0
netbox/extras/apps.py

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

+ 8 - 0
netbox/extras/constants.py

@@ -56,6 +56,14 @@ EXPORTTEMPLATE_MODELS = [
     '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
 TOPOLOGYMAP_TYPE_NETWORK = 1
 TOPOLOGYMAP_TYPE_CONSOLE = 2

+ 1 - 1
netbox/extras/filters.py

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

+ 1 - 2
netbox/extras/forms.py

@@ -4,7 +4,6 @@ from django import forms
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ObjectDoesNotExist
-from mptt.forms import TreeNodeMultipleChoiceField
 from taggit.forms import TagField
 from taggit.models import Tag
 
@@ -12,7 +11,7 @@ from dcim.models import DeviceRole, Platform, Region, Site
 from tenancy.models import Tenant, TenantGroup
 from utilities.forms import (
     add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect,
-    FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
+    FilterChoiceField, LaxURLField, JSONField, SlugField,
 )
 from .constants import (
     CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,

+ 2 - 2
netbox/extras/middleware.py

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

+ 18 - 0
netbox/extras/migrations/0017_exporttemplate_mime_type_length.py

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

+ 27 - 0
netbox/extras/migrations/0018_exporttemplate_add_jinja2.py

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

+ 33 - 8
netbox/extras/models.py

@@ -1,7 +1,6 @@
 from collections import OrderedDict
 from datetime import date
 
-import graphviz
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.fields import GenericForeignKey
 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.template import Template, Context
 from django.urls import reverse
+import graphviz
+from jinja2 import Environment
 
 from dcim.constants import CONNECTION_STATUS_CONNECTED
 from utilities.utils import deepmerge, foreground_color
@@ -355,9 +356,13 @@ class ExportTemplate(models.Model):
         max_length=200,
         blank=True
     )
+    template_language = models.PositiveSmallIntegerField(
+        choices=TEMPLATE_LANGUAGE_CHOICES,
+        default=TEMPLATE_LANGUAGE_JINJA2
+    )
     template_code = models.TextField()
     mime_type = models.CharField(
-        max_length=15,
+        max_length=50,
         blank=True
     )
     file_extension = models.CharField(
@@ -374,17 +379,37 @@ class ExportTemplate(models.Model):
     def __str__(self):
         return '{}: {}'.format(self.content_type, self.name)
 
-    def render_to_response(self, queryset):
+    def render(self, queryset):
         """
-        Render the template to an HTTP response, delivered as a named file attachment
+        Render the contents of the template.
         """
-        template = Template(self.template_code)
-        mime_type = 'text/plain' if not self.mime_type else self.mime_type
-        output = template.render(Context({'queryset': queryset}))
+        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):
+        """
+        Render the template to an HTTP response, delivered as a named file attachment
+        """
+        output = self.render(queryset)
+        mime_type = 'text/plain' if not self.mime_type else self.mime_type
+
         # Build the response
         response = HttpResponse(output, content_type=mime_type)
         filename = 'netbox_{}{}'.format(
@@ -720,7 +745,7 @@ class ConfigContextModel(models.Model):
             data = deepmerge(data, context.data)
 
         # 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)
 
         return data

+ 1 - 1
netbox/extras/tables.py

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

+ 4 - 2
netbox/extras/webhooks.py

@@ -9,7 +9,7 @@ from utilities.api import get_serializer_for_model
 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
     to be processed
@@ -47,5 +47,7 @@ def enqueue_webhooks(instance, action):
                 serializer.data,
                 instance._meta.model_name,
                 action,
-                str(datetime.datetime.now())
+                str(datetime.datetime.now()),
+                user.username,
+                request_id
             )

+ 3 - 1
netbox/extras/webhooks_worker.py

@@ -10,7 +10,7 @@ from extras.constants import WEBHOOK_CT_JSON, WEBHOOK_CT_X_WWW_FORM_ENCODED, OBJ
 
 
 @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
     """
@@ -18,6 +18,8 @@ def process_webhook(webhook, data, model_name, event, timestamp):
         'event': dict(OBJECTCHANGE_ACTION_CHOICES)[event].lower(),
         'timestamp': timestamp,
         'model': model_name,
+        'username': username,
+        'request_id': request_id,
         'data': data
     }
     headers = {

+ 2 - 0
netbox/ipam/api/serializers.py

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

+ 8 - 12
netbox/ipam/forms.py

@@ -341,11 +341,11 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
 
 
 class PrefixCSVForm(forms.ModelForm):
-    vrf = forms.ModelChoiceField(
+    vrf = FlexibleModelChoiceField(
         queryset=VRF.objects.all(),
-        required=False,
         to_field_name='rd',
-        help_text='Route distinguisher of parent VRF',
+        required=False,
+        help_text='Route distinguisher of parent VRF (or {ID})',
         error_messages={
             'invalid_choice': 'VRF not found.',
         }
@@ -518,14 +518,12 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
         label='Mask length',
         widget=StaticSelect2()
     )
-    vrf = FilterChoiceField(
+    vrf_id = FilterChoiceField(
         queryset=VRF.objects.all(),
-        to_field_name='rd',
         label='VRF',
         null_label='-- Global --',
         widget=APISelectMultiple(
             api_url="/api/ipam/vrfs/",
-            value_field="rd",
             null_option=True,
         )
     )
@@ -748,11 +746,11 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
 
 
 class IPAddressCSVForm(forms.ModelForm):
-    vrf = forms.ModelChoiceField(
+    vrf = FlexibleModelChoiceField(
         queryset=VRF.objects.all(),
-        required=False,
         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={
             'invalid_choice': 'VRF not found.',
         }
@@ -959,14 +957,12 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
         label='Mask length',
         widget=StaticSelect2()
     )
-    vrf = FilterChoiceField(
+    vrf_id = FilterChoiceField(
         queryset=VRF.objects.all(),
-        to_field_name='rd',
         label='VRF',
         null_label='-- Global --',
         widget=APISelectMultiple(
             api_url="/api/ipam/vrfs/",
-            value_field="rd",
             null_option=True,
         )
     )

+ 24 - 2
netbox/ipam/models.py

@@ -1,7 +1,7 @@
 import netaddr
 from django.conf import settings
 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.db import models
 from django.db.models import Q
@@ -10,8 +10,9 @@ from django.urls import reverse
 from taggit.managers import TaggableManager
 
 from dcim.models import Interface
-from extras.models import CustomFieldModel
+from extras.models import CustomFieldModel, ObjectChange
 from utilities.models import ChangeLoggedModel
+from utilities.utils import serialize_object
 from .constants import *
 from .fields import IPNetworkField, IPAddressField
 from .querysets import PrefixQuerySet
@@ -629,6 +630,27 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
             self.family = self.address.version
         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):
 
         # Determine if this IP is primary for a Device

+ 7 - 7
netbox/ipam/tables.py

@@ -30,7 +30,7 @@ RIR_ACTIONS = """
     <i class="fa fa-history"></i>
 </a>
 {% 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 %}
 """
 
@@ -52,7 +52,7 @@ ROLE_ACTIONS = """
     <i class="fa fa-history"></i>
 </a>
 {% 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 %}
 """
 
@@ -152,7 +152,7 @@ VLANGROUP_ACTIONS = """
     {% endif %}
 {% endwith %}
 {% 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 %}
 """
 
@@ -207,7 +207,7 @@ class RIRTable(BaseTable):
     name = tables.LinkColumn(verbose_name='Name')
     is_private = BooleanColumn(verbose_name='Private')
     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):
         model = RIR
@@ -292,7 +292,7 @@ class RoleTable(BaseTable):
         orderable=False,
         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):
         model = Role
@@ -398,7 +398,7 @@ class VLANGroupTable(BaseTable):
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     vlan_count = tables.Column(verbose_name='VLANs')
     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='')
 
     class Meta(BaseTable.Meta):
@@ -444,7 +444,7 @@ class VLANMemberTable(BaseTable):
     )
     actions = tables.TemplateColumn(
         template_code=VLAN_MEMBER_ACTIONS,
-        attrs={'td': {'class': 'text-right'}},
+        attrs={'td': {'class': 'text-right noprint'}},
         verbose_name=''
     )
 

+ 4 - 4
netbox/netbox/api.py

@@ -147,18 +147,18 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
 # 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`.
     """
-    if hasattr(view_cls, 'queryset'):
+    if hasattr(view, '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
 
     else:
         # 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, 'ViewSet')
         name = formatting.camelcase_to_spaces(name)

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

@@ -132,6 +132,7 @@ REDIS = {
     'PASSWORD': '',
     'DATABASE': 0,
     '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

+ 4 - 1
netbox/netbox/settings.py

@@ -22,7 +22,7 @@ except ImportError:
     )
 
 
-VERSION = '2.5.8-dev'
+VERSION = '2.5.11-dev'
 
 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_DATABASE = REDIS.get('DATABASE', 0)
 REDIS_DEFAULT_TIMEOUT = REDIS.get('DEFAULT_TIMEOUT', 300)
+REDIS_SSL = REDIS.get('SSL', False)
 
 # Email
 EMAIL_HOST = EMAIL.get('SERVER')
@@ -291,6 +292,7 @@ RQ_QUEUES = {
         'DB': REDIS_DATABASE,
         'PASSWORD': REDIS_PASSWORD,
         'DEFAULT_TIMEOUT': REDIS_DEFAULT_TIMEOUT,
+        'SSL': REDIS_SSL,
     }
 }
 
@@ -315,6 +317,7 @@ SWAGGER_SETTINGS = {
         'utilities.custom_inspectors.IdInFilterInspector',
         'drf_yasg.inspectors.CoreAPICompatInspector',
     ],
+    'DEFAULT_MODEL_DEPTH': 1,
     'DEFAULT_PAGINATOR_INSPECTORS': [
         'utilities.custom_inspectors.NullablePaginatorInspector',
         'drf_yasg.inspectors.DjangoRestResponsePagination',

+ 14 - 1
netbox/project-static/css/base.css

@@ -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 */
 @media (max-width: 959px) {
     .navbar-header {
@@ -575,4 +588,4 @@ td .progress {
 }
 textarea {
     font-family: Consolas, Lucida Console, monospace;
-}
+}

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

@@ -90,6 +90,10 @@ $(document).ready(function() {
     // Assign color picker selection classes
     function colorPickerClassCopy(data, container) {
         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"));
         }
         return data.text;
@@ -151,10 +155,14 @@ $(document).ready(function() {
 
                 filter_for_elements.each(function(index, filter_for_element) {
                     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();
 
-                    if (param_name && value) {
+                    if (param_name && is_visible && 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: {
             delay: 250,
-            url: "/api/extras/tags/",
+            url: netbox_api_path + "extras/tags/",
 
             data: function(params) {
                 // Paging. Note that `params.page` indexes at 1

+ 2 - 2
netbox/secrets/tables.py

@@ -8,7 +8,7 @@ SECRETROLE_ACTIONS = """
     <i class="fa fa-history"></i>
 </a>
 {% 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 %}
 """
 
@@ -23,7 +23,7 @@ class SecretRoleTable(BaseTable):
     secret_count = tables.Column(verbose_name='Secrets')
     slug = tables.Column(verbose_name='Slug')
     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):

+ 2 - 0
netbox/secrets/views.py

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

+ 1 - 1
netbox/templates/_base.html

@@ -54,7 +54,7 @@
                 <div class="col-xs-4 text-center">
                     <p class="text-muted">{% now 'Y-m-d H:i:s T' %}</p>
                 </div>
-                <div class="col-xs-4 text-right">
+                <div class="col-xs-4 text-right noprint">
                     <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-cloud text-primary"></i> <a href="{% url 'api_docs' %}">API</a> &middot;

+ 2 - 2
netbox/templates/circuits/circuit.html

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

+ 2 - 2
netbox/templates/circuits/circuit_list.html

@@ -2,7 +2,7 @@
 {% load buttons %}
 
 {% block content %}
-<div class="pull-right">
+<div class="pull-right noprint">
     {% if perms.circuits.add_circuit %}
         {% add_button 'circuits:circuit_add' %}
         {% import_button 'circuits:circuit_import' %}
@@ -14,7 +14,7 @@
 	<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' %}
     </div>
-    <div class="col-md-3">
+    <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/tags_panel.html' %}
     </div>

+ 1 - 1
netbox/templates/circuits/circuittype_list.html

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

+ 3 - 3
netbox/templates/circuits/provider.html

@@ -5,7 +5,7 @@
 {% block title %}{{ provider }}{% endblock %}
 
 {% block header %}
-    <div class="row">
+    <div class="row noprint">
         <div class="col-sm-8 col-md-9">
             <ol class="breadcrumb">
                 <li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
@@ -25,7 +25,7 @@
             </form>
         </div>
     </div>
-    <div class="pull-right">
+    <div class="pull-right noprint">
         {% 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">
                 <i class="fa fa-signal" aria-hidden="true"></i>
@@ -172,7 +172,7 @@
                 {% endfor %}
             </table>
             {% 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">
                         <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add circuit
                     </a>

+ 2 - 2
netbox/templates/circuits/provider_list.html

@@ -2,7 +2,7 @@
 {% load buttons %}
 
 {% block content %}
-<div class="pull-right">
+<div class="pull-right noprint">
     {% if perms.circuits.add_provider %}
         {% add_button 'circuits:provider_add' %}
         {% import_button 'circuits:provider_import' %}
@@ -14,7 +14,7 @@
     <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' %}
     </div>
-    <div class="col-md-3">
+    <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/tags_panel.html' %}
     </div>

+ 2 - 2
netbox/templates/dcim/cable.html

@@ -2,7 +2,7 @@
 {% load helpers %}
 
 {% block header %}
-    <div class="row">
+    <div class="row noprint">
         <div class="col-md-12">
             <ol class="breadcrumb">
                 <li><a href="{% url 'dcim:cable_list' %}">Cables</a></li>
@@ -10,7 +10,7 @@
             </ol>
         </div>
     </div>
-    <div class="pull-right">
+    <div class="pull-right noprint">
         {% if perms.dcim.change_cable %}
             <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

+ 2 - 2
netbox/templates/dcim/cable_list.html

@@ -2,7 +2,7 @@
 {% load buttons %}
 
 {% block content %}
-<div class="pull-right">
+<div class="pull-right noprint">
     {% if perms.dcim.add_cable %}
         {% import_button 'dcim:cable_import' %}
     {% endif %}
@@ -13,7 +13,7 @@
 	<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' %}
     </div>
-    <div class="col-md-3">
+    <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
     </div>
 </div>

+ 2 - 2
netbox/templates/dcim/console_connections_list.html

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

+ 15 - 15
netbox/templates/dcim/device.html

@@ -5,7 +5,7 @@
 {% block title %}{{ device }}{% endblock %}
 
 {% block header %}
-    <div class="row">
+    <div class="row noprint">
         <div class="col-sm-8 col-md-9">
         <ol class="breadcrumb">
             <li><a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a></li>
@@ -33,7 +33,7 @@
             </form>
         </div>
     </div>
-    <div class="pull-right">
+    <div class="pull-right noprint">
         {% if perms.dcim.change_device %}
             <div class="btn-group">
                 <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@@ -199,7 +199,7 @@
                             </tr>
                         {% endfor %}
                     </table>
-                    <div class="panel-footer text-right">
+                    <div class="panel-footer text-right noprint">
                         {% 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">
                                 <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Member
@@ -317,7 +317,7 @@
                         {% endfor %}
                     </table>
                     {% 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 %}
                                 <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
@@ -352,7 +352,7 @@
                         <form id="secret_form">
                             {% csrf_token %}
                         </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">
                                 <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
                                 Add secret
@@ -377,7 +377,7 @@
                     </div>
                 {% endif %}
                 {% 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">
                             <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
                         </a>
@@ -390,7 +390,7 @@
                 </div>
                 {% include 'inc/image_attachments.html' with images=device.images.all %}
                 {% 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">
                             <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
                             Attach an image
@@ -398,7 +398,7 @@
                     </div>
                 {% endif %}
             </div>
-            <div class="panel panel-default">
+            <div class="panel panel-default noprint">
                 <div class="panel-heading">
                     <strong>Related Devices</strong>
                 </div>
@@ -459,7 +459,7 @@
                             {% endfor %}
                         </tbody>
                     </table>
-                    <div class="panel-footer">
+                    <div class="panel-footer noprint">
                         {% 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">
                                 <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
@@ -493,7 +493,7 @@
                 <div class="panel panel-default">
                     <div class="panel-heading">
                         <strong>Interfaces</strong>
-                        <div class="pull-right">
+                        <div class="pull-right noprint">
                             <button class="btn btn-default btn-xs toggle-ips" selected="selected">
                                 <span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
                             </button>
@@ -521,7 +521,7 @@
                             {% endfor %}
                         </tbody>
                     </table>
-                    <div class="panel-footer">
+                    <div class="panel-footer noprint">
                         {% 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">
                                 <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
@@ -581,7 +581,7 @@
                             {% endfor %}
                         </tbody>
                     </table>
-                    <div class="panel-footer">
+                    <div class="panel-footer noprint">
                         {% 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">
                                 <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
@@ -636,7 +636,7 @@
                             {% endfor %}
                         </tbody>
                     </table>
-                    <div class="panel-footer">
+                    <div class="panel-footer noprint">
                         {% 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">
                                 <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
@@ -693,7 +693,7 @@
                                 {% endfor %}
                             </tbody>
                         </table>
-                        <div class="panel-footer">
+                        <div class="panel-footer noprint">
                             {% 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">
                                     <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
@@ -750,7 +750,7 @@
                                 {% endfor %}
                             </tbody>
                         </table>
-                        <div class="panel-footer">
+                        <div class="panel-footer noprint">
                             {% 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">
                                     <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename

+ 1 - 1
netbox/templates/dcim/device_inventory.html

@@ -53,7 +53,7 @@
                     </tbody>
                 </table>
                 {% 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">
                             <span class="fa fa-plus" aria-hidden="true"></span> Add Inventory Item
                         </a>

+ 2 - 2
netbox/templates/dcim/device_list.html

@@ -2,7 +2,7 @@
 {% load buttons %}
 
 {% block content %}
-<div class="pull-right">
+<div class="pull-right noprint">
     {% if perms.dcim.add_device %}
         {% add_button 'dcim:device_add' %}
         {% import_button 'dcim:device_import' %}
@@ -14,7 +14,7 @@
 	<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' %}
     </div>
-    <div class="col-md-3">
+    <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/tags_panel.html' %}
     </div>

+ 1 - 1
netbox/templates/dcim/devicerole_list.html

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

+ 2 - 2
netbox/templates/dcim/devicetype.html

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

+ 2 - 2
netbox/templates/dcim/devicetype_list.html

@@ -2,7 +2,7 @@
 {% load buttons %}
 
 {% block content %}
-<div class="pull-right">
+<div class="pull-right noprint">
     {% if perms.dcim.add_devicetype %}
         {% add_button 'dcim:devicetype_add' %}
         {% import_button 'dcim:devicetype_import' %}
@@ -14,7 +14,7 @@
 	<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' %}
     </div>
-    <div class="col-md-3">
+    <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/tags_panel.html' %}
     </div>

+ 1 - 1
netbox/templates/dcim/inc/consoleport.html

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

+ 1 - 1
netbox/templates/dcim/inc/consoleserverport.html

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

+ 1 - 1
netbox/templates/dcim/inc/devicebay.html

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

+ 1 - 1
netbox/templates/dcim/inc/devicetype_component_table.html

@@ -6,7 +6,7 @@
                 <strong>{{ title }}</strong>
             </div>
             {% include 'responsive_table.html' %}
-            <div class="panel-footer">
+            <div class="panel-footer noprint">
                 {% if table.rows %}
                     {% 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">

+ 1 - 1
netbox/templates/dcim/inc/frontport.html

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

+ 2 - 2
netbox/templates/dcim/inc/interface.html

@@ -134,7 +134,7 @@
     {% endif %}
 
     {# Buttons #}
-    <td class="text-right text-nowrap">
+    <td class="text-right text-nowrap noprint">
         {% if show_graphs %}
             {% 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">
@@ -231,7 +231,7 @@
                             </td>
 
                             {# Buttons #}
-                            <td class="text-right text-nowrap">
+                            <td class="text-right text-nowrap noprint">
                                 {% 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">
                                         <i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>

+ 1 - 1
netbox/templates/dcim/inc/inventoryitem.html

@@ -6,7 +6,7 @@
     <td>{{ item.serial }}</td>
     <td>{{ item.asset_tag|default:"" }}</td>
     <td>{{ item.description }}</td>
-    <td class="text-right">
+    <td class="text-right noprint">
         {% 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>
         {% endif %}

+ 1 - 1
netbox/templates/dcim/inc/poweroutlet.html

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

+ 1 - 1
netbox/templates/dcim/inc/powerport.html

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

+ 1 - 1
netbox/templates/dcim/inc/rearport.html

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

+ 2 - 2
netbox/templates/dcim/interface.html

@@ -2,7 +2,7 @@
 {% load helpers %}
 
 {% block header %}
-    <div class="row">
+    <div class="row noprint">
         <div class="col-md-12">
             <ol class="breadcrumb">
                 {% if interface.device %}
@@ -15,7 +15,7 @@
             </ol>
         </div>
     </div>
-    <div class="pull-right">
+    <div class="pull-right noprint">
         {% 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">
                 <span class="fa fa-pencil" aria-hidden="true"></span> Edit this interface

+ 2 - 2
netbox/templates/dcim/interface_connections_list.html

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

+ 2 - 2
netbox/templates/dcim/inventoryitem_list.html

@@ -3,7 +3,7 @@
 {% load helpers %}
 
 {% block content %}
-<div class="pull-right">
+<div class="pull-right noprint">
     {% if perms.dcim.add_devicetype %}
         {% import_button 'dcim:inventoryitem_import' %}
     {% endif %}
@@ -14,7 +14,7 @@
 	<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' %}
     </div>
-    <div class="col-md-3">
+    <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
     </div>
 </div>

+ 1 - 1
netbox/templates/dcim/manufacturer_list.html

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

+ 1 - 1
netbox/templates/dcim/platform_list.html

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

+ 2 - 2
netbox/templates/dcim/power_connections_list.html

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

+ 6 - 6
netbox/templates/dcim/rack.html

@@ -2,7 +2,7 @@
 {% load helpers %}
 
 {% block header %}
-    <div class="row">
+    <div class="row noprint">
         <div class="col-sm-8 col-md-9">
             <ol class="breadcrumb">
                 <li><a href="{% url 'dcim:rack_list' %}">Racks</a></li>
@@ -23,7 +23,7 @@
             </form>
         </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">
             <span class="fa fa-chevron-left" aria-hidden="true"></span> Previous Rack
         </a>
@@ -223,7 +223,7 @@
                 <div class="panel-body text-muted">None</div>
             {% endif %}
             {% 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">
                         <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
                         Add a non-racked device
@@ -237,7 +237,7 @@
             </div>
             {% include 'inc/image_attachments.html' with images=rack.images.all %}
             {% 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">
                         <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
                         Attach an image
@@ -271,7 +271,7 @@
                                 {{ resv.description }}<br />
                                 <small>{{ resv.user }} &middot; {{ resv.created }}</small>
                             </td>
-                            <td class="text-right">
+                            <td class="text-right noprint">
                                 {% if perms.dcim.change_rackreservation %}
                                     <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>
@@ -290,7 +290,7 @@
                 <div class="panel-body text-muted">None</div>
             {% endif %}
             {% 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">
                         <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
                         Add a reservation

+ 2 - 2
netbox/templates/dcim/rack_elevation_list.html

@@ -2,7 +2,7 @@
 {% load helpers %}
 
 {% 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=1 %}" class="btn btn-default{% if request.GET.face == '1' %} active{% endif %}">Rear</a>
 </div>
@@ -38,7 +38,7 @@
             <p>No racks found</p>
         </div>
     {% endif %}
-    <div class="col-md-3">
+    <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
     </div>
 </div>

+ 2 - 2
netbox/templates/dcim/rack_list.html

@@ -2,7 +2,7 @@
 {% load buttons %}
 
 {% block content %}
-<div class="pull-right">
+<div class="pull-right noprint">
     {% if perms.dcim.add_rack %}
         {% add_button 'dcim:rack_add' %}
         {% import_button 'dcim:rack_import' %}
@@ -14,7 +14,7 @@
 	<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' %}
     </div>
-    <div class="col-md-3">
+    <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/tags_panel.html' %}
     </div>

+ 2 - 2
netbox/templates/dcim/rackgroup_list.html

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

+ 1 - 1
netbox/templates/dcim/rackreservation_list.html

@@ -7,7 +7,7 @@
 	<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' %}
     </div>
-	<div class="col-md-3">
+	<div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 	</div>
 </div>

+ 1 - 1
netbox/templates/dcim/rackrole_list.html

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

+ 2 - 2
netbox/templates/dcim/region_list.html

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

+ 6 - 6
netbox/templates/dcim/site.html

@@ -4,7 +4,7 @@
 {% load helpers %}
 
 {% block header %}
-    <div class="row">
+    <div class="row noprint">
         <div class="col-sm-8 col-md-9">
             <ol class="breadcrumb">
                 <li><a href="{% url 'dcim:site_list' %}">Sites</a></li>
@@ -30,7 +30,7 @@
             </form>
         </div>
     </div>
-    <div class="pull-right">
+    <div class="pull-right noprint">
         {% 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">
                 <i class="fa fa-signal" aria-hidden="true"></i>
@@ -138,7 +138,7 @@
                     <td>Physical Address</td>
                     <td>
                         {% 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">
                                     <i class="glyphicon glyphicon-map-marker"></i> Map it
                                 </a>
@@ -157,7 +157,7 @@
                     <td>GPS Coordinates</td>
                     <td>
                         {% 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">
                                     <i class="glyphicon glyphicon-map-marker"></i> Map it
                                 </a>
@@ -251,7 +251,7 @@
                         <tr>
                             <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 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">
                                     <i class="fa fa-eye"></i>
                                 </a>
@@ -271,7 +271,7 @@
             </div>
             {% include 'inc/image_attachments.html' with images=site.images.all %}
             {% 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">
                         <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
                         Attach an image

+ 2 - 2
netbox/templates/dcim/site_list.html

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

+ 2 - 2
netbox/templates/dcim/virtualchassis_list.html

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

+ 2 - 2
netbox/templates/extras/configcontext.html

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

+ 2 - 2
netbox/templates/extras/configcontext_list.html

@@ -2,7 +2,7 @@
 {% load buttons %}
 
 {% block content %}
-    <div class="pull-right">
+    <div class="pull-right noprint">
         {% if perms.extras.add_configcontext %}
             {% add_button 'extras:configcontext_add' %}
         {% endif %}
@@ -12,7 +12,7 @@
         <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' %}
         </div>
-        <div class="col-md-3">
+        <div class="col-md-3 noprint">
             {% include 'inc/search_panel.html' %}
         </div>
     </div>

+ 2 - 2
netbox/templates/extras/objectchange.html

@@ -4,7 +4,7 @@
 {% block title %}{{ objectchange }}{% endblock %}
 
 {% block header %}
-    <div class="row">
+    <div class="row noprint">
         <div class="col-sm-8 col-md-9">
             <ol class="breadcrumb">
                 <li><a href="{% url 'extras:objectchange_list' %}">Changelog</a></li>
@@ -97,7 +97,7 @@
     </div>
     <div class="row">
         <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 %}
                 <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>

+ 2 - 2
netbox/templates/extras/objectchange_list.html

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

+ 2 - 2
netbox/templates/extras/report.html

@@ -4,7 +4,7 @@
 {% block title %}{{ report.name }}{% endblock %}
 
 {% block content %}
-    <div class="row">
+    <div class="row noprint">
         <div class="col-md-12">
             <ol class="breadcrumb">
                 <li><a href="{% url 'extras:report_list' %}">Reports</a></li>
@@ -14,7 +14,7 @@
         </div>
     </div>
     {% 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">
                 {% csrf_token %}
                 {{ run_form }}

+ 6 - 0
netbox/templates/extras/tag.html

@@ -29,6 +29,12 @@
                 Edit this tag
             </a>
         {% 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>
     <h1>{% block title %}Tag: {{ tag }}{% endblock %}</h1>
 {% endblock %}

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

@@ -14,7 +14,7 @@
                 </td>
                 <td>{{ attachment.size|filesizeformat }}</td>
                 <td>{{ attachment.created }}</td>
-                <td class="text-right">
+                <td class="text-right noprint">
                     {% if perms.extras.change_imageattachment %}
                         <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>

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

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

+ 2 - 2
netbox/templates/ipam/aggregate.html

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

+ 2 - 2
netbox/templates/ipam/aggregate_list.html

@@ -3,7 +3,7 @@
 {% load humanize %}
 
 {% block content %}
-<div class="pull-right">
+<div class="pull-right noprint">
     {% if perms.ipam.add_aggregate %}
         {% add_button 'ipam:aggregate_add' %}
         {% import_button 'ipam:aggregate_import' %}
@@ -15,7 +15,7 @@
 	<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' %}
 	</div>
-	<div class="col-md-3">
+	<div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/tags_panel.html' %}
         <div class="panel panel-default">

+ 1 - 1
netbox/templates/ipam/inc/service.html

@@ -13,7 +13,7 @@
         {% endfor %}
     </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">
             <i class="fa fa-history"></i>
         </a>

+ 3 - 3
netbox/templates/ipam/ipaddress.html

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

+ 2 - 2
netbox/templates/ipam/ipaddress_list.html

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

+ 2 - 2
netbox/templates/ipam/prefix.html

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

+ 2 - 2
netbox/templates/ipam/prefix_list.html

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

+ 2 - 2
netbox/templates/ipam/rir_list.html

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

+ 1 - 1
netbox/templates/ipam/role_list.html

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

+ 1 - 1
netbox/templates/ipam/service.html

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

+ 2 - 2
netbox/templates/ipam/service_list.html

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

+ 3 - 3
netbox/templates/ipam/vlan.html

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

+ 2 - 2
netbox/templates/ipam/vlan_list.html

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

Некоторые файлы не были показаны из-за большого количества измененных файлов