diff --git a/base_requirements.txt b/base_requirements.txt index 59d4b8255..363f97b31 100644 --- a/base_requirements.txt +++ b/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 diff --git a/docs/_theme/main.html b/docs/_theme/main.html new file mode 100644 index 000000000..4dfc4e14e --- /dev/null +++ b/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 %} + + {% endif %} +{% endblock %} diff --git a/docs/models/dcim/frontport.md b/docs/models/dcim/frontport.md index 0b753c012..6f12e8cbf 100644 --- a/docs/models/dcim/frontport.md +++ b/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. diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 1b4b85c1c..ba31b1552 100644 --- a/docs/release-notes/version-3.2.md +++ b/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 --- diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 2a3935e5e..9ec4eb9b5 100644 --- a/docs/release-notes/version-3.3.md +++ b/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) +### Bug Fixes (from Beta2) -* [#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 +* [#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 diff --git a/mkdocs.yml b/mkdocs.yml index 8bcf6cebf..9eea20397 100644 --- a/mkdocs.yml +++ b/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 diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index d82878cde..c78ea81c7 100644 --- a/netbox/circuits/models/circuits.py +++ b/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'] diff --git a/netbox/circuits/models/providers.py b/netbox/circuits/models/providers.py index 4211a54a6..e136e13ea 100644 --- a/netbox/circuits/models/providers.py +++ b/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'] diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py index 7552c0c87..cc5cf362f 100644 --- a/netbox/dcim/forms/connections.py +++ b/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 } ) diff --git a/netbox/dcim/forms/object_import.py b/netbox/dcim/forms/object_import.py index a51f48c5b..023aba8f1 100644 --- a/netbox/dcim/forms/object_import.py +++ b/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', ] diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 321d808ff..2be64451f 100644 --- a/netbox/dcim/models/cables.py +++ b/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) diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 3fc1d4e61..b7079d375 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/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, diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 5e2fc348e..838336e21 100644 --- a/netbox/dcim/models/device_components.py +++ b/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') diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index f21176d8d..136fcf6cf 100644 --- a/netbox/dcim/models/devices.py +++ b/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 diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index 94767c6c4..c275691c0 100644 --- a/netbox/dcim/models/power.py +++ b/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'] diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 4dcfcde28..50c91b52e 100644 --- a/netbox/dcim/models/racks.py +++ b/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 diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index 9b7ffdcf4..67bcc6e4c 100644 --- a/netbox/dcim/models/sites.py +++ b/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'] diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 082df56df..04ef74192 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -121,9 +121,9 @@ CONSOLEPORT_BUTTONS = """
{% else %} @@ -153,9 +153,9 @@ CONSOLESERVERPORT_BUTTONS = """ {% else %} @@ -185,8 +185,8 @@ POWERPORT_BUTTONS = """ {% else %} @@ -212,7 +212,7 @@ POWEROUTLET_BUTTONS = """ {% if not record.mark_connected %} - + {% else %} @@ -262,10 +262,10 @@ INTERFACE_BUTTONS = """ {% else %} @@ -301,12 +301,12 @@ FRONTPORT_BUTTONS = """ {% else %} @@ -338,12 +338,12 @@ REARPORT_BUTTONS = """ {% else %} diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 4480bee6e..77c6b2218 100644 --- a/netbox/dcim/views.py +++ b/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 {}'.format(device.get_absolute_url(), escape(device)) + msg = f'Added member {escape(device)}' 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(): diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 156e02f74..426565231 100644 --- a/netbox/extras/models/customfields.py +++ b/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: diff --git a/netbox/ipam/models/fhrp.py b/netbox/ipam/models/fhrp.py index 2a8d1bdcd..286251444 100644 --- a/netbox/ipam/models/fhrp.py +++ b/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'] diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 9ad763920..26cee8100 100644 --- a/netbox/ipam/models/ip.py +++ b/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 diff --git a/netbox/ipam/models/vrfs.py b/netbox/ipam/models/vrfs.py index fc34b5488..a926bec3e 100644 --- a/netbox/ipam/models/vrfs.py +++ b/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 diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index 2fe3503b7..b9d585952 100644 --- a/netbox/netbox/models/__init__.py +++ b/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." }) diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 7da241566..f78b9f37c 100644 --- a/netbox/netbox/tables/columns.py +++ b/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'{item}' - return item + return f'{escape(item)}' + 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('') if self.customfield.type == CustomFieldTypeChoices.TYPE_URL: - return mark_safe(f'{value}') + return mark_safe(f'{escape(value)}') 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)) diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 20586c298..09b4fc8e9 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/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(): diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index cb3f58123..5ff0cfdff 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/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 = '{} {}'.format(msg, obj.get_absolute_url(), escape(obj)) + msg = mark_safe(f'{msg} {escape(obj)}') 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 diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css index b929f176a..94718ac40 100644 Binary files a/netbox/project-static/dist/netbox-dark.css and b/netbox/project-static/dist/netbox-dark.css differ diff --git a/netbox/project-static/dist/netbox-light.css b/netbox/project-static/dist/netbox-light.css index 341369adf..bf0ba2f62 100644 Binary files a/netbox/project-static/dist/netbox-light.css and b/netbox/project-static/dist/netbox-light.css differ diff --git a/netbox/project-static/dist/netbox-print.css b/netbox/project-static/dist/netbox-print.css index 61bcedf9c..4ccd41d8e 100644 Binary files a/netbox/project-static/dist/netbox-print.css and b/netbox/project-static/dist/netbox-print.css differ diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 9ea2e5c7c..5ab9da845 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 60656bc6d..9b92d1489 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/src/search.ts b/netbox/project-static/src/search.ts index 9e8a31c5b..97fe1826a 100644 --- a/netbox/project-static/src/search.ts +++ b/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(); } diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss index a54b6c324..d78e9e9b9 100644 --- a/netbox/project-static/styles/netbox.scss +++ b/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; } diff --git a/netbox/project-static/styles/overrides.scss b/netbox/project-static/styles/overrides.scss index 03c72c6e6..7fa366df8 100644 --- a/netbox/project-static/styles/overrides.scss +++ b/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; +} diff --git a/netbox/project-static/styles/theme-dark.scss b/netbox/project-static/styles/theme-dark.scss index c0933e991..4bbe5cea5 100644 --- a/netbox/project-static/styles/theme-dark.scss +++ b/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); diff --git a/netbox/project-static/styles/theme-light.scss b/netbox/project-static/styles/theme-light.scss index d417e1bf6..c9478f1cc 100644 --- a/netbox/project-static/styles/theme-light.scss +++ b/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; diff --git a/netbox/project-static/styles/utilities.scss b/netbox/project-static/styles/utilities.scss index cd8ccc46b..a5a4bf038 100644 --- a/netbox/project-static/styles/utilities.scss +++ b/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; +} diff --git a/netbox/templates/dcim/consoleport.html b/netbox/templates/dcim/consoleport.html index f132a4ed8..39ffbf552 100644 --- a/netbox/templates/dcim/consoleport.html +++ b/netbox/templates/dcim/consoleport.html @@ -111,13 +111,13 @@ diff --git a/netbox/templates/dcim/consoleserverport.html b/netbox/templates/dcim/consoleserverport.html index f4da080e8..642e758a3 100644 --- a/netbox/templates/dcim/consoleserverport.html +++ b/netbox/templates/dcim/consoleserverport.html @@ -113,13 +113,13 @@ diff --git a/netbox/templates/dcim/device/interfaces.html b/netbox/templates/dcim/device/interfaces.html index 7db7ea0ae..ffb574ef3 100644 --- a/netbox/templates/dcim/device/interfaces.html +++ b/netbox/templates/dcim/device/interfaces.html @@ -4,81 +4,82 @@ {% load static %} {% block content %} -