From f949aa334ba0dec55ec7524d11be3798cd47356e Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 27 Jul 2024 05:02:14 +0000 Subject: [PATCH 01/23] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 32 ++++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index a6fa6e7f7..e37ad9ca6 100644 --- a/netbox/translations/en/LC_MESSAGES/django.po +++ b/netbox/translations/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-20 05:02+0000\n" +"POT-Creation-Date: 2024-07-27 05:02+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -6955,7 +6955,7 @@ msgstr "" #: netbox/extras/forms/model_forms.py:163 #: netbox/extras/forms/model_forms.py:204 #: netbox/extras/forms/model_forms.py:261 -#: netbox/extras/forms/model_forms.py:365 netbox/users/forms/model_forms.py:273 +#: netbox/extras/forms/model_forms.py:365 netbox/users/forms/model_forms.py:277 msgid "Object types" msgstr "" @@ -7321,7 +7321,7 @@ msgstr "" #: netbox/templates/extras/configcontext.html:60 #: netbox/templates/ipam/ipaddress.html:59 #: netbox/templates/ipam/vlan_edit.html:30 -#: netbox/tenancy/forms/filtersets.py:87 netbox/users/forms/model_forms.py:311 +#: netbox/tenancy/forms/filtersets.py:87 netbox/users/forms/model_forms.py:315 msgid "Assignment" msgstr "" @@ -10299,13 +10299,13 @@ msgid "Admin" msgstr "" #: netbox/netbox/navigation/menu.py:374 netbox/templates/users/group.html:29 -#: netbox/users/forms/model_forms.py:233 netbox/users/forms/model_forms.py:245 -#: netbox/users/forms/model_forms.py:297 netbox/users/tables.py:102 +#: netbox/users/forms/model_forms.py:237 netbox/users/forms/model_forms.py:249 +#: netbox/users/forms/model_forms.py:301 netbox/users/tables.py:102 msgid "Users" msgstr "" #: netbox/netbox/navigation/menu.py:394 netbox/users/forms/model_forms.py:182 -#: netbox/users/forms/model_forms.py:194 netbox/users/forms/model_forms.py:302 +#: netbox/users/forms/model_forms.py:194 netbox/users/forms/model_forms.py:306 #: netbox/users/tables.py:35 netbox/users/tables.py:106 msgid "Groups" msgstr "" @@ -10316,8 +10316,8 @@ msgid "API Tokens" msgstr "" #: netbox/netbox/navigation/menu.py:421 netbox/users/forms/model_forms.py:188 -#: netbox/users/forms/model_forms.py:196 netbox/users/forms/model_forms.py:239 -#: netbox/users/forms/model_forms.py:246 +#: netbox/users/forms/model_forms.py:196 netbox/users/forms/model_forms.py:243 +#: netbox/users/forms/model_forms.py:250 msgid "Permissions" msgstr "" @@ -12009,7 +12009,7 @@ msgstr "" #: netbox/templates/dcim/virtualchassis_add_member.html:27 #: netbox/templates/generic/object_edit.html:78 #: netbox/templates/users/objectpermission.html:31 -#: netbox/users/forms/filtersets.py:68 netbox/users/forms/model_forms.py:309 +#: netbox/users/forms/filtersets.py:68 netbox/users/forms/model_forms.py:313 msgid "Actions" msgstr "" @@ -13104,7 +13104,7 @@ msgid "View" msgstr "" #: netbox/templates/users/objectpermission.html:52 -#: netbox/users/forms/model_forms.py:312 +#: netbox/users/forms/model_forms.py:316 msgid "Constraints" msgstr "" @@ -13623,30 +13623,30 @@ msgstr "" msgid "Passwords do not match! Please check your input and try again." msgstr "" -#: netbox/users/forms/model_forms.py:291 +#: netbox/users/forms/model_forms.py:295 msgid "Additional actions" msgstr "" -#: netbox/users/forms/model_forms.py:294 +#: netbox/users/forms/model_forms.py:298 msgid "Actions granted in addition to those listed above" msgstr "" -#: netbox/users/forms/model_forms.py:310 +#: netbox/users/forms/model_forms.py:314 msgid "Objects" msgstr "" -#: netbox/users/forms/model_forms.py:322 +#: netbox/users/forms/model_forms.py:326 msgid "" "JSON expression of a queryset filter that will return only permitted " "objects. Leave null to match all objects of this type. A list of multiple " "objects will result in a logical OR operation." msgstr "" -#: netbox/users/forms/model_forms.py:361 +#: netbox/users/forms/model_forms.py:365 msgid "At least one action must be selected." msgstr "" -#: netbox/users/forms/model_forms.py:379 +#: netbox/users/forms/model_forms.py:383 #, python-brace-format msgid "Invalid filter for {model}: {error}" msgstr "" From 52692d49b63c62e9e06e6c824c044abe804c86a1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 31 Jul 2024 12:28:19 -0400 Subject: [PATCH 02/23] #9627: Fix calculation of available VIDs --- netbox/ipam/utils.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/netbox/ipam/utils.py b/netbox/ipam/utils.py index ccf6cb632..3297abd8f 100644 --- a/netbox/ipam/utils.py +++ b/netbox/ipam/utils.py @@ -90,42 +90,45 @@ def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False): return output -def available_vlans_from_range(vlans, vlan_group, vlan_range): +def available_vlans_from_range(vlans, vlan_group, vid_range): """ Create fake records for all gaps between used VLANs """ - min_vid = int(vlan_range.lower) if vlan_range else VLAN_VID_MIN - max_vid = int(vlan_range.upper) if vlan_range else VLAN_VID_MAX + min_vid = int(vid_range.lower) if vid_range else VLAN_VID_MIN + max_vid = int(vid_range.upper) if vid_range else VLAN_VID_MAX if not vlans: return [{ 'vid': min_vid, 'vlan_group': vlan_group, - 'available': max_vid - min_vid + 1 + 'available': max_vid - min_vid }] - prev_vid = max_vid + prev_vid = min_vid - 1 new_vlans = [] for vlan in vlans: + + # Ignore VIDs outside the range + if not min_vid <= vlan.vid < max_vid: + continue + + # Annotate any available VIDs between the previous (or minimum) VID + # and the current VID if vlan.vid - prev_vid > 1: new_vlans.append({ 'vid': prev_vid + 1, 'vlan_group': vlan_group, 'available': vlan.vid - prev_vid - 1, }) + prev_vid = vlan.vid - if vlans[0].vid > min_vid: - new_vlans.append({ - 'vid': min_vid, - 'vlan_group': vlan_group, - 'available': vlans[0].vid - min_vid, - }) + # Annotate any remaining available VLANs if prev_vid < max_vid: new_vlans.append({ 'vid': prev_vid + 1, 'vlan_group': vlan_group, - 'available': max_vid - prev_vid, + 'available': max_vid - prev_vid - 1, }) return new_vlans @@ -136,8 +139,8 @@ def add_available_vlans(vlans, vlan_group): Create fake records for all gaps between used VLANs """ new_vlans = [] - for vlan_range in vlan_group.vid_ranges: - new_vlans.extend(available_vlans_from_range(vlans, vlan_group, vlan_range)) + for vid_range in vlan_group.vid_ranges: + new_vlans.extend(available_vlans_from_range(vlans, vlan_group, vid_range)) vlans = list(vlans) + new_vlans vlans.sort(key=lambda v: v.vid if type(v) is VLAN else v['vid']) From 047d717532d12c2d3c842aea2edc62a1cb69bc9a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 31 Jul 2024 14:04:51 -0400 Subject: [PATCH 03/23] Add missing table columns --- netbox/dcim/tables/racks.py | 32 +++++++++++++++++++++++--------- netbox/dcim/views.py | 4 +++- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index 064a5a43d..b8295c286 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -84,6 +84,11 @@ class RackTypeTable(NetBoxTable): comments = columns.MarkdownColumn( verbose_name=_('Comments'), ) + instance_count = columns.LinkedCountColumn( + viewname='dcim:rack_list', + url_params={'rack_type_id': 'pk'}, + verbose_name=_('Instances') + ) tags = columns.TagColumn( url_name='dcim:rack_list' ) @@ -92,11 +97,11 @@ class RackTypeTable(NetBoxTable): model = RackType fields = ( 'pk', 'id', 'name', 'manufacturer', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width', - 'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'description', 'comments', 'tags', - 'created', 'last_updated', + 'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'description', 'comments', + 'instance_count', 'tags', 'created', 'last_updated', ) default_columns = ( - 'pk', 'name', 'manufacturer', 'type', 'u_height', 'description', + 'pk', 'name', 'manufacturer', 'type', 'u_height', 'description', 'instance_count', ) @@ -124,6 +129,15 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): role = columns.ColoredLabelColumn( verbose_name=_('Role'), ) + manufacturer = tables.Column( + verbose_name=_('Manufacturer'), + accessor=Accessor('rack_type__manufacturer'), + linkify=True + ) + rack_type = tables.Column( + linkify=True, + verbose_name=_('Type') + ) u_height = tables.TemplateColumn( template_code="{{ value }}U", verbose_name=_('Height') @@ -169,14 +183,14 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = Rack fields = ( - 'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial', - 'asset_tag', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width', 'outer_depth', - 'mounting_depth', 'airflow', 'weight', 'max_weight', 'comments', 'device_count', 'get_utilization', - 'get_power_utilization', 'description', 'contacts', 'tags', 'created', 'last_updated', + 'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', + 'rack_type', 'serial', 'asset_tag', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width', + 'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'comments', 'device_count', + 'get_utilization', 'get_power_utilization', 'description', 'contacts', 'tags', 'created', 'last_updated', ) default_columns = ( - 'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count', - 'get_utilization', + 'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'rack_type', 'u_height', + 'device_count', 'get_utilization', ) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 5ff20f35e..5ad619452 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -584,7 +584,9 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView): # class RackTypeListView(generic.ObjectListView): - queryset = RackType.objects.all() + queryset = RackType.objects.annotate( + instance_count=count_related(Rack, 'rack_type') + ) filterset = filtersets.RackTypeFilterSet filterset_form = forms.RackTypeFilterForm table = tables.RackTypeTable From da7d47d3ed5ccc85cb5ade707a85c8d4fbbd1b32 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 31 Jul 2024 14:06:07 -0400 Subject: [PATCH 04/23] Add manufacturer filter for Rack; extend RackFilterForm --- netbox/dcim/filtersets.py | 11 +++++++++++ netbox/dcim/forms/filtersets.py | 17 +++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index bcd1f23b2..5e404179a 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -375,6 +375,17 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe to_field_name='slug', label=_('Location (slug)'), ) + manufacturer_id = django_filters.ModelMultipleChoiceFilter( + field_name='rack_type__manufacturer', + queryset=Manufacturer.objects.all(), + label=_('Manufacturer (ID)'), + ) + manufacturer = django_filters.ModelMultipleChoiceFilter( + field_name='rack_type__manufacturer__slug', + queryset=Manufacturer.objects.all(), + to_field_name='slug', + label=_('Manufacturer (slug)'), + ) rack_type = django_filters.ModelMultipleChoiceFilter( field_name='rack_type__slug', queryset=RackType.objects.all(), diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index c8124aa11..b43ed85ce 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -312,8 +312,8 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterFo FieldSet('q', 'filter_id', 'tag'), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), - FieldSet('status', 'role_id', 'serial', 'asset_tag', name=_('Rack')), - FieldSet('form_factor', 'width', 'u_height', 'airflow', name=_('Rack Type')), + FieldSet('status', 'role_id', 'manufacturer_id', 'rack_type_id', 'serial', 'asset_tag', name=_('Rack')), + FieldSet('form_factor', 'width', 'u_height', 'airflow', name=_('Hardware')), FieldSet('starting_unit', 'desc_units', name=_('Numbering')), FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), @@ -357,6 +357,19 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterFo null_option='None', label=_('Role') ) + manufacturer_id = DynamicModelMultipleChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + label=_('Manufacturer') + ) + rack_type_id = DynamicModelMultipleChoiceField( + queryset=RackType.objects.all(), + required=False, + query_params={ + 'manufacturer_id': '$manufacturer_id' + }, + label=_('Rack type') + ) serial = forms.CharField( label=_('Serial'), required=False From 8d585031d851b897b06bedb86935264efdb7094f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 31 Jul 2024 14:06:35 -0400 Subject: [PATCH 05/23] Misc cleanup --- netbox/dcim/models/racks.py | 8 ++++++-- netbox/templates/dcim/rack.html | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index cfa8f28be..8457271f4 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -152,8 +152,8 @@ class RackType(RackBase): ) clone_fields = ( - 'manufacturer', 'form_factor', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', - 'mounting_depth', 'weight', 'max_weight', 'weight_unit', + 'manufacturer', 'form_factor', 'width', 'u_height', 'airflow', 'desc_units', 'outer_width', 'outer_depth', + 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', ) prerequisite_models = ( 'dcim.Manufacturer', @@ -170,6 +170,10 @@ class RackType(RackBase): def get_absolute_url(self): return reverse('dcim:racktype', args=[self.pk]) + @property + def full_name(self): + return f"{self.manufacturer} {self.name}" + def clean(self): super().clean() diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index f69abe77a..128be6d3f 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -43,7 +43,7 @@ {% trans "Rack Type" %} - {{ object.rack_type|linkify|placeholder }} + {{ object.rack_type|linkify:"full_name"|placeholder }} {% trans "Role" %} From 2168a73a2dde4a4374472df64c485d5f80e7c384 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 31 Jul 2024 14:40:05 -0400 Subject: [PATCH 06/23] Display manufacturer & model of module type on module view --- netbox/dcim/models/devices.py | 4 ++++ netbox/templates/dcim/module.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index a790cceef..c6e97a3cd 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -417,6 +417,10 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): def get_absolute_url(self): return reverse('dcim:moduletype', args=[self.pk]) + @property + def full_name(self): + return f"{self.manufacturer} {self.model}" + def to_yaml(self): data = { 'manufacturer': self.manufacturer.name, diff --git a/netbox/templates/dcim/module.html b/netbox/templates/dcim/module.html index 2180a83a3..61089e715 100644 --- a/netbox/templates/dcim/module.html +++ b/netbox/templates/dcim/module.html @@ -60,7 +60,7 @@ {% trans "Module Type" %} - {{ object.module_type|linkify }} + {{ object.module_type|linkify:"full_name" }} {% trans "Status" %} From 5cfa2bb561052d55cb752487c8d850db8e3204a4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 31 Jul 2024 15:11:40 -0400 Subject: [PATCH 07/23] #15621: Add feature documentation for user notifications --- docs/features/event-rules.md | 7 ++++--- docs/features/notifications.md | 10 ++++++++++ mkdocs.yml | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 docs/features/notifications.md diff --git a/docs/features/event-rules.md b/docs/features/event-rules.md index 158dc111a..14b54f000 100644 --- a/docs/features/event-rules.md +++ b/docs/features/event-rules.md @@ -1,9 +1,10 @@ # Event Rules -NetBox includes the ability to execute certain functions in response to internal object changes. These include: +NetBox includes the ability to automatically perform certain functions in response to internal events. These include: -* [Scripts](../customization/custom-scripts.md) execution -* [Webhooks](../integrations/webhooks.md) execution +* Executing a [custom script](../customization/custom-scripts.md) +* Sending a [webhook](../integrations/webhooks.md) +* Generating [user notifications](../features/notifications.md) For example, suppose you want to automatically configure a monitoring system to start monitoring a device when its operational status is changed to active, and remove it from monitoring for any other status. You can create a webhook in NetBox for the device model and craft its content and destination URL to effect the desired change on the receiving system. You can then associate an event rule with this webhook and the webhook will be sent automatically by NetBox whenever the configured constraints are met. diff --git a/docs/features/notifications.md b/docs/features/notifications.md new file mode 100644 index 000000000..a28a17947 --- /dev/null +++ b/docs/features/notifications.md @@ -0,0 +1,10 @@ +# Notifications + +!!! info "This feature was introduced in NetBox v4.1." + +NetBox includes a system for generating user notifications, which can be marked as read or deleted by individual users. There are two built-in mechanisms for generating a notification: + +* A user can subscribe to an object. When that object is modified, a notification is created to inform the user of the change. +* An [event rule](./event-rules.md) can be defined to automatically generate a notification for one or more users in response to specific system events. + +Additionally, NetBox plugins can generate notifications for their own purposes. diff --git a/mkdocs.yml b/mkdocs.yml index ea93ee584..f1e007dc3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -86,6 +86,7 @@ nav: - Change Logging: 'features/change-logging.md' - Journaling: 'features/journaling.md' - Event Rules: 'features/event-rules.md' + - Notifications: 'features/notifications.md' - Background Jobs: 'features/background-jobs.md' - Auth & Permissions: 'features/authentication-permissions.md' - API & Integration: 'features/api-integration.md' From d1e16013f033c4ad54ab6f348672fcb012fa661f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 31 Jul 2024 15:39:05 -0400 Subject: [PATCH 08/23] #16886: Rename Event to EventType --- docs/plugins/development/events.md | 6 +-- netbox/core/events.py | 16 +++---- netbox/extras/models/notifications.py | 4 +- netbox/extras/views.py | 2 +- netbox/netbox/events.py | 60 +++++++++++++-------------- netbox/netbox/registry.py | 2 +- 6 files changed, 45 insertions(+), 45 deletions(-) diff --git a/docs/plugins/development/events.md b/docs/plugins/development/events.md index f41ed7ff5..bbf9783e2 100644 --- a/docs/plugins/development/events.md +++ b/docs/plugins/development/events.md @@ -4,12 +4,12 @@ Plugins can register their own custom event types for use with NetBox [event rul ```python from django.utils.translation import gettext_lazy as _ -from netbox.events import Event, EVENT_TYPE_SUCCESS +from netbox.events import EventType, EVENT_TYPE_KIND_SUCCESS -Event( +EventType( name='ticket_opened', text=_('Ticket opened'), - type=EVENT_TYPE_SUCCESS + type=EVENT_TYPE_KIND_SUCCESS ).register() ``` diff --git a/netbox/core/events.py b/netbox/core/events.py index 4855e5b23..98a1ea18f 100644 --- a/netbox/core/events.py +++ b/netbox/core/events.py @@ -1,6 +1,6 @@ from django.utils.translation import gettext as _ -from netbox.events import Event, EVENT_TYPE_DANGER, EVENT_TYPE_SUCCESS, EVENT_TYPE_WARNING +from netbox.events import EventType, EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING __all__ = ( 'JOB_COMPLETED', @@ -24,10 +24,10 @@ JOB_FAILED = 'job_failed' JOB_ERRORED = 'job_errored' # Register core events -Event(OBJECT_CREATED, _('Object created')).register() -Event(OBJECT_UPDATED, _('Object updated')).register() -Event(OBJECT_DELETED, _('Object deleted')).register() -Event(JOB_STARTED, _('Job started')).register() -Event(JOB_COMPLETED, _('Job completed'), type=EVENT_TYPE_SUCCESS).register() -Event(JOB_FAILED, _('Job failed'), type=EVENT_TYPE_WARNING).register() -Event(JOB_ERRORED, _('Job errored'), type=EVENT_TYPE_DANGER).register() +EventType(OBJECT_CREATED, _('Object created')).register() +EventType(OBJECT_UPDATED, _('Object updated')).register() +EventType(OBJECT_DELETED, _('Object deleted')).register() +EventType(JOB_STARTED, _('Job started')).register() +EventType(JOB_COMPLETED, _('Job completed'), kind=EVENT_TYPE_KIND_SUCCESS).register() +EventType(JOB_FAILED, _('Job failed'), kind=EVENT_TYPE_KIND_WARNING).register() +EventType(JOB_ERRORED, _('Job errored'), kind=EVENT_TYPE_KIND_DANGER).register() diff --git a/netbox/extras/models/notifications.py b/netbox/extras/models/notifications.py index dba059ea7..a15dfe9aa 100644 --- a/netbox/extras/models/notifications.py +++ b/netbox/extras/models/notifications.py @@ -27,7 +27,7 @@ def get_event_type_choices(): """ return [ (name, event.text) - for name, event in registry['events'].items() + for name, event in registry['event_types'].items() ] @@ -102,7 +102,7 @@ class Notification(models.Model): """ Returns the registered Event which triggered this Notification. """ - return registry['events'].get(self.event_type) + return registry['event_types'].get(self.event_type) class NotificationGroup(ChangeLoggedModel): diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 54f00265a..2a5b6ef05 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -554,7 +554,7 @@ class EventRuleView(generic.ObjectView): def get_extra_context(self, request, instance): return { 'event_types': [ - event for name, event in registry['events'].items() + event for name, event in registry['event_types'].items() if name in instance.event_types ] } diff --git a/netbox/netbox/events.py b/netbox/netbox/events.py index ee2561720..2fa4b6e78 100644 --- a/netbox/netbox/events.py +++ b/netbox/netbox/events.py @@ -2,41 +2,41 @@ from dataclasses import dataclass from netbox.registry import registry -EVENT_TYPE_INFO = 'info' -EVENT_TYPE_SUCCESS = 'success' -EVENT_TYPE_WARNING = 'warning' -EVENT_TYPE_DANGER = 'danger' +EVENT_TYPE_KIND_INFO = 'info' +EVENT_TYPE_KIND_SUCCESS = 'success' +EVENT_TYPE_KIND_WARNING = 'warning' +EVENT_TYPE_KIND_DANGER = 'danger' __all__ = ( - 'EVENT_TYPE_DANGER', - 'EVENT_TYPE_INFO', - 'EVENT_TYPE_SUCCESS', - 'EVENT_TYPE_WARNING', - 'Event', - 'get_event', + 'EVENT_TYPE_KIND_DANGER', + 'EVENT_TYPE_KIND_INFO', + 'EVENT_TYPE_KIND_SUCCESS', + 'EVENT_TYPE_KIND_WARNING', + 'EventType', + 'get_event_type', 'get_event_type_choices', 'get_event_text', ) -def get_event(name): - return registry['events'].get(name) +def get_event_type(name): + return registry['event_types'].get(name) def get_event_text(name): - if event := registry['events'].get(name): + if event := registry['event_types'].get(name): return event.text return '' def get_event_type_choices(): return [ - (event.name, event.text) for event in registry['events'].values() + (event.name, event.text) for event in registry['event_types'].values() ] @dataclass -class Event: +class EventType: """ A type of event which can occur in NetBox. Event rules can be defined to automatically perform some action in response to an event. @@ -44,32 +44,32 @@ class Event: Args: name: The unique name under which the event is registered. text: The human-friendly event name. This should support translation. - type: The event's classification (info, success, warning, or danger). The default type is info. + kind: The event's classification (info, success, warning, or danger). The default type is info. """ name: str text: str - type: str = EVENT_TYPE_INFO + kind: str = EVENT_TYPE_KIND_INFO def __str__(self): return self.text def register(self): - if self.name in registry['events']: - raise Exception(f"An event named {self.name} has already been registered!") - registry['events'][self.name] = self + if self.name in registry['event_types']: + raise Exception(f"An event type named {self.name} has already been registered!") + registry['event_types'][self.name] = self def color(self): return { - EVENT_TYPE_INFO: 'blue', - EVENT_TYPE_SUCCESS: 'green', - EVENT_TYPE_WARNING: 'orange', - EVENT_TYPE_DANGER: 'red', - }.get(self.type) + EVENT_TYPE_KIND_INFO: 'blue', + EVENT_TYPE_KIND_SUCCESS: 'green', + EVENT_TYPE_KIND_WARNING: 'orange', + EVENT_TYPE_KIND_DANGER: 'red', + }.get(self.kind) def icon(self): return { - EVENT_TYPE_INFO: 'mdi mdi-information', - EVENT_TYPE_SUCCESS: 'mdi mdi-check-circle', - EVENT_TYPE_WARNING: 'mdi mdi-alert-box', - EVENT_TYPE_DANGER: 'mdi mdi-alert-octagon', - }.get(self.type) + EVENT_TYPE_KIND_INFO: 'mdi mdi-information', + EVENT_TYPE_KIND_SUCCESS: 'mdi mdi-check-circle', + EVENT_TYPE_KIND_WARNING: 'mdi mdi-alert-box', + EVENT_TYPE_KIND_DANGER: 'mdi mdi-alert-octagon', + }.get(self.kind) diff --git a/netbox/netbox/registry.py b/netbox/netbox/registry.py index 44cdfb92b..0920cbccf 100644 --- a/netbox/netbox/registry.py +++ b/netbox/netbox/registry.py @@ -25,7 +25,7 @@ registry = Registry({ 'counter_fields': collections.defaultdict(dict), 'data_backends': dict(), 'denormalized_fields': collections.defaultdict(list), - 'events': dict(), + 'event_types': dict(), 'model_features': dict(), 'models': collections.defaultdict(set), 'plugins': dict(), From 6e59db7310c32dce6cbaf5a6c42a26a5bb13c2f4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 31 Jul 2024 15:54:31 -0400 Subject: [PATCH 09/23] #16886: Updated documentation for event types --- docs/models/extras/eventrule.md | 23 +++++++++++-------- .../development/{events.md => event-types.md} | 10 ++++---- mkdocs.yml | 2 +- 3 files changed, 21 insertions(+), 14 deletions(-) rename docs/plugins/development/{events.md => event-types.md} (63%) diff --git a/docs/models/extras/eventrule.md b/docs/models/extras/eventrule.md index c105a2630..b48e17a1e 100644 --- a/docs/models/extras/eventrule.md +++ b/docs/models/extras/eventrule.md @@ -18,17 +18,22 @@ The type(s) of object in NetBox that will trigger the rule. If not selected, the event rule will not be processed. -### Events +### Events Types -The events which will trigger the rule. At least one event type must be selected. +The event types which will trigger the rule. At least one event type must be selected. -| Name | Description | -|------------|--------------------------------------| -| Creations | A new object has been created | -| Updates | An existing object has been modified | -| Deletions | An object has been deleted | -| Job starts | A job for an object starts | -| Job ends | A job for an object terminates | +| Name | Description | +|----------------|---------------------------------------------| +| Object created | A new object has been created | +| Object updated | An existing object has been modified | +| Object deleted | An object has been deleted | +| Job started | A background job is initiated | +| Job completed | A background job completes successfully | +| Job failed | A background job fails | +| Job errored | A background job is aborted due to an error | + +!!! tip "Custom Event Types" + The above list includes only built-in event types. NetBox plugins can also register their own custom event types. ### Conditions diff --git a/docs/plugins/development/events.md b/docs/plugins/development/event-types.md similarity index 63% rename from docs/plugins/development/events.md rename to docs/plugins/development/event-types.md index bbf9783e2..4bcdeea31 100644 --- a/docs/plugins/development/events.md +++ b/docs/plugins/development/event-types.md @@ -1,6 +1,8 @@ -# Events +# Event Types -Plugins can register their own custom event types for use with NetBox [event rules](../../models/extras/eventrule.md). This is accomplished by calling the `register()` method on an instance of the `Event` class. This can be done anywhere within the plugin. An example is provided below. +!!! info "This feature was introduced in NetBox v4.1." + +Plugins can register their own custom event types for use with NetBox [event rules](../../models/extras/eventrule.md). This is accomplished by calling the `register()` method on an instance of the `EventType` class. This can be done anywhere within the plugin. An example is provided below. ```python from django.utils.translation import gettext_lazy as _ @@ -9,8 +11,8 @@ from netbox.events import EventType, EVENT_TYPE_KIND_SUCCESS EventType( name='ticket_opened', text=_('Ticket opened'), - type=EVENT_TYPE_KIND_SUCCESS + kind=EVENT_TYPE_KIND_SUCCESS ).register() ``` -::: netbox.events.Event +::: netbox.events.EventType diff --git a/mkdocs.yml b/mkdocs.yml index f1e007dc3..072c564e8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -143,7 +143,7 @@ nav: - Forms: 'plugins/development/forms.md' - Filters & Filter Sets: 'plugins/development/filtersets.md' - Search: 'plugins/development/search.md' - - Events: 'plugins/development/events.md' + - Event Types: 'plugins/development/event-types.md' - Data Backends: 'plugins/development/data-backends.md' - REST API: 'plugins/development/rest-api.md' - GraphQL API: 'plugins/development/graphql-api.md' From a49a74236be2ad1bb516c0969c845e1033cb83a3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 31 Jul 2024 16:02:20 -0400 Subject: [PATCH 10/23] #16886: Misc cleanup --- docs/release-notes/version-4.1.md | 4 ++++ netbox/extras/views.py | 8 -------- netbox/templates/extras/eventrule.html | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/release-notes/version-4.1.md b/docs/release-notes/version-4.1.md index 38660447f..9ff850832 100644 --- a/docs/release-notes/version-4.1.md +++ b/docs/release-notes/version-4.1.md @@ -7,6 +7,7 @@ * Several filters deprecated in v4.0 have been removed (see [#15410](https://github.com/netbox-community/netbox/issues/15410)). * The unit size for virtual disk size has been changed from 1 gigabyte to 1 megabyte. Existing values have been updated accordingly. * The `min_vid` and `max_vid` fields on the VLAN group model have been replaced with `vid_ranges`, an array of starting and ending integer pairs. +* The five individual event type fields on the EventRule model have been replaced by a single `event_types` array field, indicating each assigned event type by name. * The `validate()` method on CustomValidator subclasses now **must** accept the request argument (deprecated in v4.0 by #14279). ### New Features @@ -72,6 +73,9 @@ NetBox now includes a user notification system. Users can subscribe to individua * Added the optional `airflow` choice field * extras.CustomField * Added the `related_object_filter` JSON field for object and multi-object custom fields +* extras.EventRule + * Removed the `type_create`, `type_update`, `type_delete`, `type_job_start`, and `type_job_end` boolean fields + * Added the `event_types` array field * ipam.VLANGroup * Removed the `min_vid` and `max_vid` fields * Added the `vid_ranges` field, and array of starting & ending VLAN IDs diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 2a5b6ef05..8eb9f412d 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -551,14 +551,6 @@ class EventRuleListView(generic.ObjectListView): class EventRuleView(generic.ObjectView): queryset = EventRule.objects.all() - def get_extra_context(self, request, instance): - return { - 'event_types': [ - event for name, event in registry['event_types'].items() - if name in instance.event_types - ] - } - @register_model_view(EventRule, 'edit') class EventRuleEditView(generic.ObjectEditView): diff --git a/netbox/templates/extras/eventrule.html b/netbox/templates/extras/eventrule.html index 4ee09af03..51b10f4d9 100644 --- a/netbox/templates/extras/eventrule.html +++ b/netbox/templates/extras/eventrule.html @@ -36,7 +36,7 @@
{% trans "Event Types" %}
    - {% for name, event in registry.events.items %} + {% for name, event in registry.event_types.items %}
  • From 28b867bde412dfc8275a62fd049ce72a34ebe0a4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 31 Jul 2024 16:26:21 -0400 Subject: [PATCH 11/23] Documentation updates for v4.1 --- docs/customization/custom-validation.md | 4 ---- docs/models/circuits/circuitgroup.md | 2 ++ docs/models/dcim/moduletype.md | 2 ++ docs/models/dcim/racktype.md | 2 ++ docs/models/extras/customfield.md | 2 ++ docs/models/ipam/vlangroup.md | 2 ++ docs/models/virtualization/virtualmachine.md | 8 ++++++-- docs/models/wireless/wirelesslink.md | 10 ++++++---- docs/plugins/development/background-jobs.md | 2 ++ docs/plugins/development/views.md | 2 +- docs/release-notes/version-4.1.md | 2 +- 11 files changed, 26 insertions(+), 12 deletions(-) diff --git a/docs/customization/custom-validation.md b/docs/customization/custom-validation.md index 909846e20..4a2aab998 100644 --- a/docs/customization/custom-validation.md +++ b/docs/customization/custom-validation.md @@ -86,8 +86,6 @@ CUSTOM_VALIDATORS = { #### Referencing Related Object Attributes -!!! info "This feature was introduced in NetBox v4.0." - The attributes of a related object can be referenced by specifying a dotted path. For example, to reference the name of a region to which a site is assigned, use `region.name`: ```python @@ -104,8 +102,6 @@ CUSTOM_VALIDATORS = { #### Validating Request Parameters -!!! info "This feature was introduced in NetBox v4.0." - In addition to validating object attributes, custom validators can also match against parameters of the current request (where available). For example, the following rule will permit only the user named "admin" to modify an object: ```json diff --git a/docs/models/circuits/circuitgroup.md b/docs/models/circuits/circuitgroup.md index 6d1503509..faa9dbc14 100644 --- a/docs/models/circuits/circuitgroup.md +++ b/docs/models/circuits/circuitgroup.md @@ -1,5 +1,7 @@ # Circuit Groups +!!! info "This feature was introduced in NetBox v4.1." + [Circuits](./circuit.md) can be arranged into administrative groups for organization. The assignment of a circuit to a group is optional. ## Fields diff --git a/docs/models/dcim/moduletype.md b/docs/models/dcim/moduletype.md index 7077e16c2..225873d61 100644 --- a/docs/models/dcim/moduletype.md +++ b/docs/models/dcim/moduletype.md @@ -42,4 +42,6 @@ The numeric weight of the module, including a unit designation (e.g. 3 kilograms ### Airflow +!!! info "The `airflow` field was introduced in NetBox v4.1." + The direction in which air circulates through the device chassis for cooling. diff --git a/docs/models/dcim/racktype.md b/docs/models/dcim/racktype.md index 476dbe178..73fb08bcd 100644 --- a/docs/models/dcim/racktype.md +++ b/docs/models/dcim/racktype.md @@ -1,5 +1,7 @@ # Rack Types +!!! info "This feature was introduced in NetBox v4.1." + A rack type defines the physical characteristics of a particular model of [rack](./rack.md). ## Fields diff --git a/docs/models/extras/customfield.md b/docs/models/extras/customfield.md index 164ce3a74..626f320be 100644 --- a/docs/models/extras/customfield.md +++ b/docs/models/extras/customfield.md @@ -44,6 +44,8 @@ For object and multiple-object fields only. Designates the type of NetBox object ### Related Object Filter +!!! info "This field was introduced in NetBox v4.1." + For object and multi-object custom fields, a filter may be defined to limit the available objects when populating a field value. This filter maps object attributes to values. For example, `{"status": "active"}` will include only objects with a status of "active." !!! warning diff --git a/docs/models/ipam/vlangroup.md b/docs/models/ipam/vlangroup.md index 67050ab4c..20989452f 100644 --- a/docs/models/ipam/vlangroup.md +++ b/docs/models/ipam/vlangroup.md @@ -16,6 +16,8 @@ A unique URL-friendly identifier. (This value can be used for filtering.) ### VLAN ID Ranges +!!! info "This field replaced the legacy `min_vid` and `max_vid` fields in NetBox v4.1." + The set of VLAN IDs which are encompassed by the group. By default, this will be the entire range of valid IEEE 802.1Q VLAN IDs (1 to 4094, inclusive). VLANs created within a group must have a VID that falls within one of these ranges. Ranges may not overlap. ### Scope diff --git a/docs/models/virtualization/virtualmachine.md b/docs/models/virtualization/virtualmachine.md index 7a801ca65..7ea31111c 100644 --- a/docs/models/virtualization/virtualmachine.md +++ b/docs/models/virtualization/virtualmachine.md @@ -50,9 +50,13 @@ The amount of running memory provisioned, in megabytes. ### Disk -The amount of disk storage provisioned, in gigabytes. +The amount of disk storage provisioned, in megabytes. + +!!! warning + This field may be directly modified only on virtual machines which do not define discrete [virtual disks](./virtualdisk.md). Otherwise, it will report the sum of all attached disks. ### Serial Number -Optional serial number assigned to this VM. +!!! info "This field was introduced in NetBox v4.1." +Optional serial number assigned to this virtual machine. Unlike devices, uniqueness is not enforced for virtual machine serial numbers. diff --git a/docs/models/wireless/wirelesslink.md b/docs/models/wireless/wirelesslink.md index e670b69ec..7553902b0 100644 --- a/docs/models/wireless/wirelesslink.md +++ b/docs/models/wireless/wirelesslink.md @@ -20,6 +20,12 @@ The operational status of the link. Options include: The service set identifier (SSID) for the wireless link (optional). +### Distance + +!!! info "This field was introduced in NetBox v4.1." + +The distance between the link's two endpoints, including a unit designation (e.g. 100 meters or 25 feet). + ### Authentication Type The type of wireless authentication in use. Options include: @@ -40,7 +46,3 @@ The security cipher used to apply wireless authentication. Options include: ### Pre-Shared Key The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types. - -### Distance - -The numeric distance of the link, including a unit designation (e.g. 100 meters or 25 feet). diff --git a/docs/plugins/development/background-jobs.md b/docs/plugins/development/background-jobs.md index e642fe585..810435268 100644 --- a/docs/plugins/development/background-jobs.md +++ b/docs/plugins/development/background-jobs.md @@ -1,5 +1,7 @@ # Background Jobs +!!! info "This feature was introduced in NetBox v4.1." + NetBox plugins can defer certain operations by enqueuing [background jobs](../../features/background-jobs.md), which are executed asynchronously by background workers. This is helpful for decoupling long-running processes from the user-facing request-response cycle. For example, your plugin might need to fetch data from a remote system. Depending on the amount of data and the responsiveness of the remote server, this could take a few minutes. Deferring this task to a queued job ensures that it can be completed in the background, without interrupting the user. The data it fetches can be made available once the job has completed. diff --git a/docs/plugins/development/views.md b/docs/plugins/development/views.md index cbf920ad5..1f5f164fd 100644 --- a/docs/plugins/development/views.md +++ b/docs/plugins/development/views.md @@ -203,7 +203,7 @@ Plugins can inject custom content into certain areas of core NetBox views. This | `right_page()` | Object view | Inject content on the right side of the page | | `full_width_page()` | Object view | Inject content across the entire bottom of the page | -!!! info "The `navbar()` method was introduced in NetBox v4.1." +!!! info "The `navbar()` and `alerts()` methods were introduced in NetBox v4.1." Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however. diff --git a/docs/release-notes/version-4.1.md b/docs/release-notes/version-4.1.md index 9ff850832..dd846269d 100644 --- a/docs/release-notes/version-4.1.md +++ b/docs/release-notes/version-4.1.md @@ -5,7 +5,7 @@ ### Breaking Changes * Several filters deprecated in v4.0 have been removed (see [#15410](https://github.com/netbox-community/netbox/issues/15410)). -* The unit size for virtual disk size has been changed from 1 gigabyte to 1 megabyte. Existing values have been updated accordingly. +* The unit size for `VirtualMachine.disk` and `VirtualDisk.size` been changed from 1 gigabyte to 1 megabyte. Existing values have been updated accordingly. * The `min_vid` and `max_vid` fields on the VLAN group model have been replaced with `vid_ranges`, an array of starting and ending integer pairs. * The five individual event type fields on the EventRule model have been replaced by a single `event_types` array field, indicating each assigned event type by name. * The `validate()` method on CustomValidator subclasses now **must** accept the request argument (deprecated in v4.0 by #14279). From 77d0e9032a722a764d5fd4d82a39f5618a8cc282 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Thu, 1 Aug 2024 13:12:58 +0700 Subject: [PATCH 12/23] fix internationalization for string --- netbox/core/models/jobs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/core/models/jobs.py b/netbox/core/models/jobs.py index 1d0e7fdeb..4a327a1d8 100644 --- a/netbox/core/models/jobs.py +++ b/netbox/core/models/jobs.py @@ -214,7 +214,7 @@ class Job(models.Model): management commands only. """ if schedule_at and immediate: - raise ValueError("enqueue() cannot be called with values for both schedule_at and immediate.") + raise ValueError(_("enqueue() cannot be called with values for both schedule_at and immediate.")) if instance: object_type = ObjectType.objects.get_for_model(instance, for_concrete_model=False) From 1a6406632ab1295a4f04e59fbc10eb992398403e Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Thu, 1 Aug 2024 13:59:20 +0700 Subject: [PATCH 13/23] For #12826 add tests to RackTest filtersets for manufacturer and DeviceType --- netbox/dcim/tests/test_filtersets.py | 95 ++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 6 deletions(-) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 4ef271620..ed2db958f 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -653,6 +653,53 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests): for location in locations: location.save() + manufacturers = ( + Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), + Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), + Manufacturer(name='Manufacturer 3', slug='manufacturer-3'), + ) + Manufacturer.objects.bulk_create(manufacturers) + + rack_types = ( + RackType( + manufacturer=manufacturers[0], + name='RackType 1', + slug='rack-type-1', + form_factor=RackFormFactorChoices.TYPE_2POST, + width=RackWidthChoices.WIDTH_19IN, + u_height=42, + starting_unit=1, + desc_units=False, + outer_width=100, + outer_depth=100, + outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER, + mounting_depth=100, + weight=10, + max_weight=1000, + weight_unit=WeightUnitChoices.UNIT_POUND, + description='foobar1' + ), + RackType( + manufacturer=manufacturers[1], + name='RackType 2', + slug='rack-type-2', + form_factor=RackFormFactorChoices.TYPE_4POST, + width=RackWidthChoices.WIDTH_21IN, + u_height=43, + starting_unit=2, + desc_units=False, + outer_width=200, + outer_depth=200, + outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER, + mounting_depth=200, + weight=20, + max_weight=2000, + weight_unit=WeightUnitChoices.UNIT_POUND, + description='foobar2' + ), + ) + RackType.objects.bulk_create(rack_types) + rack_roles = ( RackRole(name='Rack Role 1', slug='rack-role-1'), RackRole(name='Rack Role 2', slug='rack-role-2'), @@ -742,6 +789,28 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests): weight_unit=WeightUnitChoices.UNIT_KILOGRAM, description='foobar3' ), + Rack( + name='Rack 4', + facility_id='rack-4', + site=sites[2], + location=locations[2], + tenant=tenants[2], + status=RackStatusChoices.STATUS_PLANNED, + role=rack_roles[2], + rack_type=rack_types[0], + description='foobar4' + ), + Rack( + name='Rack 5', + facility_id='rack-5', + site=sites[2], + location=locations[2], + tenant=tenants[2], + status=RackStatusChoices.STATUS_PLANNED, + role=rack_roles[2], + rack_type=rack_types[1], + description='foobar5' + ), ) Rack.objects.bulk_create(racks) @@ -771,15 +840,15 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests): def test_width(self): params = {'width': [RackWidthChoices.WIDTH_19IN, RackWidthChoices.WIDTH_21IN]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_u_height(self): params = {'u_height': [42, 43]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_starting_unit(self): params = {'starting_unit': [1]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) params = {'starting_unit': [2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0) @@ -787,7 +856,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'desc_units': 'true'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) params = {'desc_units': 'false'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_outer_width(self): params = {'outer_width': [100, 200]} @@ -798,7 +867,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_outer_unit(self): - self.assertEqual(Rack.objects.filter(outer_unit__isnull=False).count(), 3) + self.assertEqual(Rack.objects.filter(outer_unit__isnull=False).count(), 5) params = {'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -832,7 +901,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests): def test_status(self): params = {'status': [RackStatusChoices.STATUS_ACTIVE, RackStatusChoices.STATUS_PLANNED]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_role(self): roles = RackRole.objects.all()[:2] @@ -873,6 +942,20 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'weight_unit': WeightUnitChoices.UNIT_POUND} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_manufacturer(self): + manufacturers = Manufacturer.objects.all()[:2] + params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_rack_type(self): + rack_types = RackType.objects.all()[:2] + params = {'rack_type_id': [rack_types[0].pk, rack_types[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'rack_type': [rack_types[0].slug, rack_types[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = RackReservation.objects.all() From c51e91dddd5b24266491d8187d5fc445a87f6242 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 31 Jul 2024 16:45:07 -0400 Subject: [PATCH 14/23] Closes #17048: Replace all calls to get_user_model() with direct imports of User --- netbox/core/filtersets.py | 6 +++--- netbox/core/forms/filtersets.py | 6 +++--- netbox/core/management/commands/nbshell.py | 4 ++-- netbox/dcim/filtersets.py | 6 +++--- netbox/dcim/forms/bulk_edit.py | 6 ++---- netbox/dcim/forms/filtersets.py | 4 ++-- netbox/dcim/forms/model_forms.py | 6 ++---- netbox/dcim/tests/test_api.py | 5 +---- netbox/dcim/tests/test_filtersets.py | 4 +--- netbox/dcim/tests/test_views.py | 4 +--- netbox/extras/api/serializers_/journaling.py | 4 ++-- netbox/extras/events.py | 5 ++--- netbox/extras/filtersets.py | 13 ++++++------- netbox/extras/forms/filtersets.py | 7 +++---- netbox/extras/management/commands/runscript.py | 5 +---- netbox/extras/models/dashboard.py | 3 +-- netbox/extras/models/staging.py | 3 +-- netbox/netbox/authentication/__init__.py | 13 +++++-------- netbox/netbox/tests/test_authentication.py | 6 +----- netbox/users/api/nested_serializers.py | 7 +++---- netbox/users/api/serializers_/users.py | 6 +++--- netbox/users/api/views.py | 5 ++--- netbox/users/filtersets.py | 15 +++++++-------- netbox/users/forms/filtersets.py | 5 ++--- netbox/users/forms/model_forms.py | 8 ++++---- netbox/users/graphql/filters.py | 6 ++---- netbox/users/graphql/schema.py | 3 +-- netbox/users/graphql/types.py | 5 ++--- netbox/users/models/preferences.py | 3 +-- netbox/users/models/tokens.py | 3 +-- netbox/users/tests/test_api.py | 6 +----- netbox/users/tests/test_filtersets.py | 5 +---- netbox/users/tests/test_models.py | 4 +--- netbox/users/tests/test_preferences.py | 5 +---- netbox/utilities/testing/api.py | 6 +----- netbox/utilities/testing/base.py | 5 ++--- netbox/utilities/testing/utils.py | 4 ++-- 37 files changed, 79 insertions(+), 132 deletions(-) diff --git a/netbox/core/filtersets.py b/netbox/core/filtersets.py index f622e789c..21fdaa4ab 100644 --- a/netbox/core/filtersets.py +++ b/netbox/core/filtersets.py @@ -1,4 +1,3 @@ -from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.utils.translation import gettext as _ @@ -7,6 +6,7 @@ import django_filters from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet from netbox.utils import get_data_backend_choices +from users.models import User from utilities.filters import ContentTypeFilter from .choices import * from .models import * @@ -141,12 +141,12 @@ class ObjectChangeFilterSet(BaseFilterSet): queryset=ContentType.objects.all() ) user_id = django_filters.ModelMultipleChoiceFilter( - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), label=_('User (ID)'), ) user = django_filters.ModelMultipleChoiceFilter( field_name='user__username', - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), to_field_name='username', label=_('User name'), ) diff --git a/netbox/core/forms/filtersets.py b/netbox/core/forms/filtersets.py index c629841ae..ab4b869b7 100644 --- a/netbox/core/forms/filtersets.py +++ b/netbox/core/forms/filtersets.py @@ -1,5 +1,4 @@ from django import forms -from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ from core.choices import * @@ -7,6 +6,7 @@ from core.models import * from netbox.forms import NetBoxModelFilterSetForm from netbox.forms.mixins import SavedFiltersMixin from netbox.utils import get_data_backend_choices +from users.models import User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice from utilities.forms.fields import ( ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, @@ -121,7 +121,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm): widget=DateTimePicker() ) user = DynamicModelMultipleChoiceField( - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), required=False, label=_('User') ) @@ -150,7 +150,7 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm): required=False ) user_id = DynamicModelMultipleChoiceField( - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), required=False, label=_('User') ) diff --git a/netbox/core/management/commands/nbshell.py b/netbox/core/management/commands/nbshell.py index 7270c005a..e26f956c8 100644 --- a/netbox/core/management/commands/nbshell.py +++ b/netbox/core/management/commands/nbshell.py @@ -5,10 +5,10 @@ import sys from django import get_version from django.apps import apps from django.conf import settings -from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand from core.models import ObjectType +from users.models import User APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless') @@ -61,7 +61,7 @@ class Command(BaseCommand): # Additional objects to include namespace['ObjectType'] = ObjectType - namespace['User'] = get_user_model() + namespace['User'] = User # Load convenience commands namespace.update({ diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 5e404179a..988c7fc76 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1,5 +1,4 @@ import django_filters -from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ from drf_spectacular.types import OpenApiTypes @@ -16,6 +15,7 @@ from netbox.filtersets import ( ) from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet from tenancy.models import * +from users.models import User from utilities.filters import ( ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter, @@ -497,12 +497,12 @@ class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet): label=_('Location (slug)'), ) user_id = django_filters.ModelMultipleChoiceFilter( - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), label=_('User (ID)'), ) user = django_filters.ModelMultipleChoiceFilter( field_name='user__username', - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), to_field_name='username', label=_('User (name)'), ) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index bc0f0bd71..c739c8384 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -1,6 +1,5 @@ from django import forms from django.conf import settings -from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ from timezone_field import TimeZoneFormField @@ -11,6 +10,7 @@ from extras.models import ConfigTemplate from ipam.models import ASN, VLAN, VLANGroup, VRF from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant +from users.models import User from utilities.forms import BulkEditForm, add_blank_choice, form_from_model from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms.rendering import FieldSet, InlineFields @@ -459,9 +459,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm): class RackReservationBulkEditForm(NetBoxModelBulkEditForm): user = forms.ModelChoiceField( label=_('User'), - queryset=get_user_model().objects.order_by( - 'username' - ), + queryset=User.objects.order_by('username'), required=False ) tenant = DynamicModelChoiceField( diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index b43ed85ce..a8531a9e9 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -1,5 +1,4 @@ from django import forms -from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ from dcim.choices import * @@ -10,6 +9,7 @@ from extras.models import ConfigTemplate from ipam.models import ASN, VRF from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm +from users.models import User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField from utilities.forms.rendering import FieldSet @@ -448,7 +448,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): label=_('Rack') ) user_id = DynamicModelMultipleChoiceField( - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), required=False, label=_('User') ) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 30c1cf3b9..7f7252c6e 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -1,5 +1,4 @@ from django import forms -from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext_lazy as _ from timezone_field import TimeZoneFormField @@ -11,6 +10,7 @@ from extras.models import ConfigTemplate from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm +from users.models import User from utilities.forms import add_blank_choice, get_field_value from utilities.forms.fields import ( CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, @@ -311,9 +311,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm): ) user = forms.ModelChoiceField( label=_('User'), - queryset=get_user_model().objects.order_by( - 'username' - ) + queryset=User.objects.order_by('username') ) comments = CommentField() diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 8d14c077f..f41ad44ff 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1,4 +1,3 @@ -from django.contrib.auth import get_user_model from django.test import override_settings from django.urls import reverse from django.utils.translation import gettext as _ @@ -11,15 +10,13 @@ from extras.models import ConfigTemplate from ipam.models import ASN, RIR, VLAN, VRF from netbox.api.serializers import GenericObjectSerializer from tenancy.models import Tenant +from users.models import User from utilities.testing import APITestCase, APIViewTestCases, create_test_device from virtualization.models import Cluster, ClusterType from wireless.choices import WirelessChannelChoices from wireless.models import WirelessLAN -User = get_user_model() - - class AppTest(APITestCase): def test_root(self): diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index ed2db958f..e76813874 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -1,4 +1,3 @@ -from django.contrib.auth import get_user_model from django.test import TestCase from circuits.models import Circuit, CircuitTermination, CircuitType, Provider @@ -8,12 +7,11 @@ from dcim.models import * from ipam.models import ASN, IPAddress, RIR, VRF from netbox.choices import ColorChoices from tenancy.models import Tenant, TenantGroup +from users.models import User from utilities.testing import ChangeLoggedFilterSetTests, create_test_device from virtualization.models import Cluster, ClusterType, ClusterGroup from wireless.choices import WirelessChannelChoices, WirelessRoleChoices -User = get_user_model() - class DeviceComponentFilterSetTests: diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 04281b9fd..e9002665b 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -2,7 +2,6 @@ from decimal import Decimal from zoneinfo import ZoneInfo import yaml -from django.contrib.auth import get_user_model from django.test import override_settings from django.urls import reverse from netaddr import EUI @@ -13,11 +12,10 @@ from dcim.models import * from ipam.models import ASN, RIR, VLAN, VRF from netbox.choices import CSVDelimiterChoices, ImportFormatChoices from tenancy.models import Tenant +from users.models import User from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data from wireless.models import WirelessLAN -User = get_user_model() - class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase): model = Region diff --git a/netbox/extras/api/serializers_/journaling.py b/netbox/extras/api/serializers_/journaling.py index 4afd3e70a..cba56fc32 100644 --- a/netbox/extras/api/serializers_/journaling.py +++ b/netbox/extras/api/serializers_/journaling.py @@ -1,4 +1,3 @@ -from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist from drf_spectacular.utils import extend_schema_field from rest_framework import serializers @@ -8,6 +7,7 @@ from extras.choices import * from extras.models import JournalEntry from netbox.api.fields import ChoiceField, ContentTypeField from netbox.api.serializers import NetBoxModelSerializer +from users.models import User from utilities.api import get_serializer_for_model __all__ = ( @@ -22,7 +22,7 @@ class JournalEntrySerializer(NetBoxModelSerializer): assigned_object = serializers.SerializerMethodField(read_only=True) created_by = serializers.PrimaryKeyRelatedField( allow_null=True, - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), required=False, default=serializers.CurrentUserDefault() ) diff --git a/netbox/extras/events.py b/netbox/extras/events.py index e80f4a558..ad603d11a 100644 --- a/netbox/extras/events.py +++ b/netbox/extras/events.py @@ -1,9 +1,7 @@ -from collections import defaultdict import logging from collections import defaultdict from django.conf import settings -from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.utils import timezone from django.utils.module_loading import import_string @@ -14,6 +12,7 @@ from core.events import * from netbox.config import get_config from netbox.constants import RQ_QUEUE_DEFAULT from netbox.registry import registry +from users.models import User from utilities.api import get_serializer_for_model from utilities.rqworker import get_rq_retry from utilities.serialization import serialize_object @@ -83,7 +82,7 @@ def enqueue_event(queue, instance, user, request_id, event_type): def process_event_rules(event_rules, object_type, event_type, data, username=None, snapshots=None, request_id=None): - user = get_user_model().objects.get(username=username) if username else None + user = User.objects.get(username=username) if username else None for event_rule in event_rules: diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 3b3de0bfa..38e7dfc9d 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -1,5 +1,4 @@ import django_filters -from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.utils.translation import gettext as _ @@ -284,12 +283,12 @@ class SavedFilterFilterSet(ChangeLoggedModelFilterSet): field_name='object_types' ) user_id = django_filters.ModelMultipleChoiceFilter( - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), label=_('User (ID)'), ) user = django_filters.ModelMultipleChoiceFilter( field_name='user__username', - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), to_field_name='username', label=_('User (name)'), ) @@ -328,12 +327,12 @@ class BookmarkFilterSet(BaseFilterSet): object_type_id = MultiValueNumberFilter() object_type = ContentTypeFilter() user_id = django_filters.ModelMultipleChoiceFilter( - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), label=_('User (ID)'), ) user = django_filters.ModelMultipleChoiceFilter( field_name='user__username', - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), to_field_name='username', label=_('User (name)'), ) @@ -410,12 +409,12 @@ class JournalEntryFilterSet(NetBoxModelFilterSet): queryset=ContentType.objects.all() ) created_by_id = django_filters.ModelMultipleChoiceFilter( - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), label=_('User (ID)'), ) created_by = django_filters.ModelMultipleChoiceFilter( field_name='created_by__username', - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), to_field_name='username', label=_('User (name)'), ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 18f777ea8..bd3883877 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -1,5 +1,4 @@ from django import forms -from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ from core.models import ObjectType, DataFile, DataSource @@ -10,7 +9,7 @@ from netbox.events import get_event_type_choices from netbox.forms.base import NetBoxModelFilterSetForm from netbox.forms.mixins import SavedFiltersMixin from tenancy.models import Tenant, TenantGroup -from users.models import Group +from users.models import Group, User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice from utilities.forms.fields import ( ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField, @@ -453,7 +452,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm): widget=DateTimePicker() ) created_by_id = DynamicModelMultipleChoiceField( - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), required=False, label=_('User') ) @@ -472,7 +471,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm): class NotificationGroupFilterForm(SavedFiltersMixin, FilterForm): user_id = DynamicModelMultipleChoiceField( - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), required=False, label=_('User') ) diff --git a/netbox/extras/management/commands/runscript.py b/netbox/extras/management/commands/runscript.py index b6d6810ac..ab0d6d894 100644 --- a/netbox/extras/management/commands/runscript.py +++ b/netbox/extras/management/commands/runscript.py @@ -3,12 +3,11 @@ import logging import sys import uuid -from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError -from django.utils.module_loading import import_string from extras.jobs import ScriptJob from extras.scripts import get_module_and_script +from users.models import User from utilities.request import NetBoxFakeRequest @@ -28,8 +27,6 @@ class Command(BaseCommand): parser.add_argument('script', help="Script to run") def handle(self, *args, **options): - User = get_user_model() - # Params script = options['script'] loglevel = options['loglevel'] diff --git a/netbox/extras/models/dashboard.py b/netbox/extras/models/dashboard.py index 7b9293777..669785cdd 100644 --- a/netbox/extras/models/dashboard.py +++ b/netbox/extras/models/dashboard.py @@ -1,4 +1,3 @@ -from django.contrib.auth import get_user_model from django.db import models from django.utils.translation import gettext_lazy as _ @@ -11,7 +10,7 @@ __all__ = ( class Dashboard(models.Model): user = models.OneToOneField( - to=get_user_model(), + to='users.User', on_delete=models.CASCADE, related_name='dashboard' ) diff --git a/netbox/extras/models/staging.py b/netbox/extras/models/staging.py index 7ffbde089..b944a6fb9 100644 --- a/netbox/extras/models/staging.py +++ b/netbox/extras/models/staging.py @@ -1,6 +1,5 @@ import logging -from django.contrib.auth import get_user_model from django.contrib.contenttypes.fields import GenericForeignKey from django.db import models, transaction from django.utils.translation import gettext_lazy as _ @@ -34,7 +33,7 @@ class Branch(ChangeLoggedModel): blank=True ) user = models.ForeignKey( - to=get_user_model(), + to='users.User', on_delete=models.SET_NULL, blank=True, null=True diff --git a/netbox/netbox/authentication/__init__.py b/netbox/netbox/authentication/__init__.py index 8c4bde436..7394f6ded 100644 --- a/netbox/netbox/authentication/__init__.py +++ b/netbox/netbox/authentication/__init__.py @@ -2,7 +2,6 @@ import logging from collections import defaultdict from django.conf import settings -from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as _RemoteUserBackend from django.contrib.auth.models import AnonymousUser from django.core.exceptions import ImproperlyConfigured @@ -10,14 +9,12 @@ from django.db.models import Q from django.utils.translation import gettext_lazy as _ from users.constants import CONSTRAINT_TOKEN_USER -from users.models import Group, ObjectPermission +from users.models import Group, ObjectPermission, User from utilities.permissions import ( permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_type, ) from .misc import _mirror_groups -UserModel = get_user_model() - AUTH_BACKEND_ATTRS = { # backend name: title, MDI icon name 'amazon': ('Amazon AWS', 'aws'), @@ -218,15 +215,15 @@ class RemoteUserBackend(_RemoteUserBackend): # instead we use get_or_create when creating unknown users since it has # built-in safeguards for multiple threads. if self.create_unknown_user: - user, created = UserModel._default_manager.get_or_create(**{ - UserModel.USERNAME_FIELD: username + user, created = User._default_manager.get_or_create(**{ + User.USERNAME_FIELD: username }) if created: user = self.configure_user(request, user) else: try: - user = UserModel._default_manager.get_by_natural_key(username) - except UserModel.DoesNotExist: + user = User._default_manager.get_by_natural_key(username) + except User.DoesNotExist: pass if self.user_can_authenticate(user): if settings.REMOTE_AUTH_GROUP_SYNC_ENABLED: diff --git a/netbox/netbox/tests/test_authentication.py b/netbox/netbox/tests/test_authentication.py index 6e049dcaf..5c7a30dc7 100644 --- a/netbox/netbox/tests/test_authentication.py +++ b/netbox/netbox/tests/test_authentication.py @@ -1,7 +1,6 @@ import datetime from django.conf import settings -from django.contrib.auth import get_user_model from django.test import Client from django.test.utils import override_settings from django.urls import reverse @@ -11,14 +10,11 @@ from rest_framework.test import APIClient from core.models import ObjectType from dcim.models import Site from ipam.models import Prefix -from users.models import Group, ObjectPermission, Token +from users.models import Group, ObjectPermission, Token, User from utilities.testing import TestCase from utilities.testing.api import APITestCase -User = get_user_model() - - class TokenAuthenticationTestCase(APITestCase): @override_settings(LOGIN_REQUIRED=True, EXEMPT_VIEW_PERMISSIONS=['*']) diff --git a/netbox/users/api/nested_serializers.py b/netbox/users/api/nested_serializers.py index 1d14a4169..b14cbcdb6 100644 --- a/netbox/users/api/nested_serializers.py +++ b/netbox/users/api/nested_serializers.py @@ -1,12 +1,11 @@ -from django.contrib.auth import get_user_model -from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from core.models import ObjectType from netbox.api.fields import ContentTypeField from netbox.api.serializers import WritableNestedSerializer -from users.models import Group, ObjectPermission, Token +from users.models import Group, ObjectPermission, Token, User __all__ = [ 'NestedGroupSerializer', @@ -26,7 +25,7 @@ class NestedGroupSerializer(WritableNestedSerializer): class NestedUserSerializer(WritableNestedSerializer): class Meta: - model = get_user_model() + model = User fields = ['id', 'url', 'display_url', 'display', 'username'] @extend_schema_field(OpenApiTypes.STR) diff --git a/netbox/users/api/serializers_/users.py b/netbox/users/api/serializers_/users.py index d3f1017b7..7b4dc5a07 100644 --- a/netbox/users/api/serializers_/users.py +++ b/netbox/users/api/serializers_/users.py @@ -1,11 +1,11 @@ -from django.contrib.auth import get_user_model, password_validation +from django.contrib.auth import password_validation from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from netbox.api.fields import SerializedPKRelatedField from netbox.api.serializers import ValidatedModelSerializer -from users.models import Group, ObjectPermission +from users.models import Group, ObjectPermission, User from .permissions import ObjectPermissionSerializer __all__ = ( @@ -49,7 +49,7 @@ class UserSerializer(ValidatedModelSerializer): ) class Meta: - model = get_user_model() + model = User fields = ( 'id', 'url', 'display_url', 'display', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'date_joined', 'last_login', 'groups', 'permissions', diff --git a/netbox/users/api/views.py b/netbox/users/api/views.py index f46cd24dc..240f68d36 100644 --- a/netbox/users/api/views.py +++ b/netbox/users/api/views.py @@ -1,6 +1,5 @@ import logging -from django.contrib.auth import get_user_model from django.db.models import Count from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema @@ -13,7 +12,7 @@ from rest_framework.viewsets import ViewSet from netbox.api.viewsets import NetBoxModelViewSet from users import filtersets -from users.models import Group, ObjectPermission, Token, UserConfig +from users.models import Group, ObjectPermission, Token, User, UserConfig from utilities.data import deepmerge from utilities.querysets import RestrictedQuerySet from . import serializers @@ -32,7 +31,7 @@ class UsersRootView(APIRootView): # class UserViewSet(NetBoxModelViewSet): - queryset = RestrictedQuerySet(model=get_user_model()).order_by('username') + queryset = RestrictedQuerySet(model=User).order_by('username') serializer_class = serializers.UserSerializer filterset_class = filtersets.UserFilterSet diff --git a/netbox/users/filtersets.py b/netbox/users/filtersets.py index 96121b7c4..af769e437 100644 --- a/netbox/users/filtersets.py +++ b/netbox/users/filtersets.py @@ -1,13 +1,12 @@ import django_filters -from django.contrib.auth import get_user_model from django.db.models import Q from django.utils.translation import gettext as _ from core.models import ObjectType from extras.models import NotificationGroup from netbox.filtersets import BaseFilterSet -from users.models import Group, ObjectPermission, Token +from users.models import Group, ObjectPermission, Token, User from utilities.filters import ContentTypeFilter __all__ = ( @@ -25,7 +24,7 @@ class GroupFilterSet(BaseFilterSet): ) user_id = django_filters.ModelMultipleChoiceFilter( field_name='user', - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), label=_('User (ID)'), ) permission_id = django_filters.ModelMultipleChoiceFilter( @@ -80,7 +79,7 @@ class UserFilterSet(BaseFilterSet): ) class Meta: - model = get_user_model() + model = User fields = ( 'id', 'username', 'first_name', 'last_name', 'email', 'date_joined', 'last_login', 'is_staff', 'is_active', 'is_superuser', @@ -104,12 +103,12 @@ class TokenFilterSet(BaseFilterSet): ) user_id = django_filters.ModelMultipleChoiceFilter( field_name='user', - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), label=_('User'), ) user = django_filters.ModelMultipleChoiceFilter( field_name='user__username', - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), to_field_name='username', label=_('User (name)'), ) @@ -171,12 +170,12 @@ class ObjectPermissionFilterSet(BaseFilterSet): ) user_id = django_filters.ModelMultipleChoiceFilter( field_name='users', - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), label=_('User'), ) user = django_filters.ModelMultipleChoiceFilter( field_name='users__username', - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), to_field_name='username', label=_('User (name)'), ) diff --git a/netbox/users/forms/filtersets.py b/netbox/users/forms/filtersets.py index 382a1821f..750b77ba4 100644 --- a/netbox/users/forms/filtersets.py +++ b/netbox/users/forms/filtersets.py @@ -1,5 +1,4 @@ from django import forms -from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ from netbox.forms import NetBoxModelFilterSetForm @@ -80,7 +79,7 @@ class ObjectPermissionFilterForm(NetBoxModelFilterSetForm): label=_('Group') ) user_id = DynamicModelMultipleChoiceField( - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), required=False, label=_('User') ) @@ -121,7 +120,7 @@ class TokenFilterForm(SavedFiltersMixin, FilterForm): FieldSet('user_id', 'write_enabled', 'expires', 'last_used', name=_('Token')), ) user_id = DynamicModelMultipleChoiceField( - queryset=get_user_model().objects.all(), + queryset=User.objects.all(), required=False, label=_('User') ) diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index 0c28621e1..639b9f726 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -1,6 +1,6 @@ from django import forms from django.conf import settings -from django.contrib.auth import get_user_model, password_validation +from django.contrib.auth import password_validation from django.contrib.postgres.forms import SimpleArrayField from django.core.exceptions import FieldError from django.utils.safestring import mark_safe @@ -152,7 +152,7 @@ class UserTokenForm(forms.ModelForm): class TokenForm(UserTokenForm): user = forms.ModelChoiceField( - queryset=get_user_model().objects.order_by('username'), + queryset=User.objects.order_by('username'), label=_('User') ) @@ -236,7 +236,7 @@ class GroupForm(forms.ModelForm): users = DynamicModelMultipleChoiceField( label=_('Users'), required=False, - queryset=get_user_model().objects.all() + queryset=User.objects.all() ) object_permissions = DynamicModelMultipleChoiceField( required=False, @@ -300,7 +300,7 @@ class ObjectPermissionForm(forms.ModelForm): users = DynamicModelMultipleChoiceField( label=_('Users'), required=False, - queryset=get_user_model().objects.all() + queryset=User.objects.all() ) groups = DynamicModelMultipleChoiceField( label=_('Groups'), diff --git a/netbox/users/graphql/filters.py b/netbox/users/graphql/filters.py index eb6c20203..d30781b1c 100644 --- a/netbox/users/graphql/filters.py +++ b/netbox/users/graphql/filters.py @@ -1,9 +1,7 @@ -import strawberry import strawberry_django -from django.contrib.auth import get_user_model -from users import filtersets, models from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin +from users import filtersets, models __all__ = ( 'GroupFilter', @@ -17,7 +15,7 @@ class GroupFilter(BaseFilterMixin): pass -@strawberry_django.filter(get_user_model(), lookups=True) +@strawberry_django.filter(models.User, lookups=True) @autotype_decorator(filtersets.UserFilterSet) class UserFilter(BaseFilterMixin): pass diff --git a/netbox/users/graphql/schema.py b/netbox/users/graphql/schema.py index 840887ad2..bfd6a7680 100644 --- a/netbox/users/graphql/schema.py +++ b/netbox/users/graphql/schema.py @@ -1,9 +1,8 @@ from typing import List + import strawberry import strawberry_django -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from users import models from .types import * diff --git a/netbox/users/graphql/types.py b/netbox/users/graphql/types.py index a638c558f..526bf6e21 100644 --- a/netbox/users/graphql/types.py +++ b/netbox/users/graphql/types.py @@ -1,10 +1,9 @@ from typing import List import strawberry_django -from django.contrib.auth import get_user_model from netbox.graphql.types import BaseObjectType -from users.models import Group +from users.models import Group, User from .filters import * __all__ = ( @@ -23,7 +22,7 @@ class GroupType(BaseObjectType): @strawberry_django.type( - get_user_model(), + User, fields=[ 'id', 'username', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'date_joined', 'groups', ], diff --git a/netbox/users/models/preferences.py b/netbox/users/models/preferences.py index ba4f914b8..75a5e99b5 100644 --- a/netbox/users/models/preferences.py +++ b/netbox/users/models/preferences.py @@ -1,4 +1,3 @@ -from django.contrib.auth import get_user_model from django.db import models from django.utils.translation import gettext_lazy as _ @@ -15,7 +14,7 @@ class UserConfig(models.Model): This model stores arbitrary user-specific preferences in a JSON data structure. """ user = models.OneToOneField( - to=get_user_model(), + to='users.User', on_delete=models.CASCADE, related_name='config' ) diff --git a/netbox/users/models/tokens.py b/netbox/users/models/tokens.py index 7c9453d77..2e7040699 100644 --- a/netbox/users/models/tokens.py +++ b/netbox/users/models/tokens.py @@ -2,7 +2,6 @@ import binascii import os from django.conf import settings -from django.contrib.auth import get_user_model from django.contrib.postgres.fields import ArrayField from django.core.validators import MinLengthValidator from django.db import models @@ -25,7 +24,7 @@ class Token(models.Model): It also supports setting an expiration time and toggling write ability. """ user = models.ForeignKey( - to=get_user_model(), + to='users.User', on_delete=models.CASCADE, related_name='tokens' ) diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index bf1d93a8f..f37eaf764 100644 --- a/netbox/users/tests/test_api.py +++ b/netbox/users/tests/test_api.py @@ -1,16 +1,12 @@ -from django.contrib.auth import get_user_model from django.test import override_settings from django.urls import reverse from core.models import ObjectType -from users.models import Group, ObjectPermission, Token +from users.models import Group, ObjectPermission, Token, User from utilities.data import deepmerge from utilities.testing import APIViewTestCases, APITestCase, create_test_user -User = get_user_model() - - class AppTest(APITestCase): def test_root(self): diff --git a/netbox/users/tests/test_filtersets.py b/netbox/users/tests/test_filtersets.py index 2cef6954a..fdf25d970 100644 --- a/netbox/users/tests/test_filtersets.py +++ b/netbox/users/tests/test_filtersets.py @@ -1,16 +1,13 @@ import datetime -from django.contrib.auth import get_user_model from django.test import TestCase from django.utils.timezone import make_aware from core.models import ObjectType from users import filtersets -from users.models import Group, ObjectPermission, Token +from users.models import Group, ObjectPermission, Token, User from utilities.testing import BaseFilterSetTests -User = get_user_model() - class UserTestCase(TestCase, BaseFilterSetTests): queryset = User.objects.all() diff --git a/netbox/users/tests/test_models.py b/netbox/users/tests/test_models.py index 791ea8fb4..7bb6f31af 100644 --- a/netbox/users/tests/test_models.py +++ b/netbox/users/tests/test_models.py @@ -1,8 +1,6 @@ -from django.contrib.auth import get_user_model from django.test import TestCase - -User = get_user_model() +from users.models import User class UserConfigTest(TestCase): diff --git a/netbox/users/tests/test_preferences.py b/netbox/users/tests/test_preferences.py index 203a67bdd..b5037ec3f 100644 --- a/netbox/users/tests/test_preferences.py +++ b/netbox/users/tests/test_preferences.py @@ -1,10 +1,10 @@ -from django.contrib.auth import get_user_model from django.test import override_settings from django.test.client import RequestFactory from django.urls import reverse from dcim.models import Site from dcim.tables import SiteTable +from users.models import User from users.preferences import UserPreference from utilities.testing import TestCase @@ -16,9 +16,6 @@ DEFAULT_USER_PREFERENCES = { } -User = get_user_model() - - class UserPreferencesTest(TestCase): user_permissions = ['dcim.view_site'] diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index d6963cb59..6484499b9 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -3,7 +3,6 @@ import json import strawberry_django from django.conf import settings -from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.test import override_settings from django.urls import reverse @@ -16,7 +15,7 @@ from strawberry.types.union import StrawberryUnion from core.choices import ObjectChangeActionChoices from core.models import ObjectChange, ObjectType from ipam.graphql.types import IPAddressFamilyType -from users.models import ObjectPermission, Token +from users.models import ObjectPermission, Token, User from utilities.api import get_graphql_type_for_model from .base import ModelTestCase from .utils import disable_warnings @@ -27,9 +26,6 @@ __all__ = ( ) -User = get_user_model() - - # # REST/GraphQL API Tests # diff --git a/netbox/utilities/testing/base.py b/netbox/utilities/testing/base.py index cb69457a2..e197a3f6a 100644 --- a/netbox/utilities/testing/base.py +++ b/netbox/utilities/testing/base.py @@ -1,6 +1,5 @@ import json -from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import ArrayField, RangeField from django.core.exceptions import FieldDoesNotExist @@ -11,7 +10,7 @@ from netaddr import IPNetwork from taggit.managers import TaggableManager from core.models import ObjectType -from users.models import ObjectPermission +from users.models import ObjectPermission, User from utilities.data import ranges_to_string from utilities.object_types import object_type_identifier from utilities.permissions import resolve_permission_type @@ -29,7 +28,7 @@ class TestCase(_TestCase): def setUp(self): # Create the test user and assign permissions - self.user = get_user_model().objects.create_user(username='testuser') + self.user = User.objects.create_user(username='testuser') self.add_permissions(*self.user_permissions) # Initialize the test client diff --git a/netbox/utilities/testing/utils.py b/netbox/utilities/testing/utils.py index 59bce2b7c..adcb8c8e5 100644 --- a/netbox/utilities/testing/utils.py +++ b/netbox/utilities/testing/utils.py @@ -3,7 +3,6 @@ import logging import re from contextlib import contextmanager -from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission from django.utils.text import slugify @@ -11,6 +10,7 @@ from core.models import ObjectType from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site from extras.choices import CustomFieldTypeChoices from extras.models import CustomField, Tag +from users.models import User from virtualization.models import Cluster, ClusterType, VirtualMachine @@ -67,7 +67,7 @@ def create_test_user(username='testuser', permissions=None): """ Create a User with the given permissions. """ - user = get_user_model().objects.create_user(username=username) + user = User.objects.create_user(username=username) if permissions is None: permissions = () for perm_name in permissions: From 408f8b49646264508bb25b0ee5982e4aa378d442 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Thu, 1 Aug 2024 11:49:46 +0700 Subject: [PATCH 15/23] 17054 upgrade sass to upgrade braces --- netbox/project-static/yarn.lock | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/netbox/project-static/yarn.lock b/netbox/project-static/yarn.lock index 4f9111c89..bb0bea154 100644 --- a/netbox/project-static/yarn.lock +++ b/netbox/project-static/yarn.lock @@ -867,13 +867,20 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.2, braces@~3.0.2: +braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== dependencies: fill-range "^7.0.1" +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -1520,10 +1527,10 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.0.1, fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -1816,9 +1823,9 @@ ignore@^5.2.0: integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== immutable@^4.0.0: - version "4.3.6" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447" - integrity sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ== + version "4.3.7" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" + integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== import-fresh@^3.2.1: version "3.3.0" From dc173a55088503d03b2449bf0313be7cb4b79a02 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Wed, 31 Jul 2024 15:39:52 +0700 Subject: [PATCH 16/23] 17038 fix export system data --- netbox/core/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/core/views.py b/netbox/core/views.py index 9697a7352..466c95afa 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -559,13 +559,14 @@ class SystemView(UserPassesTestMixin, View): # Raw data export if 'export' in request.GET: + params = [param.name for param in PARAMS] data = { **stats, 'plugins': { plugin.name: plugin.version for plugin in plugins }, 'config': { - k: config.data[k] for k in sorted(config.data) + k: getattr(config, k) for k in sorted(params) }, } response = HttpResponse(json.dumps(data, indent=4), content_type='text/json') From d6f2fc7d2921c7071f8cf58649e74297b9b258fe Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Thu, 1 Aug 2024 20:06:51 +0700 Subject: [PATCH 17/23] 17058 RackType name -> model (#17059) * 17058 RackType name -> model * 17058 RackType name -> model * 17058 fix tests * 17058 fix tests --- docs/models/dcim/racktype.md | 8 ++++++-- netbox/dcim/api/serializers_/racks.py | 4 ++-- netbox/dcim/filtersets.py | 4 ++-- netbox/dcim/forms/bulk_import.py | 2 +- netbox/dcim/forms/model_forms.py | 9 ++++++--- netbox/dcim/graphql/types.py | 1 - netbox/dcim/migrations/0188_racktype.py | 23 ++++++++++++++--------- netbox/dcim/models/racks.py | 25 +++++++++++++++---------- netbox/dcim/search.py | 2 +- netbox/dcim/tables/racks.py | 9 ++++----- netbox/dcim/tests/test_api.py | 14 +++++++------- netbox/dcim/tests/test_filtersets.py | 14 +++++++------- netbox/dcim/tests/test_models.py | 2 +- netbox/dcim/tests/test_views.py | 12 ++++++------ netbox/templates/dcim/racktype.html | 4 ++-- 15 files changed, 74 insertions(+), 59 deletions(-) diff --git a/docs/models/dcim/racktype.md b/docs/models/dcim/racktype.md index 73fb08bcd..04dd63eee 100644 --- a/docs/models/dcim/racktype.md +++ b/docs/models/dcim/racktype.md @@ -10,9 +10,13 @@ A rack type defines the physical characteristics of a particular model of [rack] The [manufacturer](./manufacturer.md) which produces this type of rack. -### Name +### Model -The unique name of the rack type. +The model number assigned to this rack type by its manufacturer. Must be unique to the manufacturer. + +### Slug + +A unique URL-friendly representation of the model identifier. (This value can be used for filtering.) ### Form Factor diff --git a/netbox/dcim/api/serializers_/racks.py b/netbox/dcim/api/serializers_/racks.py index 17c1c174e..4a0fe5daa 100644 --- a/netbox/dcim/api/serializers_/racks.py +++ b/netbox/dcim/api/serializers_/racks.py @@ -73,12 +73,12 @@ class RackTypeSerializer(RackBaseSerializer): class Meta: model = RackType fields = [ - 'id', 'url', 'display_url', 'display', 'manufacturer', 'name', 'slug', 'description', 'form_factor', + 'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'slug', 'description', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'weight', 'max_weight', 'weight_unit', 'mounting_depth', 'airflow', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] - brief_fields = ('id', 'url', 'display', 'manufacturer', 'name', 'slug', 'description') + brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description') class RackSerializer(RackBaseSerializer): diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 988c7fc76..255b9bd50 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -311,7 +311,7 @@ class RackTypeFilterSet(NetBoxModelFilterSet): class Meta: model = RackType fields = ( - 'id', 'name', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', + 'id', 'model', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description', ) @@ -319,7 +319,7 @@ class RackTypeFilterSet(NetBoxModelFilterSet): if not value.strip(): return queryset return queryset.filter( - Q(name__icontains=value) | + Q(model__icontains=value) | Q(description__icontains=value) | Q(comments__icontains=value) ) diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 785b4fb42..2e537d978 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -222,7 +222,7 @@ class RackTypeImportForm(NetBoxModelImportForm): class Meta: model = RackType fields = ( - 'manufacturer', 'name', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', + 'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags', ) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 7f7252c6e..4a9e2f25b 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -208,10 +208,13 @@ class RackTypeForm(NetBoxModelForm): queryset=Manufacturer.objects.all() ) comments = CommentField() - slug = SlugField() + slug = SlugField( + label=_('Slug'), + slug_source='model' + ) fieldsets = ( - FieldSet('manufacturer', 'name', 'slug', 'description', 'form_factor', 'airflow', 'tags', name=_('Rack Type')), + FieldSet('manufacturer', 'model', 'slug', 'description', 'form_factor', 'airflow', 'tags', name=_('Rack Type')), FieldSet( 'width', 'u_height', InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')), @@ -224,7 +227,7 @@ class RackTypeForm(NetBoxModelForm): class Meta: model = RackType fields = [ - 'manufacturer', 'name', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', + 'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'airflow', 'description', 'comments', 'tags', ] diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index d2bf4b416..ac2549616 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -613,7 +613,6 @@ class PowerPortTemplateType(ModularComponentTemplateType): filters=RackTypeFilter ) class RackTypeType(NetBoxObjectType): - _name: str manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] diff --git a/netbox/dcim/migrations/0188_racktype.py b/netbox/dcim/migrations/0188_racktype.py index 18e4152b6..b23169093 100644 --- a/netbox/dcim/migrations/0188_racktype.py +++ b/netbox/dcim/migrations/0188_racktype.py @@ -37,14 +37,7 @@ class Migration(migrations.Migration): related_name='rack_types', to='dcim.manufacturer' )), - ('name', models.CharField(max_length=100)), - ('_name', utilities.fields.NaturalOrderingField( - 'name', - blank=True, - max_length=100, - naturalize_function=utilities.ordering.naturalize - ), - ), + ('model', models.CharField(max_length=100)), ('slug', models.SlugField(max_length=100, unique=True)), ('form_factor', models.CharField(blank=True, max_length=50)), ('width', models.PositiveSmallIntegerField(default=19)), @@ -71,7 +64,7 @@ class Migration(migrations.Migration): options={ 'verbose_name': 'racktype', 'verbose_name_plural': 'racktypes', - 'ordering': ('_name', 'pk'), + 'ordering': ('manufacturer', 'model'), }, ), migrations.RenameField( @@ -90,4 +83,16 @@ class Migration(migrations.Migration): to='dcim.racktype', ), ), + migrations.AddConstraint( + model_name='racktype', + constraint=models.UniqueConstraint( + fields=('manufacturer', 'model'), name='dcim_racktype_unique_manufacturer_model' + ), + ), + migrations.AddConstraint( + model_name='racktype', + constraint=models.UniqueConstraint( + fields=('manufacturer', 'slug'), name='dcim_racktype_unique_manufacturer_slug' + ), + ), ] diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 8457271f4..68088b63a 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -136,15 +136,10 @@ class RackType(RackBase): on_delete=models.PROTECT, related_name='rack_types' ) - name = models.CharField( - verbose_name=_('name'), + model = models.CharField( + verbose_name=_('model'), max_length=100 ) - _name = NaturalOrderingField( - target_field='name', - max_length=100, - blank=True - ) slug = models.SlugField( verbose_name=_('slug'), max_length=100, @@ -160,19 +155,29 @@ class RackType(RackBase): ) class Meta: - ordering = ('_name', 'pk') # (site, location, name) may be non-unique + ordering = ('manufacturer', 'model') + constraints = ( + models.UniqueConstraint( + fields=('manufacturer', 'model'), + name='%(app_label)s_%(class)s_unique_manufacturer_model' + ), + models.UniqueConstraint( + fields=('manufacturer', 'slug'), + name='%(app_label)s_%(class)s_unique_manufacturer_slug' + ), + ) verbose_name = _('rack type') verbose_name_plural = _('rack types') def __str__(self): - return self.name + return self.model def get_absolute_url(self): return reverse('dcim:racktype', args=[self.pk]) @property def full_name(self): - return f"{self.manufacturer} {self.name}" + return f"{self.manufacturer} {self.model}" def clean(self): super().clean() diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py index 32add68d0..38c1843fe 100644 --- a/netbox/dcim/search.py +++ b/netbox/dcim/search.py @@ -246,7 +246,7 @@ class PowerPortIndex(SearchIndex): class RackTypeIndex(SearchIndex): model = models.RackType fields = ( - ('name', 100), + ('model', 100), ('description', 500), ('comments', 5000), ) diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index b8295c286..a6b704161 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -50,9 +50,8 @@ class RackRoleTable(NetBoxTable): # class RackTypeTable(NetBoxTable): - name = tables.Column( - verbose_name=_('Name'), - order_by=('_name',), + model = tables.Column( + verbose_name=_('Model'), linkify=True ) manufacturer = tables.Column( @@ -96,12 +95,12 @@ class RackTypeTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = RackType fields = ( - 'pk', 'id', 'name', 'manufacturer', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width', + 'pk', 'id', 'model', 'manufacturer', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width', 'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'description', 'comments', 'instance_count', 'tags', 'created', 'last_updated', ) default_columns = ( - 'pk', 'name', 'manufacturer', 'type', 'u_height', 'description', 'instance_count', + 'pk', 'model', 'manufacturer', 'type', 'u_height', 'description', 'instance_count', ) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index f41ad44ff..b690007be 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -273,7 +273,7 @@ class RackRoleTest(APIViewTestCases.APIViewTestCase): class RackTypeTest(APIViewTestCases.APIViewTestCase): model = RackType - brief_fields = ['description', 'display', 'id', 'manufacturer', 'name', 'slug', 'url'] + brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'slug', 'url'] bulk_update_data = { 'description': 'new description', } @@ -287,26 +287,26 @@ class RackTypeTest(APIViewTestCases.APIViewTestCase): Manufacturer.objects.bulk_create(manufacturers) rack_types = ( - RackType(manufacturer=manufacturers[0], name='Rack Type 1', slug='rack-type-1'), - RackType(manufacturer=manufacturers[0], name='Rack Type 2', slug='rack-type-2'), - RackType(manufacturer=manufacturers[0], name='Rack Type 3', slug='rack-type-3'), + RackType(manufacturer=manufacturers[0], model='Rack Type 1', slug='rack-type-1'), + RackType(manufacturer=manufacturers[0], model='Rack Type 2', slug='rack-type-2'), + RackType(manufacturer=manufacturers[0], model='Rack Type 3', slug='rack-type-3'), ) RackType.objects.bulk_create(rack_types) cls.create_data = [ { 'manufacturer': manufacturers[1].pk, - 'name': 'Rack Type 4', + 'model': 'Rack Type 4', 'slug': 'rack-type-4', }, { 'manufacturer': manufacturers[1].pk, - 'name': 'Rack Type 5', + 'model': 'Rack Type 5', 'slug': 'rack-type-5', }, { 'manufacturer': manufacturers[1].pk, - 'name': 'Rack Type 6', + 'model': 'Rack Type 6', 'slug': 'rack-type-6', }, ] diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index e76813874..156815474 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -482,7 +482,7 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests): racks = ( RackType( manufacturer=manufacturers[0], - name='RackType 1', + model='RackType 1', slug='rack-type-1', form_factor=RackFormFactorChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, @@ -500,7 +500,7 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests): ), RackType( manufacturer=manufacturers[1], - name='RackType 2', + model='RackType 2', slug='rack-type-2', form_factor=RackFormFactorChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, @@ -518,7 +518,7 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests): ), RackType( manufacturer=manufacturers[2], - name='RackType 3', + model='RackType 3', slug='rack-type-3', form_factor=RackFormFactorChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, @@ -548,8 +548,8 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_name(self): - params = {'name': ['RackType 1', 'RackType 2']} + def test_model(self): + params = {'model': ['RackType 1', 'RackType 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_slug(self): @@ -661,7 +661,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests): rack_types = ( RackType( manufacturer=manufacturers[0], - name='RackType 1', + model='RackType 1', slug='rack-type-1', form_factor=RackFormFactorChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, @@ -679,7 +679,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests): ), RackType( manufacturer=manufacturers[1], - name='RackType 2', + model='RackType 2', slug='rack-type-2', form_factor=RackFormFactorChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 229edc0de..148b9e35f 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -82,7 +82,7 @@ class RackTypeTestCase(TestCase): RackType.objects.create( manufacturer=manufacturer, - name='RackType 1', + model='RackType 1', slug='rack-type-1', width=11, u_height=22, diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index e9002665b..c8763e5b9 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -346,9 +346,9 @@ class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): Manufacturer.objects.bulk_create(manufacturers) rack_types = ( - RackType(manufacturer=manufacturers[0], name='RackType 1', slug='rack-type-1',), - RackType(manufacturer=manufacturers[0], name='RackType 2', slug='rack-type-2',), - RackType(manufacturer=manufacturers[0], name='RackType 3', slug='rack-type-3',), + RackType(manufacturer=manufacturers[0], model='RackType 1', slug='rack-type-1',), + RackType(manufacturer=manufacturers[0], model='RackType 2', slug='rack-type-2',), + RackType(manufacturer=manufacturers[0], model='RackType 3', slug='rack-type-3',), ) RackType.objects.bulk_create(rack_types) @@ -356,7 +356,7 @@ class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): cls.form_data = { 'manufacturer': manufacturers[1].pk, - 'name': 'RackType X', + 'model': 'RackType X', 'slug': 'rack-type-x', 'type': RackFormFactorChoices.TYPE_CABINET, 'width': RackWidthChoices.WIDTH_19IN, @@ -374,14 +374,14 @@ class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "manufacturer,name,slug,width,u_height,weight,max_weight,weight_unit", + "manufacturer,model,slug,width,u_height,weight,max_weight,weight_unit", "Manufacturer 1,RackType 4,rack-type-4,19,42,100,2000,kg", "Manufacturer 1,RackType 5,rack-type-5,19,42,100,2000,kg", "Manufacturer 1,RackType 6,rack-type-6,19,42,100,2000,kg", ) cls.csv_update_data = ( - "id,name", + "id,model", f"{rack_types[0].pk},RackType 7", f"{rack_types[1].pk},RackType 8", f"{rack_types[2].pk},RackType 9", diff --git a/netbox/templates/dcim/racktype.html b/netbox/templates/dcim/racktype.html index c4e445146..8a1971af9 100644 --- a/netbox/templates/dcim/racktype.html +++ b/netbox/templates/dcim/racktype.html @@ -17,8 +17,8 @@ {{ object.manufacturer|linkify }} - {% trans "Name" %} - {{ object.name }} + {% trans "Model" %} + {{ object.model }} {% trans "Description" %} From fa2b3bcfcc174780cc647934d41ad9f84f6a7fb9 Mon Sep 17 00:00:00 2001 From: bubu Date: Thu, 1 Aug 2024 09:34:46 +0800 Subject: [PATCH 18/23] Adjust HTML template for Chinese translation order. --- netbox/templates/core/rq_worker_list.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/netbox/templates/core/rq_worker_list.html b/netbox/templates/core/rq_worker_list.html index d0d8b9149..92ea601ac 100644 --- a/netbox/templates/core/rq_worker_list.html +++ b/netbox/templates/core/rq_worker_list.html @@ -24,7 +24,12 @@
    {% endblock page-header %} -{% block title %}{{ status|capfirst }} {% trans "Workers in " %}{{ queue.name }}{% endblock %} +{% block title %} + {{ status|capfirst }} + {% blocktrans trimmed with queue_name=queue.name %} + Workers in {{ queue_name }} + {% endblocktrans %} +{% endblock %} {% block controls %}{% endblock %} From 0b77b3372c067fe1fd8c0fc5ab40ca3732e31288 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 1 Aug 2024 09:14:03 -0400 Subject: [PATCH 19/23] Rename DeviceType.get_full_name to full_name --- netbox/dcim/models/devices.py | 2 +- netbox/templates/dcim/device.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index c6e97a3cd..14c6ac8ab 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -221,7 +221,7 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): return reverse('dcim:devicetype', args=[self.pk]) @property - def get_full_name(self): + def full_name(self): return f"{self.manufacturer} {self.model}" def to_yaml(self): diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 80266c78d..b12dee568 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -87,7 +87,7 @@ {% trans "Device Type" %} - {{ object.device_type|linkify:"get_full_name" }} ({{ object.device_type.u_height|floatformat }}U) + {{ object.device_type|linkify:"full_name" }} ({{ object.device_type.u_height|floatformat }}U) From bf17485290b364a8a68fe16cfd004f03a2c26206 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 1 Aug 2024 09:36:23 -0400 Subject: [PATCH 20/23] Fix export of system data & include plugins --- netbox/core/views.py | 3 +++ netbox/utilities/release.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/netbox/core/views.py b/netbox/core/views.py index 616cbb60e..61f0dbd9b 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -2,6 +2,7 @@ import json import platform from django import __version__ as DJANGO_VERSION +from django.apps import apps from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import UserPassesTestMixin @@ -630,9 +631,11 @@ class SystemView(UserPassesTestMixin, View): # Raw data export if 'export' in request.GET: + stats['netbox_release'] = stats['netbox_release'].asdict() params = [param.name for param in PARAMS] data = { **stats, + 'plugins': settings.PLUGINS, 'config': { k: getattr(config, k) for k in sorted(params) }, diff --git a/netbox/utilities/release.py b/netbox/utilities/release.py index 5eb22807e..256588b0b 100644 --- a/netbox/utilities/release.py +++ b/netbox/utilities/release.py @@ -1,7 +1,7 @@ import datetime import os import yaml -from dataclasses import dataclass, field +from dataclasses import asdict, dataclass, field from typing import List, Union from django.core.exceptions import ImproperlyConfigured @@ -42,6 +42,9 @@ class ReleaseInfo: def name(self): return f"NetBox {self.edition} v{self.full_version}" + def asdict(self): + return asdict(self) + def load_release_data(): """ From e32cfdf88c11835bbfc083dbf28ea00ae8d292cf Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Thu, 1 Aug 2024 21:30:41 +0700 Subject: [PATCH 21/23] 17034 Add error handling to Plugins Catalog API (#17035) * 17034 add error handling to plugins catalog API * 17034 refactor from review feedback * 17034 refactor from review feedback * Misc cleanup --------- Co-authored-by: Jeremy Stretch --- netbox/core/plugins.py | 164 +++++++++++++++++++++-------------------- netbox/core/views.py | 29 ++++++-- 2 files changed, 104 insertions(+), 89 deletions(-) diff --git a/netbox/core/plugins.py b/netbox/core/plugins.py index 374cfbe9a..9fac88005 100644 --- a/netbox/core/plugins.py +++ b/netbox/core/plugins.py @@ -7,12 +7,12 @@ from typing import Optional import requests from django.conf import settings from django.core.cache import cache -from django.utils.translation import gettext_lazy as _ from netbox.plugins import PluginConfig from utilities.datetime import datetime_from_timestamp USER_AGENT_STRING = f'NetBox/{settings.RELEASE.version} {settings.RELEASE.edition}' +CACHE_KEY_CATALOG_FEED = 'plugins-catalog-feed' @dataclass @@ -68,16 +68,19 @@ class Plugin: installed_version: str = '' -def get_local_plugins(): +def get_local_plugins(plugins=None): """ Return a dictionary of all locally-installed plugins, mapped by name. """ - plugins = {} + plugins = plugins or {} + local_plugins = {} + + # Gather all locally-installed plugins for plugin_name in settings.PLUGINS: plugin = importlib.import_module(plugin_name) plugin_config: PluginConfig = plugin.config - plugins[plugin_config.name] = Plugin( + local_plugins[plugin_config.name] = Plugin( slug=plugin_config.name, title_short=plugin_config.verbose_name, tag_line=plugin_config.description, @@ -87,6 +90,14 @@ def get_local_plugins(): installed_version=plugin_config.version, ) + # Update catalog entries for local plugins, or add them to the list if not listed + for k, v in local_plugins.items(): + if k in plugins: + plugins[k].is_local = True + plugins[k].is_installed = True + else: + plugins[k] = v + return plugins @@ -95,7 +106,6 @@ def get_catalog_plugins(): Return a dictionary of all entries in the plugins catalog, mapped by name. """ session = requests.Session() - plugins = {} def get_pages(): # TODO: pagination is currently broken in API @@ -121,88 +131,80 @@ def get_catalog_plugins(): ).json() yield next_page - for page in get_pages(): - for data in page['data']: + def make_plugin_dict(): + plugins = {} - # Populate releases - releases = [] - for version in data['release_recent_history']: - releases.append( - PluginVersion( - date=datetime_from_timestamp(version['date']), - version=version['version'], - netbox_min_version=version['netbox_min_version'], - netbox_max_version=version['netbox_max_version'], - has_model=version['has_model'], - is_certified=version['is_certified'], - is_feature=version['is_feature'], - is_integration=version['is_integration'], - is_netboxlabs_supported=version['is_netboxlabs_supported'], + for page in get_pages(): + for data in page['data']: + + # Populate releases + releases = [] + for version in data['release_recent_history']: + releases.append( + PluginVersion( + date=datetime_from_timestamp(version['date']), + version=version['version'], + netbox_min_version=version['netbox_min_version'], + netbox_max_version=version['netbox_max_version'], + has_model=version['has_model'], + is_certified=version['is_certified'], + is_feature=version['is_feature'], + is_integration=version['is_integration'], + is_netboxlabs_supported=version['is_netboxlabs_supported'], + ) ) + releases = sorted(releases, key=lambda x: x.date, reverse=True) + latest_release = PluginVersion( + date=datetime_from_timestamp(data['release_latest']['date']), + version=data['release_latest']['version'], + netbox_min_version=data['release_latest']['netbox_min_version'], + netbox_max_version=data['release_latest']['netbox_max_version'], + has_model=data['release_latest']['has_model'], + is_certified=data['release_latest']['is_certified'], + is_feature=data['release_latest']['is_feature'], + is_integration=data['release_latest']['is_integration'], + is_netboxlabs_supported=data['release_latest']['is_netboxlabs_supported'], ) - releases = sorted(releases, key=lambda x: x.date, reverse=True) - latest_release = PluginVersion( - date=datetime_from_timestamp(data['release_latest']['date']), - version=data['release_latest']['version'], - netbox_min_version=data['release_latest']['netbox_min_version'], - netbox_max_version=data['release_latest']['netbox_max_version'], - has_model=data['release_latest']['has_model'], - is_certified=data['release_latest']['is_certified'], - is_feature=data['release_latest']['is_feature'], - is_integration=data['release_latest']['is_integration'], - is_netboxlabs_supported=data['release_latest']['is_netboxlabs_supported'], - ) - # Populate author (if any) - if data['author']: - author = PluginAuthor( - name=data['author']['name'], - org_id=data['author']['org_id'], - url=data['author']['url'], + # Populate author (if any) + if data['author']: + author = PluginAuthor( + name=data['author']['name'], + org_id=data['author']['org_id'], + url=data['author']['url'], + ) + else: + author = None + + # Populate plugin data + plugins[data['slug']] = Plugin( + id=data['id'], + status=data['status'], + title_short=data['title_short'], + title_long=data['title_long'], + tag_line=data['tag_line'], + description_short=data['description_short'], + slug=data['slug'], + author=author, + created_at=datetime_from_timestamp(data['created_at']), + updated_at=datetime_from_timestamp(data['updated_at']), + license_type=data['license_type'], + homepage_url=data['homepage_url'], + package_name_pypi=data['package_name_pypi'], + config_name=data['config_name'], + is_certified=data['is_certified'], + release_latest=latest_release, + release_recent_history=releases, ) - else: - author = None - # Populate plugin data - plugins[data['slug']] = Plugin( - id=data['id'], - status=data['status'], - title_short=data['title_short'], - title_long=data['title_long'], - tag_line=data['tag_line'], - description_short=data['description_short'], - slug=data['slug'], - author=author, - created_at=datetime_from_timestamp(data['created_at']), - updated_at=datetime_from_timestamp(data['updated_at']), - license_type=data['license_type'], - homepage_url=data['homepage_url'], - package_name_pypi=data['package_name_pypi'], - config_name=data['config_name'], - is_certified=data['is_certified'], - release_latest=latest_release, - release_recent_history=releases, - ) + return plugins - return plugins - - -def get_plugins(): - """ - Return a dictionary of all plugins (both catalog and locally installed), mapped by name. - """ - local_plugins = get_local_plugins() - catalog_plugins = cache.get('plugins-catalog-feed') + catalog_plugins = cache.get(CACHE_KEY_CATALOG_FEED, default={}) if not catalog_plugins: - catalog_plugins = get_catalog_plugins() - cache.set('plugins-catalog-feed', catalog_plugins, 3600) + try: + catalog_plugins = make_plugin_dict() + cache.set(CACHE_KEY_CATALOG_FEED, catalog_plugins, 3600) + except requests.exceptions.RequestException: + pass - plugins = catalog_plugins - for k, v in local_plugins.items(): - if k in plugins: - plugins[k].is_local = True - plugins[k].is_installed = True - else: - plugins[k] = v - - return plugins + return catalog_plugins diff --git a/netbox/core/views.py b/netbox/core/views.py index 61f0dbd9b..cb250b68e 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -38,7 +38,7 @@ from . import filtersets, forms, tables from .choices import DataSourceStatusChoices from .jobs import SyncDataSourceJob from .models import * -from .plugins import get_plugins +from .plugins import get_catalog_plugins, get_local_plugins from .tables import CatalogPluginTable, PluginVersionTable @@ -654,15 +654,31 @@ class SystemView(UserPassesTestMixin, View): # Plugins # -class PluginListView(UserPassesTestMixin, View): +class BasePluginView(UserPassesTestMixin, View): + CACHE_KEY_CATALOG_ERROR = 'plugins-catalog-error' def test_func(self): return self.request.user.is_staff + def get_cached_plugins(self, request): + catalog_plugins = {} + catalog_plugins_error = cache.get(self.CACHE_KEY_CATALOG_ERROR, default=False) + if not catalog_plugins_error: + catalog_plugins = get_catalog_plugins() + if not catalog_plugins: + # Cache for 5 minutes to avoid spamming connection + cache.set(self.CACHE_KEY_CATALOG_ERROR, True, 300) + messages.warning(request, _("Plugins catalog could not be loaded")) + + return get_local_plugins(catalog_plugins) + + +class PluginListView(BasePluginView): + def get(self, request): q = request.GET.get('q', None) - plugins = get_plugins().values() + plugins = self.get_cached_plugins(request).values() if q: plugins = [obj for obj in plugins if q.casefold() in obj.title_short.casefold()] @@ -680,14 +696,11 @@ class PluginListView(UserPassesTestMixin, View): }) -class PluginView(UserPassesTestMixin, View): - - def test_func(self): - return self.request.user.is_staff +class PluginView(BasePluginView): def get(self, request, name): - plugins = get_plugins() + plugins = self.get_cached_plugins(request) if name not in plugins: raise Http404(_("Plugin {name} not found").format(name=name)) plugin = plugins[name] From 8e6987edbff65864b718d74368c7470f126ee0a0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 1 Aug 2024 12:58:11 -0400 Subject: [PATCH 22/23] #16388: Move change logging signal handlers into core --- netbox/core/models/data.py | 3 +- netbox/core/signals.py | 167 +++++++++++++++++- netbox/extras/jobs.py | 2 +- netbox/extras/signals.py | 182 +------------------- netbox/extras/utils.py | 36 ++++ netbox/netbox/views/generic/bulk_views.py | 4 +- netbox/netbox/views/generic/object_views.py | 2 +- 7 files changed, 210 insertions(+), 186 deletions(-) diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py index a8e90ec3f..97f9fdac7 100644 --- a/netbox/core/models/data.py +++ b/netbox/core/models/data.py @@ -21,7 +21,6 @@ from netbox.registry import registry from utilities.querysets import RestrictedQuerySet from ..choices import * from ..exceptions import SyncError -from ..signals import post_sync, pre_sync __all__ = ( 'AutoSyncRecord', @@ -159,6 +158,8 @@ class DataSource(JobsMixin, PrimaryModel): """ Create/update/delete child DataFiles as necessary to synchronize with the remote source. """ + from core.signals import post_sync, pre_sync + if self.status == DataSourceStatusChoices.SYNCING: raise SyncError(_("Cannot initiate sync; syncing already in progress.")) diff --git a/netbox/core/signals.py b/netbox/core/signals.py index f884a27b4..06432bf4c 100644 --- a/netbox/core/signals.py +++ b/netbox/core/signals.py @@ -1,9 +1,26 @@ -from django.db.models.signals import post_save -from django.dispatch import Signal, receiver +import logging +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError +from django.db.models.fields.reverse_related import ManyToManyRel +from django.db.models.signals import m2m_changed, post_save, pre_delete +from django.dispatch import receiver, Signal +from django.utils.translation import gettext_lazy as _ +from django_prometheus.models import model_deletes, model_inserts, model_updates + +from core.choices import ObjectChangeActionChoices +from core.events import * +from core.models import ObjectChange +from extras.events import enqueue_event +from extras.utils import run_validators +from netbox.config import get_config +from netbox.context import current_request, events_queue +from netbox.models.features import ChangeLoggingMixin +from utilities.exceptions import AbortRequest from .models import ConfigRevision __all__ = ( + 'clear_events', 'job_end', 'job_start', 'post_sync', @@ -18,6 +35,152 @@ job_end = Signal() pre_sync = Signal() post_sync = Signal() +# Event signals +clear_events = Signal() + + +# +# Change logging & event handling +# + +@receiver((post_save, m2m_changed)) +def handle_changed_object(sender, instance, **kwargs): + """ + Fires when an object is created or updated. + """ + m2m_changed = False + + if not hasattr(instance, 'to_objectchange'): + return + + # Get the current request, or bail if not set + request = current_request.get() + if request is None: + return + + # Determine the type of change being made + if kwargs.get('created'): + event_type = OBJECT_CREATED + elif 'created' in kwargs: + event_type = OBJECT_UPDATED + elif kwargs.get('action') in ['post_add', 'post_remove'] and kwargs['pk_set']: + # m2m_changed with objects added or removed + m2m_changed = True + event_type = OBJECT_UPDATED + else: + return + + # Create/update an ObjectChange record for this change + action = { + OBJECT_CREATED: ObjectChangeActionChoices.ACTION_CREATE, + OBJECT_UPDATED: ObjectChangeActionChoices.ACTION_UPDATE, + OBJECT_DELETED: ObjectChangeActionChoices.ACTION_DELETE, + }[event_type] + objectchange = instance.to_objectchange(action) + # If this is a many-to-many field change, check for a previous ObjectChange instance recorded + # for this object by this request and update it + if m2m_changed and ( + prev_change := ObjectChange.objects.filter( + changed_object_type=ContentType.objects.get_for_model(instance), + changed_object_id=instance.pk, + request_id=request.id + ).first() + ): + prev_change.postchange_data = objectchange.postchange_data + prev_change.save() + elif objectchange and objectchange.has_changes: + objectchange.user = request.user + objectchange.request_id = request.id + objectchange.save() + + # Ensure that we're working with fresh M2M assignments + if m2m_changed: + instance.refresh_from_db() + + # Enqueue the object for event processing + queue = events_queue.get() + enqueue_event(queue, instance, request.user, request.id, event_type) + events_queue.set(queue) + + # Increment metric counters + if event_type == OBJECT_CREATED: + model_inserts.labels(instance._meta.model_name).inc() + elif event_type == OBJECT_UPDATED: + model_updates.labels(instance._meta.model_name).inc() + + +@receiver(pre_delete) +def handle_deleted_object(sender, instance, **kwargs): + """ + Fires when an object is deleted. + """ + # Run any deletion protection rules for the object. Note that this must occur prior + # to queueing any events for the object being deleted, in case a validation error is + # raised, causing the deletion to fail. + model_name = f'{sender._meta.app_label}.{sender._meta.model_name}' + validators = get_config().PROTECTION_RULES.get(model_name, []) + try: + run_validators(instance, validators) + except ValidationError as e: + raise AbortRequest( + _("Deletion is prevented by a protection rule: {message}").format(message=e) + ) + + # Get the current request, or bail if not set + request = current_request.get() + if request is None: + return + + # Record an ObjectChange if applicable + if hasattr(instance, 'to_objectchange'): + if hasattr(instance, 'snapshot') and not getattr(instance, '_prechange_snapshot', None): + instance.snapshot() + objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE) + objectchange.user = request.user + objectchange.request_id = request.id + objectchange.save() + + # Django does not automatically send an m2m_changed signal for the reverse direction of a + # many-to-many relationship (see https://code.djangoproject.com/ticket/17688), so we need to + # trigger one manually. We do this by checking for any reverse M2M relationships on the + # instance being deleted, and explicitly call .remove() on the remote M2M field to delete + # the association. This triggers an m2m_changed signal with the `post_remove` action type + # for the forward direction of the relationship, ensuring that the change is recorded. + for relation in instance._meta.related_objects: + if type(relation) is not ManyToManyRel: + continue + related_model = relation.related_model + related_field_name = relation.remote_field.name + if not issubclass(related_model, ChangeLoggingMixin): + # We only care about triggering the m2m_changed signal for models which support + # change logging + continue + for obj in related_model.objects.filter(**{related_field_name: instance.pk}): + obj.snapshot() # Ensure the change record includes the "before" state + getattr(obj, related_field_name).remove(instance) + + # Enqueue the object for event processing + queue = events_queue.get() + enqueue_event(queue, instance, request.user, request.id, OBJECT_DELETED) + events_queue.set(queue) + + # Increment metric counters + model_deletes.labels(instance._meta.model_name).inc() + + +@receiver(clear_events) +def clear_events_queue(sender, **kwargs): + """ + Delete any queued events (e.g. because of an aborted bulk transaction) + """ + logger = logging.getLogger('events') + logger.info(f"Clearing {len(events_queue.get())} queued events ({sender})") + events_queue.set({}) + + +# +# DataSource handlers +# @receiver(post_sync) def auto_sync(instance, **kwargs): diff --git a/netbox/extras/jobs.py b/netbox/extras/jobs.py index 62f8f6959..2529e9d2b 100644 --- a/netbox/extras/jobs.py +++ b/netbox/extras/jobs.py @@ -5,8 +5,8 @@ from contextlib import nullcontext from django.db import transaction from django.utils.translation import gettext as _ +from core.signals import clear_events from extras.models import Script as ScriptModel -from extras.signals import clear_events from netbox.context_managers import event_tracking from utilities.exceptions import AbortScript, AbortTransaction from utilities.jobs import JobRunner diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index b9e4726bf..eae9c02a0 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -1,194 +1,18 @@ -import importlib -import logging - from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ImproperlyConfigured, ValidationError -from django.db.models.fields.reverse_related import ManyToManyRel from django.db.models.signals import m2m_changed, post_save, pre_delete -from django.dispatch import receiver, Signal -from django.utils.translation import gettext_lazy as _ -from django_prometheus.models import model_deletes, model_inserts, model_updates +from django.dispatch import receiver -from core.choices import ObjectChangeActionChoices from core.events import * -from core.models import ObjectChange, ObjectType +from core.models import ObjectType from core.signals import job_end, job_start from extras.events import process_event_rules from extras.models import EventRule, Notification, Subscription from netbox.config import get_config -from netbox.context import current_request, events_queue -from netbox.models.features import ChangeLoggingMixin from netbox.registry import registry from netbox.signals import post_clean from utilities.exceptions import AbortRequest -from .events import enqueue_event from .models import CustomField, TaggedItem -from .validators import CustomValidator - - -def run_validators(instance, validators): - """ - Run the provided iterable of validators for the instance. - """ - request = current_request.get() - for validator in validators: - - # Loading a validator class by dotted path - if type(validator) is str: - module, cls = validator.rsplit('.', 1) - validator = getattr(importlib.import_module(module), cls)() - - # Constructing a new instance on the fly from a ruleset - elif type(validator) is dict: - validator = CustomValidator(validator) - - elif not issubclass(validator.__class__, CustomValidator): - raise ImproperlyConfigured(f"Invalid value for custom validator: {validator}") - - validator(instance, request) - - -# -# Change logging/webhooks -# - -# Define a custom signal that can be sent to clear any queued events -clear_events = Signal() - - -@receiver((post_save, m2m_changed)) -def handle_changed_object(sender, instance, **kwargs): - """ - Fires when an object is created or updated. - """ - m2m_changed = False - - if not hasattr(instance, 'to_objectchange'): - return - - # Get the current request, or bail if not set - request = current_request.get() - if request is None: - return - - # Determine the type of change being made - if kwargs.get('created'): - event_type = OBJECT_CREATED - elif 'created' in kwargs: - event_type = OBJECT_UPDATED - elif kwargs.get('action') in ['post_add', 'post_remove'] and kwargs['pk_set']: - # m2m_changed with objects added or removed - m2m_changed = True - event_type = OBJECT_UPDATED - else: - return - - # Create/update an ObjectChange record for this change - action = { - OBJECT_CREATED: ObjectChangeActionChoices.ACTION_CREATE, - OBJECT_UPDATED: ObjectChangeActionChoices.ACTION_UPDATE, - OBJECT_DELETED: ObjectChangeActionChoices.ACTION_DELETE, - }[event_type] - objectchange = instance.to_objectchange(action) - # If this is a many-to-many field change, check for a previous ObjectChange instance recorded - # for this object by this request and update it - if m2m_changed and ( - prev_change := ObjectChange.objects.filter( - changed_object_type=ContentType.objects.get_for_model(instance), - changed_object_id=instance.pk, - request_id=request.id - ).first() - ): - prev_change.postchange_data = objectchange.postchange_data - prev_change.save() - elif objectchange and objectchange.has_changes: - objectchange.user = request.user - objectchange.request_id = request.id - objectchange.save() - - # Ensure that we're working with fresh M2M assignments - if m2m_changed: - instance.refresh_from_db() - - # Enqueue the object for event processing - queue = events_queue.get() - enqueue_event(queue, instance, request.user, request.id, event_type) - events_queue.set(queue) - - # Increment metric counters - if event_type == OBJECT_CREATED: - model_inserts.labels(instance._meta.model_name).inc() - elif event_type == OBJECT_UPDATED: - model_updates.labels(instance._meta.model_name).inc() - - -@receiver(pre_delete) -def handle_deleted_object(sender, instance, **kwargs): - """ - Fires when an object is deleted. - """ - # Run any deletion protection rules for the object. Note that this must occur prior - # to queueing any events for the object being deleted, in case a validation error is - # raised, causing the deletion to fail. - model_name = f'{sender._meta.app_label}.{sender._meta.model_name}' - validators = get_config().PROTECTION_RULES.get(model_name, []) - try: - run_validators(instance, validators) - except ValidationError as e: - raise AbortRequest( - _("Deletion is prevented by a protection rule: {message}").format(message=e) - ) - - # Get the current request, or bail if not set - request = current_request.get() - if request is None: - return - - # Record an ObjectChange if applicable - if hasattr(instance, 'to_objectchange'): - if hasattr(instance, 'snapshot') and not getattr(instance, '_prechange_snapshot', None): - instance.snapshot() - objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE) - objectchange.user = request.user - objectchange.request_id = request.id - objectchange.save() - - # Django does not automatically send an m2m_changed signal for the reverse direction of a - # many-to-many relationship (see https://code.djangoproject.com/ticket/17688), so we need to - # trigger one manually. We do this by checking for any reverse M2M relationships on the - # instance being deleted, and explicitly call .remove() on the remote M2M field to delete - # the association. This triggers an m2m_changed signal with the `post_remove` action type - # for the forward direction of the relationship, ensuring that the change is recorded. - for relation in instance._meta.related_objects: - if type(relation) is not ManyToManyRel: - continue - related_model = relation.related_model - related_field_name = relation.remote_field.name - if not issubclass(related_model, ChangeLoggingMixin): - # We only care about triggering the m2m_changed signal for models which support - # change logging - continue - for obj in related_model.objects.filter(**{related_field_name: instance.pk}): - obj.snapshot() # Ensure the change record includes the "before" state - getattr(obj, related_field_name).remove(instance) - - # Enqueue the object for event processing - queue = events_queue.get() - enqueue_event(queue, instance, request.user, request.id, OBJECT_DELETED) - events_queue.set(queue) - - # Increment metric counters - model_deletes.labels(instance._meta.model_name).inc() - - -@receiver(clear_events) -def clear_events_queue(sender, **kwargs): - """ - Delete any queued events (e.g. because of an aborted bulk transaction) - """ - logger = logging.getLogger('events') - logger.info(f"Clearing {len(events_queue.get())} queued events ({sender})") - events_queue.set({}) +from .utils import run_validators # diff --git a/netbox/extras/utils.py b/netbox/extras/utils.py index e67b9b50c..28d2e13f0 100644 --- a/netbox/extras/utils.py +++ b/netbox/extras/utils.py @@ -1,5 +1,19 @@ +import importlib + +from django.core.exceptions import ImproperlyConfigured from taggit.managers import _TaggableManager +from netbox.context import current_request +from .validators import CustomValidator + +__all__ = ( + 'image_upload', + 'is_report', + 'is_script', + 'is_taggable', + 'run_validators', +) + def is_taggable(obj): """ @@ -48,3 +62,25 @@ def is_report(obj): return issubclass(obj, Report) and obj != Report except TypeError: return False + + +def run_validators(instance, validators): + """ + Run the provided iterable of CustomValidators for the instance. + """ + request = current_request.get() + for validator in validators: + + # Loading a validator class by dotted path + if type(validator) is str: + module, cls = validator.rsplit('.', 1) + validator = getattr(importlib.import_module(module), cls)() + + # Constructing a new instance on the fly from a ruleset + elif type(validator) is dict: + validator = CustomValidator(validator) + + elif not issubclass(validator.__class__, CustomValidator): + raise ImproperlyConfigured(f"Invalid value for custom validator: {validator}") + + validator(instance, request) diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 71ce411ba..7a2d4c08b 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -8,7 +8,7 @@ from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, Valida from django.db import transaction, IntegrityError from django.db.models import ManyToManyField, ProtectedError, RestrictedError from django.db.models.fields.reverse_related import ManyToManyRel -from django.forms import HiddenInput, ModelMultipleChoiceField, MultipleHiddenInput +from django.forms import ModelMultipleChoiceField, MultipleHiddenInput from django.http import HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse @@ -17,8 +17,8 @@ from django.utils.translation import gettext as _ from django_tables2.export import TableExport from core.models import ObjectType +from core.signals import clear_events from extras.models import ExportTemplate -from extras.signals import clear_events from utilities.error_handlers import handle_protectederror from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index cad7facd3..85f90cbc1 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -13,7 +13,7 @@ from django.utils.html import escape from django.utils.safestring import mark_safe from django.utils.translation import gettext as _ -from extras.signals import clear_events +from core.signals import clear_events from utilities.error_handlers import handle_protectederror from utilities.exceptions import AbortRequest, PermissionsViolation from utilities.forms import ConfirmationForm, restrict_form_fields From 80fc9abb03d4bf04bfdf2231f5a44807356fb3a7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 1 Aug 2024 13:57:19 -0400 Subject: [PATCH 23/23] 16927: Move JobRunner from utilities to netbox --- docs/plugins/development/background-jobs.md | 6 +++--- netbox/core/jobs.py | 2 +- netbox/extras/jobs.py | 2 +- netbox/{utilities => netbox}/jobs.py | 0 netbox/{utilities => netbox}/tests/test_jobs.py | 0 5 files changed, 5 insertions(+), 5 deletions(-) rename netbox/{utilities => netbox}/jobs.py (100%) rename netbox/{utilities => netbox}/tests/test_jobs.py (100%) diff --git a/docs/plugins/development/background-jobs.md b/docs/plugins/development/background-jobs.md index 810435268..873390a58 100644 --- a/docs/plugins/development/background-jobs.md +++ b/docs/plugins/development/background-jobs.md @@ -10,12 +10,12 @@ For example, your plugin might need to fetch data from a remote system. Dependin A background job implements a basic [Job](../../models/core/job.md) executor for all kinds of tasks. It has logic implemented to handle the management of the associated job object, rescheduling of periodic jobs in the given interval and error handling. Adding custom jobs is done by subclassing NetBox's `JobRunner` class. -::: utilities.jobs.JobRunner +::: netbox.jobs.JobRunner #### Example ```python title="jobs.py" -from utilities.jobs import JobRunner +from netbox.jobs import JobRunner class MyTestJob(JobRunner): @@ -47,7 +47,7 @@ As described above, jobs can be scheduled for immediate execution or at any late #### Example ```python title="jobs.py" -from utilities.jobs import JobRunner +from netbox.jobs import JobRunner class MyHousekeepingJob(JobRunner): diff --git a/netbox/core/jobs.py b/netbox/core/jobs.py index 1c38cf61f..d2b846398 100644 --- a/netbox/core/jobs.py +++ b/netbox/core/jobs.py @@ -1,7 +1,7 @@ import logging +from netbox.jobs import JobRunner from netbox.search.backends import search_backend -from utilities.jobs import JobRunner from .choices import DataSourceStatusChoices from .exceptions import SyncError from .models import DataSource diff --git a/netbox/extras/jobs.py b/netbox/extras/jobs.py index 2529e9d2b..e540ef439 100644 --- a/netbox/extras/jobs.py +++ b/netbox/extras/jobs.py @@ -8,8 +8,8 @@ from django.utils.translation import gettext as _ from core.signals import clear_events from extras.models import Script as ScriptModel from netbox.context_managers import event_tracking +from netbox.jobs import JobRunner from utilities.exceptions import AbortScript, AbortTransaction -from utilities.jobs import JobRunner from .utils import is_report diff --git a/netbox/utilities/jobs.py b/netbox/netbox/jobs.py similarity index 100% rename from netbox/utilities/jobs.py rename to netbox/netbox/jobs.py diff --git a/netbox/utilities/tests/test_jobs.py b/netbox/netbox/tests/test_jobs.py similarity index 100% rename from netbox/utilities/tests/test_jobs.py rename to netbox/netbox/tests/test_jobs.py