Quellcode durchsuchen

Merge branch 'feature' into docs-refresh

jeremystretch vor 3 Jahren
Ursprung
Commit
150c3d3a97
61 geänderte Dateien mit 378 neuen und 255 gelöschten Zeilen
  1. 1 1
      base_requirements.txt
  2. 9 0
      docs/_theme/main.html
  3. 1 1
      docs/models/dcim/frontport.md
  4. 22 1
      docs/release-notes/version-3.2.md
  5. 5 16
      docs/release-notes/version-3.3.md
  6. 2 0
      mkdocs.yml
  7. 2 2
      netbox/circuits/models/circuits.py
  8. 2 2
      netbox/circuits/models/providers.py
  9. 1 0
      netbox/dcim/forms/connections.py
  10. 2 2
      netbox/dcim/forms/object_import.py
  11. 2 2
      netbox/dcim/models/cables.py
  12. 2 0
      netbox/dcim/models/device_component_templates.py
  13. 13 10
      netbox/dcim/models/device_components.py
  14. 6 5
      netbox/dcim/models/devices.py
  15. 3 3
      netbox/dcim/models/power.py
  16. 2 2
      netbox/dcim/models/racks.py
  17. 5 5
      netbox/dcim/models/sites.py
  18. 25 25
      netbox/dcim/tables/template_code.py
  19. 3 3
      netbox/dcim/views.py
  20. 2 2
      netbox/extras/models/customfields.py
  21. 1 1
      netbox/ipam/models/fhrp.py
  22. 9 9
      netbox/ipam/models/ip.py
  23. 2 2
      netbox/ipam/models/vrfs.py
  24. 2 2
      netbox/netbox/models/__init__.py
  25. 6 5
      netbox/netbox/tables/columns.py
  26. 9 8
      netbox/netbox/views/generic/bulk_views.py
  27. 3 3
      netbox/netbox/views/generic/object_views.py
  28. 0 0
      netbox/project-static/dist/netbox-dark.css
  29. 0 0
      netbox/project-static/dist/netbox-light.css
  30. 0 0
      netbox/project-static/dist/netbox-print.css
  31. 0 0
      netbox/project-static/dist/netbox.js
  32. 0 0
      netbox/project-static/dist/netbox.js.map
  33. 44 0
      netbox/project-static/src/search.ts
  34. 22 4
      netbox/project-static/styles/netbox.scss
  35. 8 0
      netbox/project-static/styles/overrides.scss
  36. 4 0
      netbox/project-static/styles/theme-dark.scss
  37. 1 2
      netbox/project-static/styles/theme-light.scss
  38. 6 0
      netbox/project-static/styles/utilities.scss
  39. 3 3
      netbox/templates/dcim/consoleport.html
  40. 3 3
      netbox/templates/dcim/consoleserverport.html
  41. 67 66
      netbox/templates/dcim/device/interfaces.html
  42. 6 6
      netbox/templates/dcim/frontport.html
  43. 4 4
      netbox/templates/dcim/interface.html
  44. 1 1
      netbox/templates/dcim/powerfeed.html
  45. 1 1
      netbox/templates/dcim/poweroutlet.html
  46. 2 2
      netbox/templates/dcim/powerport.html
  47. 4 4
      netbox/templates/dcim/rearport.html
  48. 16 0
      netbox/templates/inc/panels/contacts.html
  49. 12 22
      netbox/templates/inc/table_controls_htmx.html
  50. 4 4
      netbox/tenancy/models/contacts.py
  51. 2 2
      netbox/tenancy/models/tenants.py
  52. 7 5
      netbox/users/api/views.py
  53. 2 1
      netbox/users/views.py
  54. 1 0
      netbox/utilities/forms/fields/fields.py
  55. 1 1
      netbox/utilities/forms/forms.py
  56. 2 2
      netbox/utilities/templatetags/builtins/filters.py
  57. 1 3
      netbox/utilities/templatetags/helpers.py
  58. 1 1
      netbox/virtualization/forms/filtersets.py
  59. 5 5
      netbox/virtualization/models.py
  60. 2 0
      netbox/wireless/models.py
  61. 4 1
      requirements.txt

+ 1 - 1
base_requirements.txt

@@ -4,7 +4,7 @@ bleach
 
 
 # The Python web framework on which NetBox is built
 # The Python web framework on which NetBox is built
 # https://github.com/django/django
 # https://github.com/django/django
-Django
+Django<4.1
 
 
 # Django middleware which permits cross-domain API requests
 # Django middleware which permits cross-domain API requests
 # https://github.com/OttoYiu/django-cors-headers
 # https://github.com/OttoYiu/django-cors-headers

+ 9 - 0
docs/_theme/main.html

@@ -0,0 +1,9 @@
+{% extends "base.html" %}
+
+{% block site_meta %}
+  {{ super() }}
+  {# Disable search indexing unless we're building for ReadTheDocs #}
+  {% if not config.extra.readthedocs %}
+    <meta name="robots" content="noindex">
+  {% endif %}
+{% endblock %}

+ 1 - 1
docs/models/dcim/frontport.md

@@ -1,3 +1,3 @@
 ## Front Ports
 ## Front Ports
 
 
-Front ports are pass-through ports used to represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. Each port is assigned a physical type, and must be mapped to a specific rear port on the same device. A single rear port may be mapped to multiple rear ports, using numeric positions to annotate the specific alignment of each.
+Front ports are pass-through ports used to represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. Each port is assigned a physical type, and must be mapped to a specific rear port on the same device. A single rear port may be mapped to multiple front ports, using numeric positions to annotate the specific alignment of each.

+ 22 - 1
docs/release-notes/version-3.2.md

@@ -1,6 +1,20 @@
 # NetBox v3.2
 # NetBox v3.2
 
 
-## v3.2.8 (FUTURE)
+## v3.2.9 (FUTURE)
+
+### Enhancements
+
+* [#9161](https://github.com/netbox-community/netbox/issues/9161) - Pretty print JSON custom field data when editing
+* [#9625](https://github.com/netbox-community/netbox/issues/9625) - Add phone & email details to contacts panel
+* [#9857](https://github.com/netbox-community/netbox/issues/9857) - Add clear button to quick search fields
+
+### Bug Fixes
+
+* [#9986](https://github.com/netbox-community/netbox/issues/9986) - Workaround for upstream timezone data bug
+
+---
+
+## v3.2.8 (2022-08-08)
 
 
 ### Enhancements
 ### Enhancements
 
 
@@ -11,13 +25,20 @@
 * [#9881](https://github.com/netbox-community/netbox/issues/9881) - Increase granularity in utilization graph values
 * [#9881](https://github.com/netbox-community/netbox/issues/9881) - Increase granularity in utilization graph values
 * [#9882](https://github.com/netbox-community/netbox/issues/9882) - Add manufacturer column to modules table
 * [#9882](https://github.com/netbox-community/netbox/issues/9882) - Add manufacturer column to modules table
 * [#9883](https://github.com/netbox-community/netbox/issues/9883) - Linkify location column in power panels table
 * [#9883](https://github.com/netbox-community/netbox/issues/9883) - Linkify location column in power panels table
+* [#9906](https://github.com/netbox-community/netbox/issues/9906) - Include `color` attribute in front & rear port YAML import/export
 
 
 ### Bug Fixes
 ### Bug Fixes
 
 
+* [#9827](https://github.com/netbox-community/netbox/issues/9827) - Fix assignment of module bay position during bulk creation
 * [#9871](https://github.com/netbox-community/netbox/issues/9871) - Fix utilization graph value alignments
 * [#9871](https://github.com/netbox-community/netbox/issues/9871) - Fix utilization graph value alignments
 * [#9884](https://github.com/netbox-community/netbox/issues/9884) - Prevent querying assigned VRF on prefix object init
 * [#9884](https://github.com/netbox-community/netbox/issues/9884) - Prevent querying assigned VRF on prefix object init
 * [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk
 * [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk
 * [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization
 * [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization
+* [#9919](https://github.com/netbox-community/netbox/issues/9919) - Fix potential XSS avenue via linked objects in tables
+* [#9948](https://github.com/netbox-community/netbox/issues/9948) - Fix TypeError exception when requesting API tokens list as non-authenticated user
+* [#9949](https://github.com/netbox-community/netbox/issues/9949) - Fix KeyError exception resulting from invalid API token provisioning request
+* [#9950](https://github.com/netbox-community/netbox/issues/9950) - Prevent redirection to arbitrary URLs via `next` parameter on login URL
+* [#9952](https://github.com/netbox-community/netbox/issues/9952) - Prevent InvalidMove when attempting to assign a nested child object as parent
 
 
 ---
 ---
 
 

+ 5 - 16
docs/release-notes/version-3.3.md

@@ -97,22 +97,11 @@ Custom field UI visibility has no impact on API operation.
 * [#9536](https://github.com/netbox-community/netbox/issues/9536) - Track API token usage times
 * [#9536](https://github.com/netbox-community/netbox/issues/9536) - Track API token usage times
 * [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location
 * [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location
 
 
-### Bug Fixes (from Beta1)
-
-* [#9728](https://github.com/netbox-community/netbox/issues/9728) - Fix validation when assigning a virtual machine to a device
-* [#9729](https://github.com/netbox-community/netbox/issues/9729) - Fix ordering of content type creation to ensure compatability with demo data
-* [#9730](https://github.com/netbox-community/netbox/issues/9730) - Fix validation error when creating a new cable via UI form
-* [#9733](https://github.com/netbox-community/netbox/issues/9733) - Handle split paths during trace when fanning out to front ports with differing cables
-* [#9765](https://github.com/netbox-community/netbox/issues/9765) - Report correct segment count under cable trace UI view
-* [#9778](https://github.com/netbox-community/netbox/issues/9778) - Fix exception during cable deletion after deleting a connected termination
-* [#9788](https://github.com/netbox-community/netbox/issues/9788) - Ensure denormalized fields on CableTermination are kept in sync with related objects
-* [#9789](https://github.com/netbox-community/netbox/issues/9789) - Fix rendering of cable traces ending at provider networks
-* [#9794](https://github.com/netbox-community/netbox/issues/9794) - Fix link to connect a rear port to a circuit termination
-* [#9818](https://github.com/netbox-community/netbox/issues/9818) - Fix circuit side selection when connecting a cable to a circuit termination
-* [#9829](https://github.com/netbox-community/netbox/issues/9829) - Arrange custom fields by group when editing objects
-* [#9843](https://github.com/netbox-community/netbox/issues/9843) - Fix rendering of custom field values (regression from #9647)
-* [#9844](https://github.com/netbox-community/netbox/issues/9844) - Fix interface api request when creating/editing L2VPN termination
-* [#9847](https://github.com/netbox-community/netbox/issues/9847) - Respect `desc_units` when ordering rack units
+### Bug Fixes (from Beta2)
+
+* [#9900](https://github.com/netbox-community/netbox/issues/9900) - Pre-populate site & rack fields for cable connection form
+* [#9938](https://github.com/netbox-community/netbox/issues/9938) - Exclude virtual interfaces from terminations list when connecting a cable
+* [#9939](https://github.com/netbox-community/netbox/issues/9939) - Fix list of next nodes for split paths under trace view
 
 
 ### Plugins API
 ### Plugins API
 
 

+ 2 - 0
mkdocs.yml

@@ -5,6 +5,7 @@ repo_name: netbox-community/netbox
 repo_url: https://github.com/netbox-community/netbox
 repo_url: https://github.com/netbox-community/netbox
 theme:
 theme:
   name: material
   name: material
+  custom_dir: docs/_theme/
   icon:
   icon:
     repo: fontawesome/brands/github
     repo: fontawesome/brands/github
   palette:
   palette:
@@ -37,6 +38,7 @@ plugins:
             show_root_toc_entry: false
             show_root_toc_entry: false
             show_source: false
             show_source: false
 extra:
 extra:
+  readthedocs: !ENV READTHEDOCS
   social:
   social:
     - icon: fontawesome/brands/github
     - icon: fontawesome/brands/github
       link: https://github.com/netbox-community/netbox
       link: https://github.com/netbox-community/netbox

+ 2 - 2
netbox/circuits/models/circuits.py

@@ -125,9 +125,9 @@ class Circuit(NetBoxModel):
         null=True
         null=True
     )
     )
 
 
-    clone_fields = [
+    clone_fields = (
         'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description',
         'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description',
-    ]
+    )
 
 
     class Meta:
     class Meta:
         ordering = ['provider', 'cid']
         ordering = ['provider', 'cid']

+ 2 - 2
netbox/circuits/models/providers.py

@@ -61,9 +61,9 @@ class Provider(NetBoxModel):
         to='tenancy.ContactAssignment'
         to='tenancy.ContactAssignment'
     )
     )
 
 
-    clone_fields = [
+    clone_fields = (
         'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
         'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
-    ]
+    )
 
 
     class Meta:
     class Meta:
         ordering = ['name']
         ordering = ['name']

+ 1 - 0
netbox/dcim/forms/connections.py

@@ -84,6 +84,7 @@ def get_cable_form(a_type, b_type):
                         disabled_indicator='_occupied',
                         disabled_indicator='_occupied',
                         query_params={
                         query_params={
                             'device_id': f'$termination_{cable_end}_device',
                             'device_id': f'$termination_{cable_end}_device',
+                            'kind': 'physical',  # Exclude virtual interfaces
                         }
                         }
                     )
                     )
 
 

+ 2 - 2
netbox/dcim/forms/object_import.py

@@ -156,7 +156,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
     class Meta:
     class Meta:
         model = FrontPortTemplate
         model = FrontPortTemplate
         fields = [
         fields = [
-            'device_type', 'module_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description',
+            'device_type', 'module_type', 'name', 'type', 'color', 'rear_port', 'rear_port_position', 'label', 'description',
         ]
         ]
 
 
 
 
@@ -168,7 +168,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
     class Meta:
     class Meta:
         model = RearPortTemplate
         model = RearPortTemplate
         fields = [
         fields = [
-            'device_type', 'module_type', 'name', 'type', 'positions', 'label', 'description',
+            'device_type', 'module_type', 'name', 'type', 'color', 'positions', 'label', 'description',
         ]
         ]
 
 
 
 

+ 2 - 2
netbox/dcim/models/cables.py

@@ -677,6 +677,6 @@ class CablePath(models.Model):
         """
         """
         Return all available next segments in a split cable path.
         Return all available next segments in a split cable path.
         """
         """
-        rearport = path_node_to_object(self._nodes[-1])
+        rearports = self.path_objects[-1]
 
 
-        return FrontPort.objects.filter(rear_port=rearport)
+        return FrontPort.objects.filter(rear_port__in=rearports)

+ 2 - 0
netbox/dcim/models/device_component_templates.py

@@ -478,6 +478,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
         return {
         return {
             'name': self.name,
             'name': self.name,
             'type': self.type,
             'type': self.type,
+            'color': self.color,
             'rear_port': self.rear_port.name,
             'rear_port': self.rear_port.name,
             'rear_port_position': self.rear_port_position,
             'rear_port_position': self.rear_port_position,
             'label': self.label,
             'label': self.label,
@@ -527,6 +528,7 @@ class RearPortTemplate(ModularComponentTemplateModel):
         return {
         return {
             'name': self.name,
             'name': self.name,
             'type': self.type,
             'type': self.type,
+            'color': self.color,
             'positions': self.positions,
             'positions': self.positions,
             'label': self.label,
             'label': self.label,
             'description': self.description,
             'description': self.description,

+ 13 - 10
netbox/dcim/models/device_components.py

@@ -263,7 +263,7 @@ class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint):
         help_text='Port speed in bits per second'
         help_text='Port speed in bits per second'
     )
     )
 
 
-    clone_fields = ['device', 'type', 'speed']
+    clone_fields = ('device', 'module', 'type', 'speed')
 
 
     class Meta:
     class Meta:
         ordering = ('device', '_name')
         ordering = ('device', '_name')
@@ -290,7 +290,7 @@ class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
         help_text='Port speed in bits per second'
         help_text='Port speed in bits per second'
     )
     )
 
 
-    clone_fields = ['device', 'type', 'speed']
+    clone_fields = ('device', 'module', 'type', 'speed')
 
 
     class Meta:
     class Meta:
         ordering = ('device', '_name')
         ordering = ('device', '_name')
@@ -327,7 +327,7 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
         help_text="Allocated power draw (watts)"
         help_text="Allocated power draw (watts)"
     )
     )
 
 
-    clone_fields = ['device', 'maximum_draw', 'allocated_draw']
+    clone_fields = ('device', 'module', 'maximum_draw', 'allocated_draw')
 
 
     class Meta:
     class Meta:
         ordering = ('device', '_name')
         ordering = ('device', '_name')
@@ -441,7 +441,7 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint):
         help_text="Phase (for three-phase feeds)"
         help_text="Phase (for three-phase feeds)"
     )
     )
 
 
-    clone_fields = ['device', 'type', 'power_port', 'feed_leg']
+    clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg')
 
 
     class Meta:
     class Meta:
         ordering = ('device', '_name')
         ordering = ('device', '_name')
@@ -672,7 +672,10 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
         related_query_name='interface',
         related_query_name='interface',
     )
     )
 
 
-    clone_fields = ['device', 'parent', 'bridge', 'lag', 'type', 'mgmt_only', 'poe_mode', 'poe_type']
+    clone_fields = (
+        'device', 'module', 'parent', 'bridge', 'lag', 'type', 'mgmt_only', 'mtu', 'mode', 'speed', 'duplex', 'rf_role',
+        'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'poe_mode', 'poe_type', 'vrf',
+    )
 
 
     class Meta:
     class Meta:
         ordering = ('device', CollateAsChar('_name'))
         ordering = ('device', CollateAsChar('_name'))
@@ -890,7 +893,7 @@ class FrontPort(ModularComponentModel, CabledObjectModel):
         ]
         ]
     )
     )
 
 
-    clone_fields = ['device', 'type']
+    clone_fields = ('device', 'type', 'color')
 
 
     class Meta:
     class Meta:
         ordering = ('device', '_name')
         ordering = ('device', '_name')
@@ -937,7 +940,7 @@ class RearPort(ModularComponentModel, CabledObjectModel):
             MaxValueValidator(REARPORT_POSITIONS_MAX)
             MaxValueValidator(REARPORT_POSITIONS_MAX)
         ]
         ]
     )
     )
-    clone_fields = ['device', 'type', 'positions']
+    clone_fields = ('device', 'type', 'color', 'positions')
 
 
     class Meta:
     class Meta:
         ordering = ('device', '_name')
         ordering = ('device', '_name')
@@ -972,7 +975,7 @@ class ModuleBay(ComponentModel):
         help_text='Identifier to reference when renaming installed components'
         help_text='Identifier to reference when renaming installed components'
     )
     )
 
 
-    clone_fields = ['device']
+    clone_fields = ('device',)
 
 
     class Meta:
     class Meta:
         ordering = ('device', '_name')
         ordering = ('device', '_name')
@@ -994,7 +997,7 @@ class DeviceBay(ComponentModel):
         null=True
         null=True
     )
     )
 
 
-    clone_fields = ['device']
+    clone_fields = ('device',)
 
 
     class Meta:
     class Meta:
         ordering = ('device', '_name')
         ordering = ('device', '_name')
@@ -1131,7 +1134,7 @@ class InventoryItem(MPTTModel, ComponentModel):
 
 
     objects = TreeManager()
     objects = TreeManager()
 
 
-    clone_fields = ['device', 'parent', 'role', 'manufacturer', 'part_id']
+    clone_fields = ('device', 'parent', 'role', 'manufacturer', 'part_id',)
 
 
     class Meta:
     class Meta:
         ordering = ('device__id', 'parent__id', '_name')
         ordering = ('device__id', 'parent__id', '_name')

+ 6 - 5
netbox/dcim/models/devices.py

@@ -135,9 +135,9 @@ class DeviceType(NetBoxModel):
         blank=True
         blank=True
     )
     )
 
 
-    clone_fields = [
+    clone_fields = (
         'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
         'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
-    ]
+    )
 
 
     class Meta:
     class Meta:
         ordering = ['manufacturer', 'model']
         ordering = ['manufacturer', 'model']
@@ -630,9 +630,10 @@ class Device(NetBoxModel, ConfigContextModel):
 
 
     objects = ConfigContextModelQuerySet.as_manager()
     objects = ConfigContextModelQuerySet.as_manager()
 
 
-    clone_fields = [
-        'device_type', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'status', 'airflow', 'cluster',
-    ]
+    clone_fields = (
+        'device_type', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'face', 'status', 'airflow',
+        'cluster', 'virtual_chassis',
+    )
 
 
     class Meta:
     class Meta:
         ordering = ('_name', 'pk')  # Name may be null
         ordering = ('_name', 'pk')  # Name may be null

+ 3 - 3
netbox/dcim/models/power.py

@@ -126,10 +126,10 @@ class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel):
         blank=True
         blank=True
     )
     )
 
 
-    clone_fields = [
+    clone_fields = (
         'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
         'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
-        'max_utilization', 'available_power',
-    ]
+        'max_utilization',
+    )
 
 
     class Meta:
     class Meta:
         ordering = ['power_panel', 'name']
         ordering = ['power_panel', 'name']

+ 2 - 2
netbox/dcim/models/racks.py

@@ -183,10 +183,10 @@ class Rack(NetBoxModel):
         to='extras.ImageAttachment'
         to='extras.ImageAttachment'
     )
     )
 
 
-    clone_fields = [
+    clone_fields = (
         'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
         'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
         'outer_depth', 'outer_unit',
         'outer_depth', 'outer_unit',
-    ]
+    )
 
 
     class Meta:
     class Meta:
         ordering = ('site', 'location', '_name', 'pk')  # (site, location, name) may be non-unique
         ordering = ('site', 'location', '_name', 'pk')  # (site, location, name) may be non-unique

+ 5 - 5
netbox/dcim/models/sites.py

@@ -295,10 +295,10 @@ class Site(NetBoxModel):
         to='extras.ImageAttachment'
         to='extras.ImageAttachment'
     )
     )
 
 
-    clone_fields = [
-        'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description', 'physical_address',
-        'shipping_address', 'latitude', 'longitude',
-    ]
+    clone_fields = (
+        'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'physical_address', 'shipping_address',
+        'latitude', 'longitude', 'description',
+    )
 
 
     class Meta:
     class Meta:
         ordering = ('_name',)
         ordering = ('_name',)
@@ -372,7 +372,7 @@ class Location(NestedGroupModel):
         to='extras.ImageAttachment'
         to='extras.ImageAttachment'
     )
     )
 
 
-    clone_fields = ['site', 'parent', 'status', 'tenant', 'description']
+    clone_fields = ('site', 'parent', 'status', 'tenant', 'description')
 
 
     class Meta:
     class Meta:
         ordering = ['site', 'name']
         ordering = ['site', 'name']

+ 25 - 25
netbox/dcim/tables/template_code.py

@@ -121,9 +121,9 @@ CONSOLEPORT_BUTTONS = """
             <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
             <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
         </button>
         </button>
         <ul class="dropdown-menu dropdown-menu-end">
         <ul class="dropdown-menu dropdown-menu-end">
-            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleserverport&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Console Server Port</a></li>
-            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Front Port</a></li>
-            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Rear Port</a></li>
+            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleserverport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Console Server Port</a></li>
+            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Front Port</a></li>
+            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Rear Port</a></li>
         </ul>
         </ul>
     </span>
     </span>
 {% else %}
 {% else %}
@@ -153,9 +153,9 @@ CONSOLESERVERPORT_BUTTONS = """
             <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
             <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
         </button>
         </button>
         <ul class="dropdown-menu dropdown-menu-end">
         <ul class="dropdown-menu dropdown-menu-end">
-            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Port</a></li>
-            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Front Port</a></li>
-            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Rear Port</a></li>
+            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Port</a></li>
+            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Front Port</a></li>
+            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Rear Port</a></li>
         </ul>
         </ul>
     </span>
     </span>
 {% else %}
 {% else %}
@@ -185,8 +185,8 @@ POWERPORT_BUTTONS = """
             <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
             <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
         </button>
         </button>
         <ul class="dropdown-menu dropdown-menu-end">
         <ul class="dropdown-menu dropdown-menu-end">
-            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerport&a_terminations={{ record.pk }}&b_terminations_type=dcim.poweroutlet&return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Outlet</a></li>
-            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerport&a_terminations={{ record.pk }}&b_terminations_type=dcim.powerfeed&return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Feed</a></li>
+            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerport&a_terminations={{ record.pk }}&b_terminations_type=dcim.poweroutlet&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Outlet</a></li>
+            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerport&a_terminations={{ record.pk }}&b_terminations_type=dcim.powerfeed&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Feed</a></li>
         </ul>
         </ul>
     </span>
     </span>
 {% else %}
 {% else %}
@@ -212,7 +212,7 @@ POWEROUTLET_BUTTONS = """
     <a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
     <a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
     <a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
     <a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
     {% if not record.mark_connected %}
     {% if not record.mark_connected %}
-        <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ record.pk }}&b_terminations_type=dcim.powerport&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" title="Connect" class="btn btn-success btn-sm">
+        <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ record.pk }}&b_terminations_type=dcim.powerport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" title="Connect" class="btn btn-success btn-sm">
             <i class="mdi mdi-ethernet-cable" aria-hidden="true"></i>
             <i class="mdi mdi-ethernet-cable" aria-hidden="true"></i>
         </a>
         </a>
     {% else %}
     {% else %}
@@ -262,10 +262,10 @@ INTERFACE_BUTTONS = """
             <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
             <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
         </button>
         </button>
         <ul class="dropdown-menu dropdown-menu-end">
         <ul class="dropdown-menu dropdown-menu-end">
-            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=dcim.interface&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Interface</a></li>
-            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Front Port</a></li>
-            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Rear Port</a></li>
-            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuittermination&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Circuit Termination</a></li>
+            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Interface</a></li>
+            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Front Port</a></li>
+            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Rear Port</a></li>
+            <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ object.site.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Circuit Termination</a></li>
         </ul>
         </ul>
     </span>
     </span>
     {% else %}
     {% else %}
@@ -301,12 +301,12 @@ FRONTPORT_BUTTONS = """
                 <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
                 <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
             </button>
             </button>
             <ul class="dropdown-menu dropdown-menu-end">
             <ul class="dropdown-menu dropdown-menu-end">
-                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.interface&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Interface</a></li>
-                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleserverport&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Server Port</a></li>
-                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Port</a></li>
-                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Front Port</a></li>
-                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Rear Port</a></li>
-                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuittermination&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Circuit Termination</a></li>
+                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Interface</a></li>
+                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleserverport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Server Port</a></li>
+                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Port</a></li>
+                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Front Port</a></li>
+                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Rear Port</a></li>
+                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ object.site.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Circuit Termination</a></li>
             </ul>
             </ul>
         </span>
         </span>
     {% else %}
     {% else %}
@@ -338,12 +338,12 @@ REARPORT_BUTTONS = """
                 <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
                 <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
             </button>
             </button>
             <ul class="dropdown-menu dropdown-menu-end">
             <ul class="dropdown-menu dropdown-menu-end">
-                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.interface&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Interface</a></li>
-                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleserverport&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Console Server Port</a></li>
-                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Console Port</a></li>
-                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Front Port</a></li>
-                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Rear Port</a></li>
-                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuittermination&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Circuit Termination</a></li>
+                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Interface</a></li>
+                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleserverport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Console Server Port</a></li>
+                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Console Port</a></li>
+                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Front Port</a></li>
+                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Rear Port</a></li>
+                <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ object.site.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Circuit Termination</a></li>
             </ul>
             </ul>
         </span>
         </span>
     {% else %}
     {% else %}

+ 3 - 3
netbox/dcim/views.py

@@ -2721,6 +2721,7 @@ class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView):
     filterset = filtersets.DeviceFilterSet
     filterset = filtersets.DeviceFilterSet
     table = tables.DeviceTable
     table = tables.DeviceTable
     default_return_url = 'dcim:device_list'
     default_return_url = 'dcim:device_list'
+    patterned_fields = ('name', 'label', 'position')
 
 
 
 
 class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
 class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
@@ -3066,7 +3067,7 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix
             if membership_form.is_valid():
             if membership_form.is_valid():
 
 
                 membership_form.save()
                 membership_form.save()
-                msg = 'Added member <a href="{}">{}</a>'.format(device.get_absolute_url(), escape(device))
+                msg = f'Added member <a href="{device.get_absolute_url()}">{escape(device)}</a>'
                 messages.success(request, mark_safe(msg))
                 messages.success(request, mark_safe(msg))
 
 
                 if '_addanother' in request.POST:
                 if '_addanother' in request.POST:
@@ -3111,8 +3112,7 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
         # Protect master device from being removed
         # Protect master device from being removed
         virtual_chassis = VirtualChassis.objects.filter(master=device).first()
         virtual_chassis = VirtualChassis.objects.filter(master=device).first()
         if virtual_chassis is not None:
         if virtual_chassis is not None:
-            msg = 'Unable to remove master device {} from the virtual chassis.'.format(escape(device))
-            messages.error(request, mark_safe(msg))
+            messages.error(request, f'Unable to remove master device {device} from the virtual chassis.')
             return redirect(device.get_absolute_url())
             return redirect(device.get_absolute_url())
 
 
         if form.is_valid():
         if form.is_valid():

+ 2 - 2
netbox/extras/models/customfields.py

@@ -18,7 +18,7 @@ from netbox.models.features import ExportTemplatesMixin, WebhooksMixin
 from utilities import filters
 from utilities import filters
 from utilities.forms import (
 from utilities.forms import (
     CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
     CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
-    LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice,
+    JSONField, LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice,
 )
 )
 from utilities.querysets import RestrictedQuerySet
 from utilities.querysets import RestrictedQuerySet
 from utilities.validators import validate_regex
 from utilities.validators import validate_regex
@@ -355,7 +355,7 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
 
 
         # JSON
         # JSON
         elif self.type == CustomFieldTypeChoices.TYPE_JSON:
         elif self.type == CustomFieldTypeChoices.TYPE_JSON:
-            field = forms.JSONField(required=required, initial=initial)
+            field = JSONField(required=required, initial=initial)
 
 
         # Object
         # Object
         elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
         elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:

+ 1 - 1
netbox/ipam/models/fhrp.py

@@ -48,7 +48,7 @@ class FHRPGroup(NetBoxModel):
         related_query_name='fhrpgroup'
         related_query_name='fhrpgroup'
     )
     )
 
 
-    clone_fields = ('protocol', 'auth_type', 'auth_key')
+    clone_fields = ('protocol', 'auth_type', 'auth_key', 'description')
 
 
     class Meta:
     class Meta:
         ordering = ['protocol', 'group_id', 'pk']
         ordering = ['protocol', 'group_id', 'pk']

+ 9 - 9
netbox/ipam/models/ip.py

@@ -175,9 +175,9 @@ class Aggregate(GetAvailablePrefixesMixin, NetBoxModel):
         blank=True
         blank=True
     )
     )
 
 
-    clone_fields = [
+    clone_fields = (
         'rir', 'tenant', 'date_added', 'description',
         'rir', 'tenant', 'date_added', 'description',
-    ]
+    )
 
 
     class Meta:
     class Meta:
         ordering = ('prefix', 'pk')  # prefix may be non-unique
         ordering = ('prefix', 'pk')  # prefix may be non-unique
@@ -360,9 +360,9 @@ class Prefix(GetAvailablePrefixesMixin, NetBoxModel):
 
 
     objects = PrefixQuerySet.as_manager()
     objects = PrefixQuerySet.as_manager()
 
 
-    clone_fields = [
+    clone_fields = (
         'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
         'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
-    ]
+    )
 
 
     class Meta:
     class Meta:
         ordering = (F('vrf').asc(nulls_first=True), 'prefix', 'pk')  # (vrf, prefix) may be non-unique
         ordering = (F('vrf').asc(nulls_first=True), 'prefix', 'pk')  # (vrf, prefix) may be non-unique
@@ -608,9 +608,9 @@ class IPRange(NetBoxModel):
         blank=True
         blank=True
     )
     )
 
 
-    clone_fields = [
+    clone_fields = (
         'vrf', 'tenant', 'status', 'role', 'description',
         'vrf', 'tenant', 'status', 'role', 'description',
-    ]
+    )
 
 
     class Meta:
     class Meta:
         ordering = (F('vrf').asc(nulls_first=True), 'start_address', 'pk')  # (vrf, start_address) may be non-unique
         ordering = (F('vrf').asc(nulls_first=True), 'start_address', 'pk')  # (vrf, start_address) may be non-unique
@@ -836,9 +836,9 @@ class IPAddress(NetBoxModel):
 
 
     objects = IPAddressManager()
     objects = IPAddressManager()
 
 
-    clone_fields = [
-        'vrf', 'tenant', 'status', 'role', 'description',
-    ]
+    clone_fields = (
+        'vrf', 'tenant', 'status', 'role', 'dns_name', 'description',
+    )
 
 
     class Meta:
     class Meta:
         ordering = ('address', 'pk')  # address may be non-unique
         ordering = ('address', 'pk')  # address may be non-unique

+ 2 - 2
netbox/ipam/models/vrfs.py

@@ -55,9 +55,9 @@ class VRF(NetBoxModel):
         blank=True
         blank=True
     )
     )
 
 
-    clone_fields = [
+    clone_fields = (
         'tenant', 'enforce_unique', 'description',
         'tenant', 'enforce_unique', 'description',
-    ]
+    )
 
 
     class Meta:
     class Meta:
         ordering = ('name', 'rd', 'pk')  # (name, rd) may be non-unique
         ordering = ('name', 'rd', 'pk')  # (name, rd) may be non-unique

+ 2 - 2
netbox/netbox/models/__init__.py

@@ -109,9 +109,9 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
         super().clean()
         super().clean()
 
 
         # An MPTT model cannot be its own parent
         # An MPTT model cannot be its own parent
-        if self.pk and self.parent_id == self.pk:
+        if self.pk and self.parent and self.parent in self.get_descendants(include_self=True):
             raise ValidationError({
             raise ValidationError({
-                "parent": "Cannot assign self as parent."
+                "parent": f"Cannot assign self or child {self._meta.verbose_name} as parent."
             })
             })
 
 
 
 

+ 6 - 5
netbox/netbox/tables/columns.py

@@ -7,6 +7,7 @@ from django.contrib.auth.models import AnonymousUser
 from django.db.models import DateField, DateTimeField
 from django.db.models import DateField, DateTimeField
 from django.template import Context, Template
 from django.template import Context, Template
 from django.urls import reverse
 from django.urls import reverse
+from django.utils.html import escape
 from django.utils.formats import date_format
 from django.utils.formats import date_format
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 from django_tables2.columns import library
 from django_tables2.columns import library
@@ -428,8 +429,8 @@ class CustomFieldColumn(tables.Column):
     @staticmethod
     @staticmethod
     def _likify_item(item):
     def _likify_item(item):
         if hasattr(item, 'get_absolute_url'):
         if hasattr(item, 'get_absolute_url'):
-            return f'<a href="{item.get_absolute_url()}">{item}</a>'
-        return item
+            return f'<a href="{item.get_absolute_url()}">{escape(item)}</a>'
+        return escape(item)
 
 
     def render(self, value):
     def render(self, value):
         if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is True:
         if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is True:
@@ -437,13 +438,13 @@ class CustomFieldColumn(tables.Column):
         if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is False:
         if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is False:
             return mark_safe('<i class="mdi mdi-close-thick text-danger"></i>')
             return mark_safe('<i class="mdi mdi-close-thick text-danger"></i>')
         if self.customfield.type == CustomFieldTypeChoices.TYPE_URL:
         if self.customfield.type == CustomFieldTypeChoices.TYPE_URL:
-            return mark_safe(f'<a href="{value}">{value}</a>')
+            return mark_safe(f'<a href="{escape(value)}">{escape(value)}</a>')
         if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
         if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
             return ', '.join(v for v in value)
             return ', '.join(v for v in value)
         if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
         if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
-            return mark_safe(', '.join([
+            return mark_safe(', '.join(
                 self._likify_item(obj) for obj in self.customfield.deserialize(value)
                 self._likify_item(obj) for obj in self.customfield.deserialize(value)
-            ]))
+            ))
         if value is not None:
         if value is not None:
             obj = self.customfield.deserialize(value)
             obj = self.customfield.deserialize(value)
             return mark_safe(self._likify_item(obj))
             return mark_safe(self._likify_item(obj))

+ 9 - 8
netbox/netbox/views/generic/bulk_views.py

@@ -770,6 +770,7 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
     model_form = None
     model_form = None
     filterset = None
     filterset = None
     table = None
     table = None
+    patterned_fields = ('name', 'label')
 
 
     def get_required_permission(self):
     def get_required_permission(self):
         return f'dcim.add_{self.queryset.model._meta.model_name}'
         return f'dcim.add_{self.queryset.model._meta.model_name}'
@@ -805,16 +806,16 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
 
 
                         for obj in data['pk']:
                         for obj in data['pk']:
 
 
-                            names = data['name_pattern']
-                            labels = data['label_pattern'] if 'label_pattern' in data else None
-                            for i, name in enumerate(names):
-                                label = labels[i] if labels else None
-
+                            pattern_count = len(data[f'{self.patterned_fields[0]}_pattern'])
+                            for i in range(pattern_count):
                                 component_data = {
                                 component_data = {
-                                    self.parent_field: obj.pk,
-                                    'name': name,
-                                    'label': label
+                                    self.parent_field: obj.pk
                                 }
                                 }
+
+                                for field_name in self.patterned_fields:
+                                    if data.get(f'{field_name}_pattern'):
+                                        component_data[field_name] = data[f'{field_name}_pattern'][i]
+
                                 component_data.update(data)
                                 component_data.update(data)
                                 component_form = self.model_form(component_data)
                                 component_form = self.model_form(component_data)
                                 if component_form.is_valid():
                                 if component_form.is_valid():

+ 3 - 3
netbox/netbox/views/generic/object_views.py

@@ -389,10 +389,10 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
                 )
                 )
                 logger.info(f"{msg} {obj} (PK: {obj.pk})")
                 logger.info(f"{msg} {obj} (PK: {obj.pk})")
                 if hasattr(obj, 'get_absolute_url'):
                 if hasattr(obj, 'get_absolute_url'):
-                    msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), escape(obj))
+                    msg = mark_safe(f'{msg} <a href="{obj.get_absolute_url()}">{escape(obj)}</a>')
                 else:
                 else:
-                    msg = '{} {}'.format(msg, escape(obj))
-                messages.success(request, mark_safe(msg))
+                    msg = f'{msg} {obj}'
+                messages.success(request, msg)
 
 
                 if '_addanother' in request.POST:
                 if '_addanother' in request.POST:
                     redirect_url = request.path
                     redirect_url = request.path

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
netbox/project-static/dist/netbox-dark.css


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
netbox/project-static/dist/netbox-light.css


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
netbox/project-static/dist/netbox-print.css


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
netbox/project-static/dist/netbox.js


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 44 - 0
netbox/project-static/src/search.ts

@@ -27,6 +27,23 @@ function handleSearchDropdownClick(event: Event, button: HTMLButtonElement): voi
   }
   }
 }
 }
 
 
+/**
+ * Show/hide quicksearch clear button.
+ *
+ * @param event "keyup" or "search" event for the quicksearch input
+ */
+function quickSearchEventHandler(event: Event): void {
+  const quicksearch = event.currentTarget as HTMLInputElement;
+  const inputgroup = quicksearch.parentElement as HTMLDivElement;
+  if (isTruthy(inputgroup)) {
+    if (quicksearch.value === "") {
+      inputgroup.classList.add("hide-last-child");
+    } else {
+      inputgroup.classList.remove("hide-last-child");
+    }
+  }
+}
+
 /**
 /**
  * Initialize Search Bar Elements.
  * Initialize Search Bar Elements.
  */
  */
@@ -40,8 +57,35 @@ function initSearchBar(): void {
   }
   }
 }
 }
 
 
+/**
+ * Initialize Quicksearch Event listener/handlers.
+ */
+function initQuickSearch(): void {
+  const quicksearch = document.getElementById("quicksearch") as HTMLInputElement;
+  const clearbtn = document.getElementById("quicksearch_clear") as HTMLButtonElement;
+  if (isTruthy(quicksearch)) {
+    quicksearch.addEventListener("keyup", quickSearchEventHandler, {
+      passive: true
+    })
+    quicksearch.addEventListener("search", quickSearchEventHandler, {
+      passive: true
+    })
+    if (isTruthy(clearbtn)) {
+      clearbtn.addEventListener("click", async () => {
+        const search = new Event('search');
+        quicksearch.value = '';
+        await new Promise(f => setTimeout(f, 100));
+        quicksearch.dispatchEvent(search);
+      }, {
+        passive: true
+      })
+    }
+  }
+}
+
 export function initSearch(): void {
 export function initSearch(): void {
   for (const func of [initSearchBar]) {
   for (const func of [initSearchBar]) {
     func();
     func();
   }
   }
+  initQuickSearch();
 }
 }

+ 22 - 4
netbox/project-static/styles/netbox.scss

@@ -416,6 +416,27 @@ nav.search {
   }
   }
 }
 }
 
 
+// Styles for the quicksearch and its clear button; 
+// Overrides input-group styles and adds transition effects
+.quicksearch {
+  input[type="search"] {
+    border-radius: $border-radius  !important;
+  }
+
+  button {
+    margin-left: -32px !important;
+    z-index: 100 !important;
+    outline: none !important;
+    border-radius: $border-radius  !important;
+    transition: visibility 0s, opacity 0.2s linear;
+  }
+
+  button :hover {
+    opacity: 50%;
+    transition: visibility 0s, opacity 0.1s linear;
+  }
+}
+
 main.layout {
 main.layout {
   display: flex;
   display: flex;
   flex-wrap: nowrap;
   flex-wrap: nowrap;
@@ -714,11 +735,8 @@ textarea.form-control[rows='10'] {
   height: 18rem;
   height: 18rem;
 }
 }
 
 
-textarea#id_local_context_data,
 textarea.markdown,
 textarea.markdown,
-textarea#id_public_key,
-textarea.form-control[name='csv'],
-textarea.form-control[name='data'] {
+textarea.form-control[name='csv'] {
   font-family: $font-family-monospace;
   font-family: $font-family-monospace;
 }
 }
 
 

+ 8 - 0
netbox/project-static/styles/overrides.scss

@@ -34,3 +34,11 @@ a[type='button'] {
 .badge {
 .badge {
   font-size: $font-size-xs;
   font-size: $font-size-xs;
 }
 }
+
+/* clears the 'X' in search inputs from webkit browsers */
+input[type='search']::-webkit-search-decoration,
+input[type='search']::-webkit-search-cancel-button,
+input[type='search']::-webkit-search-results-button,
+input[type='search']::-webkit-search-results-decoration {
+  -webkit-appearance: none !important;
+}

+ 4 - 0
netbox/project-static/styles/theme-dark.scss

@@ -92,6 +92,10 @@ $input-focus-color: $input-color;
 $input-placeholder-color: $gray-700;
 $input-placeholder-color: $gray-700;
 $input-plaintext-color: $body-color;
 $input-plaintext-color: $body-color;
 
 
+input {
+  color-scheme: dark;
+}
+
 $form-check-input-active-filter: brightness(90%);
 $form-check-input-active-filter: brightness(90%);
 $form-check-input-bg: $input-bg;
 $form-check-input-bg: $input-bg;
 $form-check-input-border: 1px solid rgba(255, 255, 255, 0.25);
 $form-check-input-border: 1px solid rgba(255, 255, 255, 0.25);

+ 1 - 2
netbox/project-static/styles/theme-light.scss

@@ -22,7 +22,6 @@ $theme-colors: (
   'danger': $danger,
   'danger': $danger,
   'light': $light,
   'light': $light,
   'dark': $dark,
   'dark': $dark,
-
   // General-purpose palette
   // General-purpose palette
   'blue': $blue-500,
   'blue': $blue-500,
   'indigo': $indigo-500,
   'indigo': $indigo-500,
@@ -36,7 +35,7 @@ $theme-colors: (
   'cyan': $cyan-500,
   'cyan': $cyan-500,
   'gray': $gray-500,
   'gray': $gray-500,
   'black': $black,
   'black': $black,
-  'white': $white,
+  'white': $white
 );
 );
 
 
 $light: $gray-200;
 $light: $gray-200;

+ 6 - 0
netbox/project-static/styles/utilities.scss

@@ -42,3 +42,9 @@ table td {
     visibility: visible !important;
     visibility: visible !important;
   }
   }
 }
 }
+
+// Hides the last child of an element
+.hide-last-child :last-child {
+  visibility: hidden;
+  opacity: 0;
+}

+ 3 - 3
netbox/templates/dcim/consoleport.html

@@ -111,13 +111,13 @@
                                     </button>
                                     </button>
                                     <ul class="dropdown-menu dropdown-menu-end">
                                     <ul class="dropdown-menu dropdown-menu-end">
                                         <li>
                                         <li>
-                                            <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Server Port</a>
+                                            <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Server Port</a>
                                         </li>
                                         </li>
                                         <li>
                                         <li>
-                                            <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
+                                            <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
                                         </li>
                                         </li>
                                         <li>
                                         <li>
-                                            <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
+                                            <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
                                         </li>
                                         </li>
                                     </ul>
                                     </ul>
                                 </div>
                                 </div>

+ 3 - 3
netbox/templates/dcim/consoleserverport.html

@@ -113,13 +113,13 @@
                                 </button>
                                 </button>
                                 <ul class="dropdown-menu dropdown-menu-end">
                                 <ul class="dropdown-menu dropdown-menu-end">
                                     <li>
                                     <li>
-                                        <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Port</a>
+                                        <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Port</a>
                                     </li>
                                     </li>
                                     <li>
                                     <li>
-                                        <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
+                                        <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
                                     </li>
                                     </li>
                                     <li>
                                     <li>
-                                        <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
+                                        <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
                                     </li>
                                     </li>
                                 </ul>
                                 </ul>
                             </div>
                             </div>

+ 67 - 66
netbox/templates/dcim/device/interfaces.html

@@ -4,81 +4,82 @@
 {% load static %}
 {% load static %}
 
 
 {% block content %}
 {% block content %}
-  <div class="row mb-3 justify-content-between">
-    <div class="col col-12 col-lg-4 my-3 my-lg-0 d-flex noprint table-controls">
-      <div class="input-group input-group-sm">
-        <input
-            type="text"
-            name="q"
-            class="form-control"
-            placeholder="Quick search"
-            hx-get="{{ request.full_path }}"
-            hx-target="#object_list"
-            hx-trigger="keyup changed delay:500ms"
-        />
-      </div>
+<div class="row mb-3 justify-content-between">
+  <div class="col col-12 col-lg-4 my-3 my-lg-0 d-flex noprint table-controls">
+    <div class="input-group input-group-sm quicksearch hide-last-child">
+      <input type="search" results=5 name="q" id="quicksearch" class="form-control" placeholder="Quick search"
+        hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
+      <button class="btn bg-transparent" type="button" id="quicksearch_clear"><i
+          class="mdi mdi-close-circle"></i></button>
     </div>
     </div>
-    <div class="col col-md-3 mb-0 d-flex noprint table-controls">
-      <div class="input-group input-group-sm justify-content-end">
-        {% if request.user.is_authenticated %}
-          <button
-              type="button"
-              class="btn btn-sm btn-outline-dark"
-              data-bs-toggle="modal"
-              data-bs-target="#DeviceInterfaceTable_config"
-              title="Configure Table">
-              <i class="mdi mdi-cog"></i> Configure Table
-          </button>
-        {% endif %}
-        <button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
-          <i class="mdi mdi-eye"></i>
-        </button>
-        <ul class="dropdown-menu">
-          <button type="button" class="dropdown-item toggle-enabled" data-state="show">Hide Enabled</button>
-          <button type="button" class="dropdown-item toggle-disabled" data-state="show">Hide Disabled</button>
-        </ul>
-      </div>
+  </div>
+  <div class="col col-md-3 mb-0 d-flex noprint table-controls">
+    <div class="input-group input-group-sm justify-content-end">
+      {% if request.user.is_authenticated %}
+      <button type="button" class="btn btn-sm btn-outline-dark" data-bs-toggle="modal"
+        data-bs-target="#DeviceInterfaceTable_config" title="Configure Table">
+        <i class="mdi mdi-cog"></i> Configure Table
+      </button>
+      {% endif %}
+      <button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown"
+        aria-expanded="false">
+        <i class="mdi mdi-eye"></i>
+      </button>
+      <ul class="dropdown-menu">
+        <button type="button" class="dropdown-item toggle-enabled" data-state="show">Hide Enabled</button>
+        <button type="button" class="dropdown-item toggle-disabled" data-state="show">Hide Disabled</button>
+      </ul>
     </div>
     </div>
   </div>
   </div>
+</div>
 
 
-  <form method="post">
-    {% csrf_token %}
+<form method="post">
+  {% csrf_token %}
 
 
 
 
-    <div class="card">
-      <div class="card-body" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
+  <div class="card">
+    <div class="card-body" id="object_list">
+      {% include 'htmx/table.html' %}
     </div>
     </div>
+  </div>
 
 
-    <div class="noprint bulk-buttons">
-        <div class="bulk-button-group">
-          {% if 'bulk_edit' in actions %}
-            <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-                <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-            <button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-warning btn-sm">
-                <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-                <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-        {% endif %}
-          {% if 'bulk_delete' in actions %}
-            <button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-danger btn-sm">
-                <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-            </button>
-        {% endif %}
-        </div>
-        {% if perms.dcim.add_interface %}
-            <div class="bulk-button-group">
-                <a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-primary btn-sm">
-                    <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Interfaces
-                </a>
-            </div>
-        {% endif %}
+  <div class="noprint bulk-buttons">
+    <div class="bulk-button-group">
+      {% if perms.dcim.change_interface %}
+        <button type="submit" name="_rename"
+          formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
+          class="btn btn-outline-warning btn-sm">
+          <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
+        </button>
+        <button type="submit" name="_edit"
+          formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
+          class="btn btn-warning btn-sm">
+          <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+        </button>
+        <button type="submit" name="_disconnect"
+          formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
+          class="btn btn-outline-danger btn-sm">
+          <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
+        </button>
+      {% endif %}
+      {% if perms.dcim.delete_interface %}
+        <button type="submit" name="_delete"
+          formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
+          class="btn btn-danger btn-sm">
+          <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+        </button>
+      {% endif %}
     </div>
     </div>
-  </form>
+    {% if perms.dcim.add_interface %}
+      <div class="bulk-button-group">
+        <a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
+          class="btn btn-primary btn-sm">
+          <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Interfaces
+        </a>
+      </div>
+    {% endif %}
+  </div>
+</form>
 {% endblock %}
 {% endblock %}
 
 
 {% block modals %}
 {% block modals %}

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

@@ -109,22 +109,22 @@
                                 </button>
                                 </button>
                                 <ul class="dropdown-menu dropdown-menu-end">
                                 <ul class="dropdown-menu dropdown-menu-end">
                                     <li>
                                     <li>
-                                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">Interface</a>
+                                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Interface</a>
                                     </li>
                                     </li>
                                     <li>
                                     <li>
-                                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&return_url={{ object.get_absolute_url }}">Console Server Port</a>
+                                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Console Server Port</a>
                                     </li>
                                     </li>
                                     <li>
                                     <li>
-                                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&return_url={{ object.get_absolute_url }}">Console Port</a>
+                                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Console Port</a>
                                     </li>
                                     </li>
                                     <li>
                                     <li>
-                                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">Front Port</a>
+                                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Front Port</a>
                                     </li>
                                     </li>
                                     <li>
                                     <li>
-                                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">Rear Port</a>
+                                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Rear Port</a>
                                     </li>
                                     </li>
                                     <li>
                                     <li>
-                                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">Circuit Termination</a>
+                                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ object.device.site.pk }}&return_url={{ object.get_absolute_url }}">Circuit Termination</a>
                                     </li>
                                     </li>
                                 </ul>
                                 </ul>
                             </div>
                             </div>

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

@@ -263,16 +263,16 @@
                     </button>
                     </button>
                     <ul class="dropdown-menu dropdown-menu-end">
                     <ul class="dropdown-menu dropdown-menu-end">
                       <li>
                       <li>
-                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">Interface</a>
+                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Interface</a>
                       </li>
                       </li>
                       <li>
                       <li>
-                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">Front Port</a>
+                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Front Port</a>
                       </li>
                       </li>
                       <li>
                       <li>
-                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">Rear Port</a>
+                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Rear Port</a>
                       </li>
                       </li>
                       <li>
                       <li>
-                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">Circuit Termination</a>
+                        <a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ object.device.site.pk }}&return_url={{ object.get_absolute_url }}">Circuit Termination</a>
                       </li>
                       </li>
                     </ul>
                     </ul>
                   </div>
                   </div>

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

@@ -158,7 +158,7 @@
             {% if not object.mark_connected and not object.cable %}
             {% if not object.mark_connected and not object.cable %}
             <div class="card-footer">
             <div class="card-footer">
             {% if perms.dcim.add_cable %}
             {% if perms.dcim.add_cable %}
-                <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerfeed&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm float-end">
+                <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerfeed&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm float-end">
                     <i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
                     <i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
                 </a>
                 </a>
                     {% endif %}
                     {% endif %}

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

@@ -111,7 +111,7 @@
                     <div class="text-muted">
                     <div class="text-muted">
                         Not Connected
                         Not Connected
                         {% if perms.dcim.add_cable %}
                         {% if perms.dcim.add_cable %}
-                            <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&return_url={{ object.get_absolute_url }}" title="Connect" class="btn btn-primary btn-sm float-end">
+                            <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" title="Connect" class="btn btn-primary btn-sm float-end">
                                 <i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
                                 <i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
                             </a>
                             </a>
                         {% endif %}
                         {% endif %}

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

@@ -117,10 +117,10 @@
                                 </button>
                                 </button>
                                 <ul class="dropdown-menu dropdown-menu-end">
                                 <ul class="dropdown-menu dropdown-menu-end">
                                     <li>
                                     <li>
-                                        <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.poweroutlet&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Outlet</a>
+                                        <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.poweroutlet&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Outlet</a>
                                     </li>
                                     </li>
                                     <li>
                                     <li>
-                                        <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.powerfeed&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Feed</a>
+                                        <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.powerfeed&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Feed</a>
                                     </li>
                                     </li>
                                 </ul>
                                 </ul>
                             </span>
                             </span>

+ 4 - 4
netbox/templates/dcim/rearport.html

@@ -105,16 +105,16 @@
                                 </button>
                                 </button>
                                 <ul class="dropdown-menu dropdown-menu-end">
                                 <ul class="dropdown-menu dropdown-menu-end">
                                     <li>
                                     <li>
-                                        <a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">Interface</a>
+                                        <a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Interface</a>
                                     </li>
                                     </li>
                                     <li>
                                     <li>
-                                        <a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">Front Port</a>
+                                        <a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Front Port</a>
                                     </li>
                                     </li>
                                     <li>
                                     <li>
-                                        <a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">Rear Port</a>
+                                        <a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Rear Port</a>
                                     </li>
                                     </li>
                                     <li>
                                     <li>
-                                        <a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">Circuit Termination</a>
+                                        <a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ object.device.site.pk }}&return_url={{ object.get_absolute_url }}">Circuit Termination</a>
                                     </li>
                                     </li>
                                 </ul>
                                 </ul>
                             </span>
                             </span>

+ 16 - 0
netbox/templates/inc/panels/contacts.html

@@ -10,6 +10,8 @@
             <th>Name</th>
             <th>Name</th>
             <th>Role</th>
             <th>Role</th>
             <th>Priority</th>
             <th>Priority</th>
+            <th>Phone</th>
+            <th>Email</th>
             <th></th>
             <th></th>
           </tr>
           </tr>
           {% for contact in contacts %}
           {% for contact in contacts %}
@@ -17,6 +19,20 @@
               <td>{{ contact.contact|linkify }}</td>
               <td>{{ contact.contact|linkify }}</td>
               <td>{{ contact.role|placeholder }}</td>
               <td>{{ contact.role|placeholder }}</td>
               <td>{{ contact.get_priority_display|placeholder }}</td>
               <td>{{ contact.get_priority_display|placeholder }}</td>
+              <td>
+                {% if contact.contact.phone %}
+                  <a href="tel:{{ contact.contact.phone }}">{{ contact.contact.phone }}</a>
+                {% else %}
+                  {{ ''|placeholder }}
+                {% endif %}
+              </td>
+              <td>
+                {% if contact.contact.email %}
+                  <a href="mailto:{{ contact.contact.email }}">{{ contact.contact.email }}</a>
+                {% else %}
+                  {{ ''|placeholder }}
+                {% endif %}
+              </td>
               <td class="text-end noprint">
               <td class="text-end noprint">
                 {% if perms.tenancy.change_contactassignment %}
                 {% if perms.tenancy.change_contactassignment %}
                   <a href="{% url 'tenancy:contactassignment_edit' pk=contact.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning btn-sm lh-1" title="Edit">
                   <a href="{% url 'tenancy:contactassignment_edit' pk=contact.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning btn-sm lh-1" title="Edit">

+ 12 - 22
netbox/templates/inc/table_controls_htmx.html

@@ -2,31 +2,21 @@
 
 
 <div class="row mb-3 justify-content-between">
 <div class="row mb-3 justify-content-between">
   <div class="table-controls noprint col col-12 col-md-8 col-lg-4">
   <div class="table-controls noprint col col-12 col-md-8 col-lg-4">
-    <div class="input-group input-group-sm">
-      <input
-        type="text"
-        name="q"
-        class="form-control"
-        placeholder="Quick search"
-        hx-get="{{ request.full_path }}"
-        hx-target="#object_list"
-        hx-trigger="keyup changed delay:500ms"
-      />
+    <div class="input-group input-group-sm quicksearch hide-last-child">
+      <input type="search" results=5 name="q" id="quicksearch" class="form-control" placeholder="Quick search"
+        hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
+      <button class="btn bg-transparent" type="button" id="quicksearch_clear"><i
+          class="mdi mdi-close-circle"></i></button>
     </div>
     </div>
   </div>
   </div>
   <div class="table-controls noprint col col-md-3 mb-0">
   <div class="table-controls noprint col col-md-3 mb-0">
     {% if request.user.is_authenticated and table_modal %}
     {% if request.user.is_authenticated and table_modal %}
-      <div class="table-configure input-group input-group-sm">
-        <button
-          type="button"
-          data-bs-toggle="modal"
-          title="Configure Table"
-          data-bs-target="#{{ table_modal }}"
-          class="btn btn-sm btn-outline-dark"
-        >
-          <i class="mdi mdi-cog"></i> Configure Table
-        </button>
-      </div>
+    <div class="table-configure input-group input-group-sm">
+      <button type="button" data-bs-toggle="modal" title="Configure Table" data-bs-target="#{{ table_modal }}"
+        class="btn btn-sm btn-outline-dark">
+        <i class="mdi mdi-cog"></i> Configure Table
+      </button>
+    </div>
     {% endif %}
     {% endif %}
   </div>
   </div>
-</div>
+</div>

+ 4 - 4
netbox/tenancy/models/contacts.py

@@ -112,9 +112,9 @@ class Contact(NetBoxModel):
         blank=True
         blank=True
     )
     )
 
 
-    clone_fields = [
-        'group',
-    ]
+    clone_fields = (
+        'group', 'name', 'title', 'phone', 'email', 'address', 'link',
+    )
 
 
     class Meta:
     class Meta:
         ordering = ['name']
         ordering = ['name']
@@ -155,7 +155,7 @@ class ContactAssignment(WebhooksMixin, ChangeLoggedModel):
         blank=True
         blank=True
     )
     )
 
 
-    clone_fields = ('content_type', 'object_id')
+    clone_fields = ('content_type', 'object_id', 'role', 'priority')
 
 
     class Meta:
     class Meta:
         ordering = ('priority', 'contact')
         ordering = ('priority', 'contact')

+ 2 - 2
netbox/tenancy/models/tenants.py

@@ -76,9 +76,9 @@ class Tenant(NetBoxModel):
         to='tenancy.ContactAssignment'
         to='tenancy.ContactAssignment'
     )
     )
 
 
-    clone_fields = [
+    clone_fields = (
         'group', 'description',
         'group', 'description',
-    ]
+    )
 
 
     class Meta:
     class Meta:
         ordering = ['name']
         ordering = ['name']

+ 7 - 5
netbox/users/api/views.py

@@ -58,6 +58,8 @@ class TokenViewSet(NetBoxModelViewSet):
         # Workaround for schema generation (drf_yasg)
         # Workaround for schema generation (drf_yasg)
         if getattr(self, 'swagger_fake_view', False):
         if getattr(self, 'swagger_fake_view', False):
             return queryset.none()
             return queryset.none()
+        if not self.request.user.is_authenticated:
+            return queryset.none()
         if self.request.user.is_superuser:
         if self.request.user.is_superuser:
             return queryset
             return queryset
         return queryset.filter(user=self.request.user)
         return queryset.filter(user=self.request.user)
@@ -74,11 +76,11 @@ class TokenProvisionView(APIView):
         serializer.is_valid()
         serializer.is_valid()
 
 
         # Authenticate the user account based on the provided credentials
         # Authenticate the user account based on the provided credentials
-        user = authenticate(
-            request=request,
-            username=serializer.data['username'],
-            password=serializer.data['password']
-        )
+        username = serializer.data.get('username')
+        password = serializer.data.get('password')
+        if not username or not password:
+            raise AuthenticationFailed("Username and password must be provided to provision a token.")
+        user = authenticate(request=request, username=username, password=password)
         if user is None:
         if user is None:
             raise AuthenticationFailed("Invalid username/password")
             raise AuthenticationFailed("Invalid username/password")
 
 

+ 2 - 1
netbox/users/views.py

@@ -10,6 +10,7 @@ from django.http import HttpResponseRedirect
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.decorators import method_decorator
 from django.utils.decorators import method_decorator
+from django.utils.http import url_has_allowed_host_and_scheme
 from django.views.decorators.debug import sensitive_post_parameters
 from django.views.decorators.debug import sensitive_post_parameters
 from django.views.generic import View
 from django.views.generic import View
 from social_core.backends.utils import load_backends
 from social_core.backends.utils import load_backends
@@ -92,7 +93,7 @@ class LoginView(View):
         data = request.POST if request.method == "POST" else request.GET
         data = request.POST if request.method == "POST" else request.GET
         redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
         redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
 
 
-        if redirect_url and redirect_url.startswith('/'):
+        if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None):
             logger.debug(f"Redirecting user to {redirect_url}")
             logger.debug(f"Redirecting user to {redirect_url}")
         else:
         else:
             if redirect_url:
             if redirect_url:

+ 1 - 0
netbox/utilities/forms/fields/fields.py

@@ -99,6 +99,7 @@ class JSONField(_JSONField):
         if not self.help_text:
         if not self.help_text:
             self.help_text = 'Enter context data in <a href="https://json.org/">JSON</a> format.'
             self.help_text = 'Enter context data in <a href="https://json.org/">JSON</a> format.'
             self.widget.attrs['placeholder'] = ''
             self.widget.attrs['placeholder'] = ''
+            self.widget.attrs['class'] = 'font-monospace'
 
 
     def prepare_value(self, value):
     def prepare_value(self, value):
         if isinstance(value, InvalidJSONInput):
         if isinstance(value, InvalidJSONInput):

+ 1 - 1
netbox/utilities/forms/forms.py

@@ -136,7 +136,7 @@ class ImportForm(BootstrapMixin, forms.Form):
     Generic form for creating an object from JSON/YAML data
     Generic form for creating an object from JSON/YAML data
     """
     """
     data = forms.CharField(
     data = forms.CharField(
-        widget=forms.Textarea,
+        widget=forms.Textarea(attrs={'class': 'font-monospace'}),
         help_text="Enter object data in JSON or YAML format. Note: Only a single object/document is supported."
         help_text="Enter object data in JSON or YAML format. Note: Only a single object/document is supported."
     )
     )
     format = forms.ChoiceField(
     format = forms.ChoiceField(

+ 2 - 2
netbox/utilities/templatetags/builtins/filters.py

@@ -86,8 +86,8 @@ def placeholder(value):
     """
     """
     if value not in ('', None):
     if value not in ('', None):
         return value
         return value
-    placeholder = '<span class="text-muted">&mdash;</span>'
-    return mark_safe(placeholder)
+
+    return mark_safe('<span class="text-muted">&mdash;</span>')
 
 
 
 
 @register.filter()
 @register.filter()

+ 1 - 3
netbox/utilities/templatetags/helpers.py

@@ -109,9 +109,7 @@ def annotated_date(date_value):
         long_ts = date(date_value, 'DATETIME_FORMAT')
         long_ts = date(date_value, 'DATETIME_FORMAT')
         short_ts = date(date_value, 'SHORT_DATETIME_FORMAT')
         short_ts = date(date_value, 'SHORT_DATETIME_FORMAT')
 
 
-    span = f'<span title="{long_ts}">{short_ts}</span>'
-
-    return mark_safe(span)
+    return mark_safe(f'<span title="{long_ts}">{short_ts}</span>')
 
 
 
 
 @register.simple_tag
 @register.simple_tag

+ 1 - 1
netbox/virtualization/forms/filtersets.py

@@ -93,7 +93,7 @@ class VirtualMachineFilterForm(
         (None, ('q', 'tag')),
         (None, ('q', 'tag')),
         ('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')),
         ('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')),
         ('Location', ('region_id', 'site_group_id', 'site_id')),
         ('Location', ('region_id', 'site_group_id', 'site_id')),
-        ('Attriubtes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
+        ('Attributes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
         ('Tenant', ('tenant_group_id', 'tenant_id')),
         ('Tenant', ('tenant_group_id', 'tenant_id')),
         ('Contacts', ('contact', 'contact_role', 'contact_group')),
         ('Contacts', ('contact', 'contact_role', 'contact_group')),
     )
     )

+ 5 - 5
netbox/virtualization/models.py

@@ -153,9 +153,9 @@ class Cluster(NetBoxModel):
         to='tenancy.ContactAssignment'
         to='tenancy.ContactAssignment'
     )
     )
 
 
-    clone_fields = [
-        'type', 'group', 'tenant', 'site',
-    ]
+    clone_fields = (
+        'type', 'group', 'status', 'tenant', 'site',
+    )
 
 
     class Meta:
     class Meta:
         ordering = ['name']
         ordering = ['name']
@@ -299,9 +299,9 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
 
 
     objects = ConfigContextModelQuerySet.as_manager()
     objects = ConfigContextModelQuerySet.as_manager()
 
 
-    clone_fields = [
+    clone_fields = (
         'site', 'cluster', 'device', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk',
         'site', 'cluster', 'device', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk',
-    ]
+    )
 
 
     class Meta:
     class Meta:
         ordering = ('_name', 'pk')  # Name may be non-unique
         ordering = ('_name', 'pk')  # Name may be non-unique

+ 2 - 0
netbox/wireless/models.py

@@ -113,6 +113,8 @@ class WirelessLAN(WirelessAuthenticationBase, NetBoxModel):
         blank=True
         blank=True
     )
     )
 
 
+    clone_fields = ('ssid', 'group', 'tenant', 'description')
+
     class Meta:
     class Meta:
         ordering = ('ssid', 'pk')
         ordering = ('ssid', 'pk')
         verbose_name = 'Wireless LAN'
         verbose_name = 'Wireless LAN'

+ 4 - 1
requirements.txt

@@ -25,7 +25,7 @@ netaddr==0.8.0
 Pillow==9.2.0
 Pillow==9.2.0
 psycopg2-binary==2.9.3
 psycopg2-binary==2.9.3
 PyYAML==6.0
 PyYAML==6.0
-sentry-sdk==1.9.0
+sentry-sdk==1.9.2
 social-auth-app-django==5.0.0
 social-auth-app-django==5.0.0
 social-auth-core==4.3.0
 social-auth-core==4.3.0
 svgwrite==1.4.3
 svgwrite==1.4.3
@@ -34,3 +34,6 @@ tzdata==2022.1
 
 
 # Workaround for #7401
 # Workaround for #7401
 jsonschema==3.2.0
 jsonschema==3.2.0
+
+# Workaround for #9986
+pytz==2022.1

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.