Explorar o código

Merge branch 'feature' into docs-refresh

jeremystretch %!s(int64=3) %!d(string=hai) anos
pai
achega
150c3d3a97
Modificáronse 61 ficheiros con 378 adicións e 255 borrados
  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
 # https://github.com/django/django
-Django
+Django<4.1
 
 # Django middleware which permits cross-domain API requests
 # 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 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
 
-## 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
 
@@ -11,13 +25,20 @@
 * [#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
 * [#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
 
+* [#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
 * [#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
 * [#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
 * [#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
 

+ 2 - 0
mkdocs.yml

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

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

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

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

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

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

@@ -84,6 +84,7 @@ def get_cable_form(a_type, b_type):
                         disabled_indicator='_occupied',
                         query_params={
                             '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:
         model = FrontPortTemplate
         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:
         model = RearPortTemplate
         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.
         """
-        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 {
             'name': self.name,
             'type': self.type,
+            'color': self.color,
             'rear_port': self.rear_port.name,
             'rear_port_position': self.rear_port_position,
             'label': self.label,
@@ -527,6 +528,7 @@ class RearPortTemplate(ModularComponentTemplateModel):
         return {
             'name': self.name,
             'type': self.type,
+            'color': self.color,
             'positions': self.positions,
             'label': self.label,
             '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'
     )
 
-    clone_fields = ['device', 'type', 'speed']
+    clone_fields = ('device', 'module', 'type', 'speed')
 
     class Meta:
         ordering = ('device', '_name')
@@ -290,7 +290,7 @@ class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
         help_text='Port speed in bits per second'
     )
 
-    clone_fields = ['device', 'type', 'speed']
+    clone_fields = ('device', 'module', 'type', 'speed')
 
     class Meta:
         ordering = ('device', '_name')
@@ -327,7 +327,7 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
         help_text="Allocated power draw (watts)"
     )
 
-    clone_fields = ['device', 'maximum_draw', 'allocated_draw']
+    clone_fields = ('device', 'module', 'maximum_draw', 'allocated_draw')
 
     class Meta:
         ordering = ('device', '_name')
@@ -441,7 +441,7 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint):
         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:
         ordering = ('device', '_name')
@@ -672,7 +672,10 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
         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:
         ordering = ('device', CollateAsChar('_name'))
@@ -890,7 +893,7 @@ class FrontPort(ModularComponentModel, CabledObjectModel):
         ]
     )
 
-    clone_fields = ['device', 'type']
+    clone_fields = ('device', 'type', 'color')
 
     class Meta:
         ordering = ('device', '_name')
@@ -937,7 +940,7 @@ class RearPort(ModularComponentModel, CabledObjectModel):
             MaxValueValidator(REARPORT_POSITIONS_MAX)
         ]
     )
-    clone_fields = ['device', 'type', 'positions']
+    clone_fields = ('device', 'type', 'color', 'positions')
 
     class Meta:
         ordering = ('device', '_name')
@@ -972,7 +975,7 @@ class ModuleBay(ComponentModel):
         help_text='Identifier to reference when renaming installed components'
     )
 
-    clone_fields = ['device']
+    clone_fields = ('device',)
 
     class Meta:
         ordering = ('device', '_name')
@@ -994,7 +997,7 @@ class DeviceBay(ComponentModel):
         null=True
     )
 
-    clone_fields = ['device']
+    clone_fields = ('device',)
 
     class Meta:
         ordering = ('device', '_name')
@@ -1131,7 +1134,7 @@ class InventoryItem(MPTTModel, ComponentModel):
 
     objects = TreeManager()
 
-    clone_fields = ['device', 'parent', 'role', 'manufacturer', 'part_id']
+    clone_fields = ('device', 'parent', 'role', 'manufacturer', 'part_id',)
 
     class Meta:
         ordering = ('device__id', 'parent__id', '_name')

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

@@ -135,9 +135,9 @@ class DeviceType(NetBoxModel):
         blank=True
     )
 
-    clone_fields = [
+    clone_fields = (
         'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
-    ]
+    )
 
     class Meta:
         ordering = ['manufacturer', 'model']
@@ -630,9 +630,10 @@ class Device(NetBoxModel, ConfigContextModel):
 
     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:
         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
     )
 
-    clone_fields = [
+    clone_fields = (
         'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
-        'max_utilization', 'available_power',
-    ]
+        'max_utilization',
+    )
 
     class Meta:
         ordering = ['power_panel', 'name']

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

@@ -183,10 +183,10 @@ class Rack(NetBoxModel):
         to='extras.ImageAttachment'
     )
 
-    clone_fields = [
+    clone_fields = (
         'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
         'outer_depth', 'outer_unit',
-    ]
+    )
 
     class Meta:
         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'
     )
 
-    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:
         ordering = ('_name',)
@@ -372,7 +372,7 @@ class Location(NestedGroupModel):
         to='extras.ImageAttachment'
     )
 
-    clone_fields = ['site', 'parent', 'status', 'tenant', 'description']
+    clone_fields = ('site', 'parent', 'status', 'tenant', 'description')
 
     class Meta:
         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>
         </button>
         <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>
     </span>
 {% else %}
@@ -153,9 +153,9 @@ CONSOLESERVERPORT_BUTTONS = """
             <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
         </button>
         <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>
     </span>
 {% else %}
@@ -185,8 +185,8 @@ POWERPORT_BUTTONS = """
             <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
         </button>
         <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>
     </span>
 {% 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-lan-connect" aria-hidden="true"></i></a>
     {% 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>
         </a>
     {% else %}
@@ -262,10 +262,10 @@ INTERFACE_BUTTONS = """
             <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
         </button>
         <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>
     </span>
     {% else %}
@@ -301,12 +301,12 @@ FRONTPORT_BUTTONS = """
                 <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
             </button>
             <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>
         </span>
     {% else %}
@@ -338,12 +338,12 @@ REARPORT_BUTTONS = """
                 <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
             </button>
             <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>
         </span>
     {% else %}

+ 3 - 3
netbox/dcim/views.py

@@ -2721,6 +2721,7 @@ class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView):
     filterset = filtersets.DeviceFilterSet
     table = tables.DeviceTable
     default_return_url = 'dcim:device_list'
+    patterned_fields = ('name', 'label', 'position')
 
 
 class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
@@ -3066,7 +3067,7 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix
             if membership_form.is_valid():
 
                 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))
 
                 if '_addanother' in request.POST:
@@ -3111,8 +3112,7 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
         # Protect master device from being removed
         virtual_chassis = VirtualChassis.objects.filter(master=device).first()
         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())
 
         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.forms import (
     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.validators import validate_regex
@@ -355,7 +355,7 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
 
         # JSON
         elif self.type == CustomFieldTypeChoices.TYPE_JSON:
-            field = forms.JSONField(required=required, initial=initial)
+            field = JSONField(required=required, initial=initial)
 
         # 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'
     )
 
-    clone_fields = ('protocol', 'auth_type', 'auth_key')
+    clone_fields = ('protocol', 'auth_type', 'auth_key', 'description')
 
     class Meta:
         ordering = ['protocol', 'group_id', 'pk']

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

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

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

@@ -55,9 +55,9 @@ class VRF(NetBoxModel):
         blank=True
     )
 
-    clone_fields = [
+    clone_fields = (
         'tenant', 'enforce_unique', 'description',
-    ]
+    )
 
     class Meta:
         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()
 
         # 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({
-                "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.template import Context, Template
 from django.urls import reverse
+from django.utils.html import escape
 from django.utils.formats import date_format
 from django.utils.safestring import mark_safe
 from django_tables2.columns import library
@@ -428,8 +429,8 @@ class CustomFieldColumn(tables.Column):
     @staticmethod
     def _likify_item(item):
         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):
         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:
             return mark_safe('<i class="mdi mdi-close-thick text-danger"></i>')
         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:
             return ', '.join(v for v in value)
         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)
-            ]))
+            ))
         if value is not None:
             obj = self.customfield.deserialize(value)
             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
     filterset = None
     table = None
+    patterned_fields = ('name', 'label')
 
     def get_required_permission(self):
         return f'dcim.add_{self.queryset.model._meta.model_name}'
@@ -805,16 +806,16 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
 
                         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 = {
-                                    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_form = self.model_form(component_data)
                                 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})")
                 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:
-                    msg = '{} {}'.format(msg, escape(obj))
-                messages.success(request, mark_safe(msg))
+                    msg = f'{msg} {obj}'
+                messages.success(request, msg)
 
                 if '_addanother' in request.POST:
                     redirect_url = request.path

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
netbox/project-static/dist/netbox-dark.css


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
netbox/project-static/dist/netbox-light.css


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
netbox/project-static/dist/netbox-print.css


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
netbox/project-static/dist/netbox.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 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.
  */
@@ -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 {
   for (const func of [initSearchBar]) {
     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 {
   display: flex;
   flex-wrap: nowrap;
@@ -714,11 +735,8 @@ textarea.form-control[rows='10'] {
   height: 18rem;
 }
 
-textarea#id_local_context_data,
 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;
 }
 

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

@@ -34,3 +34,11 @@ a[type='button'] {
 .badge {
   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-plaintext-color: $body-color;
 
+input {
+  color-scheme: dark;
+}
+
 $form-check-input-active-filter: brightness(90%);
 $form-check-input-bg: $input-bg;
 $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,
   'light': $light,
   'dark': $dark,
-
   // General-purpose palette
   'blue': $blue-500,
   'indigo': $indigo-500,
@@ -36,7 +35,7 @@ $theme-colors: (
   'cyan': $cyan-500,
   'gray': $gray-500,
   'black': $black,
-  'white': $white,
+  'white': $white
 );
 
 $light: $gray-200;

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

@@ -42,3 +42,9 @@ table td {
     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>
                                     <ul class="dropdown-menu dropdown-menu-end">
                                         <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>
-                                            <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>
-                                            <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>
                                     </ul>
                                 </div>

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

@@ -113,13 +113,13 @@
                                 </button>
                                 <ul class="dropdown-menu dropdown-menu-end">
                                     <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>
-                                        <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>
-                                        <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>
                                 </ul>
                             </div>

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

@@ -4,81 +4,82 @@
 {% load static %}
 
 {% 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 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>
 
-  <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 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>
-  </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 %}
 
 {% block modals %}

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

@@ -109,22 +109,22 @@
                                 </button>
                                 <ul class="dropdown-menu dropdown-menu-end">
                                     <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>
-                                        <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>
-                                        <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>
-                                        <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>
-                                        <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>
-                                        <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>
                                 </ul>
                             </div>

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

@@ -263,16 +263,16 @@
                     </button>
                     <ul class="dropdown-menu dropdown-menu-end">
                       <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>
-                        <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>
-                        <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>
-                        <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>
                     </ul>
                   </div>

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

@@ -158,7 +158,7 @@
             {% if not object.mark_connected and not object.cable %}
             <div class="card-footer">
             {% 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
                 </a>
                     {% endif %}

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

@@ -111,7 +111,7 @@
                     <div class="text-muted">
                         Not Connected
                         {% 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
                             </a>
                         {% endif %}

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

@@ -117,10 +117,10 @@
                                 </button>
                                 <ul class="dropdown-menu dropdown-menu-end">
                                     <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>
-                                        <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>
                                 </ul>
                             </span>

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

@@ -105,16 +105,16 @@
                                 </button>
                                 <ul class="dropdown-menu dropdown-menu-end">
                                     <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>
-                                        <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>
-                                        <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>
-                                        <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>
                                 </ul>
                             </span>

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

@@ -10,6 +10,8 @@
             <th>Name</th>
             <th>Role</th>
             <th>Priority</th>
+            <th>Phone</th>
+            <th>Email</th>
             <th></th>
           </tr>
           {% for contact in contacts %}
@@ -17,6 +19,20 @@
               <td>{{ contact.contact|linkify }}</td>
               <td>{{ contact.role|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">
                 {% 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">

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

@@ -2,31 +2,21 @@
 
 <div class="row mb-3 justify-content-between">
   <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 class="table-controls noprint col col-md-3 mb-0">
     {% 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 %}
   </div>
-</div>
+</div>

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

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

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

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

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

@@ -58,6 +58,8 @@ class TokenViewSet(NetBoxModelViewSet):
         # Workaround for schema generation (drf_yasg)
         if getattr(self, 'swagger_fake_view', False):
             return queryset.none()
+        if not self.request.user.is_authenticated:
+            return queryset.none()
         if self.request.user.is_superuser:
             return queryset
         return queryset.filter(user=self.request.user)
@@ -74,11 +76,11 @@ class TokenProvisionView(APIView):
         serializer.is_valid()
 
         # 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:
             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.urls import reverse
 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.generic import View
 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
         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}")
         else:
             if redirect_url:

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

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

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

@@ -86,8 +86,8 @@ def placeholder(value):
     """
     if value not in ('', None):
         return value
-    placeholder = '<span class="text-muted">&mdash;</span>'
-    return mark_safe(placeholder)
+
+    return mark_safe('<span class="text-muted">&mdash;</span>')
 
 
 @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')
         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

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

@@ -93,7 +93,7 @@ class VirtualMachineFilterForm(
         (None, ('q', 'tag')),
         ('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_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')),
         ('Contacts', ('contact', 'contact_role', 'contact_group')),
     )

+ 5 - 5
netbox/virtualization/models.py

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

+ 2 - 0
netbox/wireless/models.py

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

+ 4 - 1
requirements.txt

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

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio