From 0df68bf29180a3fab599bf3f828821560d622dd2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 11:47:46 -0500 Subject: [PATCH 01/25] Rename ContentType proxy model to ObjectType --- netbox/core/forms/filtersets.py | 2 +- netbox/core/management/commands/nbshell.py | 4 +-- .../core/migrations/0008_contenttype_proxy.py | 6 ++--- netbox/core/models/contenttypes.py | 12 ++++----- netbox/core/models/jobs.py | 6 ++--- netbox/dcim/models/cables.py | 12 ++++----- netbox/extras/api/serializers.py | 26 +++++++++---------- netbox/extras/dashboard/widgets.py | 12 ++++----- netbox/extras/forms/bulk_import.py | 18 ++++++------- netbox/extras/forms/filtersets.py | 22 ++++++++-------- netbox/extras/forms/model_forms.py | 22 ++++++++-------- netbox/extras/models/change_logging.py | 4 +-- netbox/extras/models/customfields.py | 4 +-- netbox/extras/models/models.py | 8 +++--- netbox/ipam/models/ip.py | 4 +-- netbox/netbox/models/features.py | 6 ++--- netbox/tenancy/forms/filtersets.py | 4 +-- netbox/tenancy/models/contacts.py | 4 +-- netbox/users/models.py | 2 +- netbox/vpn/models/l2vpn.py | 6 ++--- 20 files changed, 91 insertions(+), 93 deletions(-) diff --git a/netbox/core/forms/filtersets.py b/netbox/core/forms/filtersets.py index 0c164ac29..bd74c0f14 100644 --- a/netbox/core/forms/filtersets.py +++ b/netbox/core/forms/filtersets.py @@ -68,7 +68,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm): ) object_type = ContentTypeChoiceField( label=_('Object Type'), - queryset=ContentType.objects.with_feature('jobs'), + queryset=ObjectType.objects.with_feature('jobs'), required=False, ) status = forms.MultipleChoiceField( diff --git a/netbox/core/management/commands/nbshell.py b/netbox/core/management/commands/nbshell.py index eeefe502b..b96870252 100644 --- a/netbox/core/management/commands/nbshell.py +++ b/netbox/core/management/commands/nbshell.py @@ -8,7 +8,7 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand -from core.models import ContentType +from core.models import ObjectType APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless') @@ -60,7 +60,7 @@ class Command(BaseCommand): pass # Additional objects to include - namespace['ContentType'] = ContentType + namespace['ObjectType'] = ObjectType namespace['User'] = get_user_model() # Load convenience commands diff --git a/netbox/core/migrations/0008_contenttype_proxy.py b/netbox/core/migrations/0008_contenttype_proxy.py index ac11d906a..dee82a969 100644 --- a/netbox/core/migrations/0008_contenttype_proxy.py +++ b/netbox/core/migrations/0008_contenttype_proxy.py @@ -1,5 +1,3 @@ -# Generated by Django 4.2.6 on 2023-10-31 19:38 - import core.models.contenttypes from django.db import migrations @@ -13,7 +11,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='ContentType', + name='ObjectType', fields=[ ], options={ @@ -23,7 +21,7 @@ class Migration(migrations.Migration): }, bases=('contenttypes.contenttype',), managers=[ - ('objects', core.models.contenttypes.ContentTypeManager()), + ('objects', core.models.contenttypes.ObjectTypeManager()), ], ), ] diff --git a/netbox/core/models/contenttypes.py b/netbox/core/models/contenttypes.py index c98184c3d..b0301848f 100644 --- a/netbox/core/models/contenttypes.py +++ b/netbox/core/models/contenttypes.py @@ -1,15 +1,15 @@ -from django.contrib.contenttypes.models import ContentType as ContentType_, ContentTypeManager as ContentTypeManager_ +from django.contrib.contenttypes.models import ContentType, ContentTypeManager from django.db.models import Q from netbox.registry import registry __all__ = ( - 'ContentType', - 'ContentTypeManager', + 'ObjectType', + 'ObjectTypeManager', ) -class ContentTypeManager(ContentTypeManager_): +class ObjectTypeManager(ContentTypeManager): def public(self): """ @@ -40,11 +40,11 @@ class ContentTypeManager(ContentTypeManager_): return self.get_queryset().filter(q) -class ContentType(ContentType_): +class ObjectType(ContentType): """ Wrap Django's native ContentType model to use our custom manager. """ - objects = ContentTypeManager() + objects = ObjectTypeManager() class Meta: proxy = True diff --git a/netbox/core/models/jobs.py b/netbox/core/models/jobs.py index 2e3425129..b9f0d0b91 100644 --- a/netbox/core/models/jobs.py +++ b/netbox/core/models/jobs.py @@ -11,7 +11,7 @@ from django.utils import timezone from django.utils.translation import gettext as _ from core.choices import JobStatusChoices -from core.models import ContentType +from core.models import ObjectType from core.signals import job_end, job_start from extras.constants import EVENT_JOB_END, EVENT_JOB_START from netbox.config import get_config @@ -130,7 +130,7 @@ class Job(models.Model): super().clean() # Validate the assigned object type - if self.object_type not in ContentType.objects.with_feature('jobs'): + if self.object_type not in ObjectType.objects.with_feature('jobs'): raise ValidationError( _("Jobs cannot be assigned to this object type ({type}).").format(type=self.object_type) ) @@ -210,7 +210,7 @@ class Job(models.Model): schedule_at: Schedule the job to be executed at the passed date and time interval: Recurrence interval (in minutes) """ - object_type = ContentType.objects.get_for_model(instance, for_concrete_model=False) + object_type = ObjectType.objects.get_for_model(instance, for_concrete_model=False) rq_queue_name = get_queue_for_model(object_type.model) queue = django_rq.get_queue(rq_queue_name) status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index cba345941..f8a61a794 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -9,7 +9,7 @@ from django.dispatch import Signal from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from dcim.choices import * from dcim.constants import * from dcim.fields import PathField @@ -481,13 +481,13 @@ class CablePath(models.Model): def origin_type(self): if self.path: ct_id, _ = decompile_path_node(self.path[0][0]) - return ContentType.objects.get_for_id(ct_id) + return ObjectType.objects.get_for_id(ct_id) @property def destination_type(self): if self.is_complete: ct_id, _ = decompile_path_node(self.path[-1][0]) - return ContentType.objects.get_for_id(ct_id) + return ObjectType.objects.get_for_id(ct_id) @property def path_objects(self): @@ -594,7 +594,7 @@ class CablePath(models.Model): # Step 6: Determine the far-end terminations if isinstance(links[0], Cable): - termination_type = ContentType.objects.get_for_model(terminations[0]) + termination_type = ObjectType.objects.get_for_model(terminations[0]) local_cable_terminations = CableTermination.objects.filter( termination_type=termination_type, termination_id__in=[t.pk for t in terminations] @@ -747,7 +747,7 @@ class CablePath(models.Model): # Prefetch path objects using one query per model type. Prefetch related devices where appropriate. prefetched = {} for ct_id, object_ids in to_prefetch.items(): - model_class = ContentType.objects.get_for_id(ct_id).model_class() + model_class = ObjectType.objects.get_for_id(ct_id).model_class() queryset = model_class.objects.filter(pk__in=object_ids) if hasattr(model_class, 'device'): queryset = queryset.prefetch_related('device') @@ -774,7 +774,7 @@ class CablePath(models.Model): """ Return all Cable IDs within the path. """ - cable_ct = ContentType.objects.get_for_model(Cable).pk + cable_ct = ObjectType.objects.get_for_model(Cable).pk cable_ids = [] for node in self._nodes: diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 7dad95263..a87947d68 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -7,7 +7,7 @@ from rest_framework import serializers from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer from core.api.serializers import JobSerializer -from core.models import ContentType +from core.models import ObjectType from dcim.api.nested_serializers import ( NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer, NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer, @@ -60,12 +60,12 @@ __all__ = ( class EventRuleSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail') content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('event_rules'), + queryset=ObjectType.objects.with_feature('event_rules'), many=True ) action_type = ChoiceField(choices=EventRuleActionChoices) action_object_type = ContentTypeField( - queryset=ContentType.objects.with_feature('event_rules'), + queryset=ObjectType.objects.with_feature('event_rules'), ) action_object = serializers.SerializerMethodField(read_only=True) @@ -118,12 +118,12 @@ class WebhookSerializer(NetBoxModelSerializer): class CustomFieldSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail') content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('custom_fields'), + queryset=ObjectType.objects.with_feature('custom_fields'), many=True ) type = ChoiceField(choices=CustomFieldTypeChoices) object_type = ContentTypeField( - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), required=False, allow_null=True ) @@ -197,7 +197,7 @@ class CustomFieldChoiceSetSerializer(ValidatedModelSerializer): class CustomLinkSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail') content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('custom_links'), + queryset=ObjectType.objects.with_feature('custom_links'), many=True ) @@ -217,7 +217,7 @@ class CustomLinkSerializer(ValidatedModelSerializer): class ExportTemplateSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail') content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('export_templates'), + queryset=ObjectType.objects.with_feature('export_templates'), many=True ) data_source = NestedDataSourceSerializer( @@ -244,7 +244,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer): class SavedFilterSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail') content_types = ContentTypeField( - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), many=True ) @@ -264,7 +264,7 @@ class SavedFilterSerializer(ValidatedModelSerializer): class BookmarkSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail') object_type = ContentTypeField( - queryset=ContentType.objects.with_feature('bookmarks'), + queryset=ObjectType.objects.with_feature('bookmarks'), ) object = serializers.SerializerMethodField(read_only=True) user = NestedUserSerializer() @@ -289,7 +289,7 @@ class BookmarkSerializer(ValidatedModelSerializer): class TagSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail') object_types = ContentTypeField( - queryset=ContentType.objects.with_feature('tags'), + queryset=ObjectType.objects.with_feature('tags'), many=True, required=False ) @@ -313,7 +313,7 @@ class TagSerializer(ValidatedModelSerializer): class ImageAttachmentSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail') content_type = ContentTypeField( - queryset=ContentType.objects.all() + queryset=ObjectType.objects.all() ) parent = serializers.SerializerMethodField(read_only=True) @@ -353,7 +353,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer): class JournalEntrySerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail') assigned_object_type = ContentTypeField( - queryset=ContentType.objects.all() + queryset=ObjectType.objects.all() ) assigned_object = serializers.SerializerMethodField(read_only=True) created_by = serializers.PrimaryKeyRelatedField( @@ -645,7 +645,7 @@ class ContentTypeSerializer(BaseModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail') class Meta: - model = ContentType + model = ObjectType fields = ['id', 'url', 'display', 'app_label', 'model'] diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py index 1bdc4bc1d..69bef0d8f 100644 --- a/netbox/extras/dashboard/widgets.py +++ b/netbox/extras/dashboard/widgets.py @@ -12,7 +12,7 @@ from django.template.loader import render_to_string from django.urls import NoReverseMatch, resolve, reverse from django.utils.translation import gettext as _ -from core.models import ContentType +from core.models import ObjectType from extras.choices import BookmarkOrderingChoices from utilities.choices import ButtonColorChoices from utilities.permissions import get_permission_for_model @@ -34,14 +34,14 @@ __all__ = ( def get_object_type_choices(): return [ (content_type_identifier(ct), content_type_name(ct)) - for ct in ContentType.objects.public().order_by('app_label', 'model') + for ct in ObjectType.objects.public().order_by('app_label', 'model') ] def get_bookmarks_object_type_choices(): return [ (content_type_identifier(ct), content_type_name(ct)) - for ct in ContentType.objects.with_feature('bookmarks').order_by('app_label', 'model') + for ct in ObjectType.objects.with_feature('bookmarks').order_by('app_label', 'model') ] @@ -52,7 +52,7 @@ def get_models_from_content_types(content_types): models = [] for content_type_id in content_types: app_label, model_name = content_type_id.split('.') - content_type = ContentType.objects.get_by_natural_key(app_label, model_name) + content_type = ObjectType.objects.get_by_natural_key(app_label, model_name) models.append(content_type.model_class()) return models @@ -238,7 +238,7 @@ class ObjectListWidget(DashboardWidget): def render(self, request): app_label, model_name = self.config['model'].split('.') - model = ContentType.objects.get_by_natural_key(app_label, model_name).model_class() + model = ObjectType.objects.get_by_natural_key(app_label, model_name).model_class() viewname = get_viewname(model, action='list') # Evaluate user's permission. Note that this controls only whether the HTMX element is @@ -371,7 +371,7 @@ class BookmarksWidget(DashboardWidget): bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by']) if object_types := self.config.get('object_types'): models = get_models_from_content_types(object_types) - conent_types = ContentType.objects.get_for_models(*models).values() + conent_types = ObjectType.objects.get_for_models(*models).values() bookmarks = bookmarks.filter(object_type__in=conent_types) if max_items := self.config.get('max_items'): bookmarks = bookmarks[:max_items] diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 440600af5..133233ccc 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -6,7 +6,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from extras.choices import * from extras.models import * from netbox.forms import NetBoxModelImportForm @@ -32,7 +32,7 @@ __all__ = ( class CustomFieldImportForm(CSVModelForm): content_types = CSVMultipleContentTypeField( label=_('Content types'), - queryset=ContentType.objects.with_feature('custom_fields'), + queryset=ObjectType.objects.with_feature('custom_fields'), help_text=_("One or more assigned object types") ) type = CSVChoiceField( @@ -42,7 +42,7 @@ class CustomFieldImportForm(CSVModelForm): ) object_type = CSVContentTypeField( label=_('Object type'), - queryset=ContentType.objects.public(), + queryset=ObjectType.objects.public(), required=False, help_text=_("Object type (for object or multi-object fields)") ) @@ -113,7 +113,7 @@ class CustomFieldChoiceSetImportForm(CSVModelForm): class CustomLinkImportForm(CSVModelForm): content_types = CSVMultipleContentTypeField( label=_('Content types'), - queryset=ContentType.objects.with_feature('custom_links'), + queryset=ObjectType.objects.with_feature('custom_links'), help_text=_("One or more assigned object types") ) @@ -128,7 +128,7 @@ class CustomLinkImportForm(CSVModelForm): class ExportTemplateImportForm(CSVModelForm): content_types = CSVMultipleContentTypeField( label=_('Content types'), - queryset=ContentType.objects.with_feature('export_templates'), + queryset=ObjectType.objects.with_feature('export_templates'), help_text=_("One or more assigned object types") ) @@ -151,7 +151,7 @@ class ConfigTemplateImportForm(CSVModelForm): class SavedFilterImportForm(CSVModelForm): content_types = CSVMultipleContentTypeField( label=_('Content types'), - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), help_text=_("One or more assigned object types") ) @@ -175,7 +175,7 @@ class WebhookImportForm(NetBoxModelImportForm): class EventRuleImportForm(NetBoxModelImportForm): content_types = CSVMultipleContentTypeField( label=_('Content types'), - queryset=ContentType.objects.with_feature('event_rules'), + queryset=ObjectType.objects.with_feature('event_rules'), help_text=_("One or more assigned object types") ) action_object = forms.CharField( @@ -213,7 +213,7 @@ class EventRuleImportForm(NetBoxModelImportForm): except ObjectDoesNotExist: raise forms.ValidationError(_("Script {name} not found").format(name=action_object)) self.instance.action_object = script - self.instance.action_object_type = ContentType.objects.get_for_model(script, for_concrete_model=False) + self.instance.action_object_type = ObjectType.objects.get_for_model(script, for_concrete_model=False) class TagImportForm(CSVModelForm): @@ -229,7 +229,7 @@ class TagImportForm(CSVModelForm): class JournalEntryImportForm(NetBoxModelImportForm): assigned_object_type = CSVContentTypeField( - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), label=_('Assigned object type'), ) kind = CSVChoiceField( diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 3a6421901..075144d12 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -2,7 +2,7 @@ from django import forms from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ -from core.models import ContentType, DataFile, DataSource +from core.models import ObjectType, DataFile, DataSource from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from extras.choices import * from extras.models import * @@ -43,7 +43,7 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): )), ) content_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.with_feature('custom_fields'), + queryset=ObjectType.objects.with_feature('custom_fields'), required=False, label=_('Object type') ) @@ -112,7 +112,7 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): ) content_types = ContentTypeMultipleChoiceField( label=_('Content types'), - queryset=ContentType.objects.with_feature('custom_links'), + queryset=ObjectType.objects.with_feature('custom_links'), required=False ) enabled = forms.NullBooleanField( @@ -155,7 +155,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): } ) content_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.with_feature('export_templates'), + queryset=ObjectType.objects.with_feature('export_templates'), required=False, label=_('Content types') ) @@ -183,7 +183,7 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): ) content_type_id = ContentTypeChoiceField( label=_('Content type'), - queryset=ContentType.objects.with_feature('image_attachments'), + queryset=ObjectType.objects.with_feature('image_attachments'), required=False ) name = forms.CharField( @@ -199,7 +199,7 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): ) content_types = ContentTypeMultipleChoiceField( label=_('Content types'), - queryset=ContentType.objects.public(), + queryset=ObjectType.objects.public(), required=False ) enabled = forms.NullBooleanField( @@ -254,7 +254,7 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm): (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')), ) content_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.with_feature('event_rules'), + queryset=ObjectType.objects.with_feature('event_rules'), required=False, label=_('Object type') ) @@ -310,12 +310,12 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm): class TagFilterForm(SavedFiltersMixin, FilterForm): model = Tag content_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.with_feature('tags'), + queryset=ObjectType.objects.with_feature('tags'), required=False, label=_('Tagged object type') ) for_object_type_id = ContentTypeChoiceField( - queryset=ContentType.objects.with_feature('tags'), + queryset=ObjectType.objects.with_feature('tags'), required=False, label=_('Allowed object type') ) @@ -464,7 +464,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm): label=_('User') ) assigned_object_type_id = DynamicModelMultipleChoiceField( - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), required=False, label=_('Object Type'), widget=APISelectMultiple( @@ -507,7 +507,7 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm): label=_('User') ) changed_object_type_id = DynamicModelMultipleChoiceField( - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), required=False, label=_('Object Type'), widget=APISelectMultiple( diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 5c3671b3c..443ba0a30 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -7,7 +7,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from core.forms.mixins import SyncedDataMixin -from core.models import ContentType +from core.models import ObjectType from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from extras.choices import * from extras.models import * @@ -41,11 +41,11 @@ __all__ = ( class CustomFieldForm(forms.ModelForm): content_types = ContentTypeMultipleChoiceField( label=_('Content types'), - queryset=ContentType.objects.with_feature('custom_fields') + queryset=ObjectType.objects.with_feature('custom_fields') ) object_type = ContentTypeChoiceField( label=_('Object type'), - queryset=ContentType.objects.public(), + queryset=ObjectType.objects.public(), required=False, help_text=_("Type of the related object (for object/multi-object fields only)") ) @@ -125,7 +125,7 @@ class CustomFieldChoiceSetForm(forms.ModelForm): class CustomLinkForm(forms.ModelForm): content_types = ContentTypeMultipleChoiceField( label=_('Content types'), - queryset=ContentType.objects.with_feature('custom_links') + queryset=ObjectType.objects.with_feature('custom_links') ) fieldsets = ( @@ -154,7 +154,7 @@ class CustomLinkForm(forms.ModelForm): class ExportTemplateForm(SyncedDataMixin, forms.ModelForm): content_types = ContentTypeMultipleChoiceField( label=_('Content types'), - queryset=ContentType.objects.with_feature('export_templates') + queryset=ObjectType.objects.with_feature('export_templates') ) template_code = forms.CharField( label=_('Template code'), @@ -195,7 +195,7 @@ class SavedFilterForm(forms.ModelForm): slug = SlugField() content_types = ContentTypeMultipleChoiceField( label=_('Content types'), - queryset=ContentType.objects.all() + queryset=ObjectType.objects.all() ) parameters = JSONField() @@ -221,7 +221,7 @@ class SavedFilterForm(forms.ModelForm): class BookmarkForm(forms.ModelForm): object_type = ContentTypeChoiceField( label=_('Object type'), - queryset=ContentType.objects.with_feature('bookmarks') + queryset=ObjectType.objects.with_feature('bookmarks') ) class Meta: @@ -251,7 +251,7 @@ class WebhookForm(NetBoxModelForm): class EventRuleForm(NetBoxModelForm): content_types = ContentTypeMultipleChoiceField( label=_('Content types'), - queryset=ContentType.objects.with_feature('event_rules'), + queryset=ObjectType.objects.with_feature('event_rules'), ) action_choice = forms.ChoiceField( label=_('Action choice'), @@ -339,11 +339,11 @@ class EventRuleForm(NetBoxModelForm): action_choice = self.cleaned_data.get('action_choice') # Webhook if self.cleaned_data.get('action_type') == EventRuleActionChoices.WEBHOOK: - self.cleaned_data['action_object_type'] = ContentType.objects.get_for_model(action_choice) + self.cleaned_data['action_object_type'] = ObjectType.objects.get_for_model(action_choice) self.cleaned_data['action_object_id'] = action_choice.id # Script elif self.cleaned_data.get('action_type') == EventRuleActionChoices.SCRIPT: - self.cleaned_data['action_object_type'] = ContentType.objects.get_for_model( + self.cleaned_data['action_object_type'] = ObjectType.objects.get_for_model( Script, for_concrete_model=False ) @@ -356,7 +356,7 @@ class TagForm(forms.ModelForm): slug = SlugField() object_types = ContentTypeMultipleChoiceField( label=_('Object types'), - queryset=ContentType.objects.with_feature('tags'), + queryset=ObjectType.objects.with_feature('tags'), required=False ) diff --git a/netbox/extras/models/change_logging.py b/netbox/extras/models/change_logging.py index 0155849aa..ebcebc09a 100644 --- a/netbox/extras/models/change_logging.py +++ b/netbox/extras/models/change_logging.py @@ -5,7 +5,7 @@ from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from extras.choices import * from ..querysets import ObjectChangeQuerySet @@ -113,7 +113,7 @@ class ObjectChange(models.Model): super().clean() # Validate the assigned object type - if self.changed_object_type not in ContentType.objects.with_feature('change_logging'): + if self.changed_object_type not in ObjectType.objects.with_feature('change_logging'): raise ValidationError( _("Change logging is not supported for this object type ({type}).").format( type=self.changed_object_type diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index e78d1af23..34b58e712 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -12,7 +12,7 @@ from django.urls import reverse from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from extras.choices import * from extras.data import CHOICE_SETS from netbox.models import ChangeLoggedModel @@ -52,7 +52,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)): """ Return all CustomFields assigned to the given model. """ - content_type = ContentType.objects.get_for_model(model._meta.concrete_model) + content_type = ObjectType.objects.get_for_model(model._meta.concrete_model) return self.get_queryset().filter(content_types=content_type) def get_defaults_for_model(self, model): diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 60bccd8f2..6b5a7c150 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -12,7 +12,7 @@ from django.utils.formats import date_format from django.utils.translation import gettext_lazy as _ from rest_framework.utils.encoders import JSONEncoder -from core.models import ContentType +from core.models import ObjectType from extras.choices import * from extras.conditions import ConditionSet from extras.constants import * @@ -646,7 +646,7 @@ class ImageAttachment(ChangeLoggedModel): super().clean() # Validate the assigned object type - if self.content_type not in ContentType.objects.with_feature('image_attachments'): + if self.content_type not in ObjectType.objects.with_feature('image_attachments'): raise ValidationError( _("Image attachments cannot be assigned to this object type ({type}).").format(type=self.content_type) ) @@ -739,7 +739,7 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat super().clean() # Validate the assigned object type - if self.assigned_object_type not in ContentType.objects.with_feature('journaling'): + if self.assigned_object_type not in ObjectType.objects.with_feature('journaling'): raise ValidationError( _("Journaling is not supported for this object type ({type}).").format(type=self.assigned_object_type) ) @@ -795,7 +795,7 @@ class Bookmark(models.Model): super().clean() # Validate the assigned object type - if self.object_type not in ContentType.objects.with_feature('bookmarks'): + if self.object_type not in ObjectType.objects.with_feature('bookmarks'): raise ValidationError( _("Bookmarks cannot be assigned to this object type ({type}).").format(type=self.object_type) ) diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 76fae2990..cce5b6b68 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -8,7 +8,7 @@ from django.urls import reverse from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from ipam.choices import * from ipam.constants import * from ipam.fields import IPNetworkField, IPAddressField @@ -861,7 +861,7 @@ class IPAddress(PrimaryModel): if self._original_assigned_object_id and self._original_assigned_object_type_id: parent = getattr(self.assigned_object, 'parent_object', None) - ct = ContentType.objects.get_for_id(self._original_assigned_object_type_id) + ct = ObjectType.objects.get_for_id(self._original_assigned_object_type_id) original_assigned_object = ct.get_object_for_this_type(pk=self._original_assigned_object_id) original_parent = getattr(original_assigned_object, 'parent_object', None) diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index dbd008354..fb6cf8498 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy as _ from taggit.managers import TaggableManager from core.choices import JobStatusChoices -from core.models import ContentType +from core.models import ObjectType from extras.choices import * from extras.utils import is_taggable from netbox.config import get_config @@ -490,7 +490,7 @@ class SyncedDataMixin(models.Model): ret = super().save(*args, **kwargs) # Create/delete AutoSyncRecord as needed - content_type = ContentType.objects.get_for_model(self) + content_type = ObjectType.objects.get_for_model(self) if self.auto_sync_enabled: AutoSyncRecord.objects.update_or_create( object_type=content_type, @@ -510,7 +510,7 @@ class SyncedDataMixin(models.Model): from core.models import AutoSyncRecord # Delete AutoSyncRecord - content_type = ContentType.objects.get_for_model(self) + content_type = ObjectType.objects.get_for_model(self) AutoSyncRecord.objects.filter( datafile=self.data_file, object_type=content_type, diff --git a/netbox/tenancy/forms/filtersets.py b/netbox/tenancy/forms/filtersets.py index 77e945542..e5f038923 100644 --- a/netbox/tenancy/forms/filtersets.py +++ b/netbox/tenancy/forms/filtersets.py @@ -1,7 +1,7 @@ from django import forms from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from netbox.forms import NetBoxModelFilterSetForm from tenancy.choices import * from tenancy.models import * @@ -86,7 +86,7 @@ class ContactAssignmentFilterForm(NetBoxModelFilterSetForm): (_('Assignment'), ('content_type_id', 'group_id', 'contact_id', 'role_id', 'priority')), ) content_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.with_feature('contacts'), + queryset=ObjectType.objects.with_feature('contacts'), required=False, label=_('Object type') ) diff --git a/netbox/tenancy/models/contacts.py b/netbox/tenancy/models/contacts.py index 664fff098..1ea62db0c 100644 --- a/netbox/tenancy/models/contacts.py +++ b/netbox/tenancy/models/contacts.py @@ -4,7 +4,7 @@ from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from netbox.models import ChangeLoggedModel, NestedGroupModel, OrganizationalModel, PrimaryModel from netbox.models.features import CustomFieldsMixin, ExportTemplatesMixin, TagsMixin from tenancy.choices import * @@ -165,7 +165,7 @@ class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, Chan super().clean() # Validate the assigned object type - if self.content_type not in ContentType.objects.with_feature('contacts'): + if self.content_type not in ObjectType.objects.with_feature('contacts'): raise ValidationError( _("Contacts cannot be assigned to this object type ({type}).").format(type=self.content_type) ) diff --git a/netbox/users/models.py b/netbox/users/models.py index 19d6013c7..94eb0ad58 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -22,7 +22,7 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from netaddr import IPNetwork -from core.models import ContentType +from core.models import ObjectType from ipam.fields import IPNetworkField from netbox.config import get_config from utilities.querysets import RestrictedQuerySet diff --git a/netbox/vpn/models/l2vpn.py b/netbox/vpn/models/l2vpn.py index 31d267113..39956edc8 100644 --- a/netbox/vpn/models/l2vpn.py +++ b/netbox/vpn/models/l2vpn.py @@ -5,7 +5,7 @@ from django.urls import reverse from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from netbox.models import NetBoxModel, PrimaryModel from netbox.models.features import ContactsMixin from vpn.choices import L2VPNTypeChoices @@ -128,7 +128,7 @@ class L2VPNTermination(NetBoxModel): # Only check is assigned_object is set. Required otherwise we have an Integrity Error thrown. if self.assigned_object: obj_id = self.assigned_object.pk - obj_type = ContentType.objects.get_for_model(self.assigned_object) + obj_type = ObjectType.objects.get_for_model(self.assigned_object) if L2VPNTermination.objects.filter(assigned_object_id=obj_id, assigned_object_type=obj_type).\ exclude(pk=self.pk).count() > 0: raise ValidationError( @@ -150,7 +150,7 @@ class L2VPNTermination(NetBoxModel): @property def assigned_object_parent(self): - obj_type = ContentType.objects.get_for_model(self.assigned_object) + obj_type = ObjectType.objects.get_for_model(self.assigned_object) if obj_type.model == 'vminterface': return self.assigned_object.virtual_machine elif obj_type.model == 'interface': From aeeec284a5d55cc70acff5628e4d0aa46589c892 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 14:36:35 -0500 Subject: [PATCH 02/25] Rename CustomField.content_types to object_types & use ObjectType proxy --- netbox/dcim/tests/test_models.py | 6 +- netbox/extras/api/customfields.py | 4 +- netbox/extras/api/serializers.py | 4 +- netbox/extras/filtersets.py | 10 ++- netbox/extras/forms/bulk_import.py | 6 +- netbox/extras/forms/filtersets.py | 4 +- netbox/extras/forms/model_forms.py | 7 +- netbox/extras/graphql/types.py | 2 +- .../migrations/0111_rename_content_types.py | 28 ++++++ netbox/extras/models/customfields.py | 10 +-- netbox/extras/signals.py | 6 +- netbox/extras/tests/test_api.py | 14 +-- netbox/extras/tests/test_changelog.py | 13 +-- netbox/extras/tests/test_customfields.py | 90 +++++++++---------- netbox/extras/tests/test_filtersets.py | 17 ++-- netbox/extras/tests/test_forms.py | 29 +++--- netbox/extras/tests/test_views.py | 9 +- netbox/extras/views.py | 4 +- netbox/netbox/api/viewsets/mixins.py | 5 +- netbox/netbox/filtersets.py | 2 +- netbox/netbox/forms/base.py | 2 +- netbox/netbox/forms/mixins.py | 7 +- netbox/netbox/middleware.py | 4 +- netbox/netbox/search/backends.py | 13 +-- netbox/netbox/tables/tables.py | 8 +- netbox/utilities/testing/base.py | 7 +- netbox/utilities/tests/test_api.py | 8 +- 27 files changed, 177 insertions(+), 142 deletions(-) create mode 100644 netbox/extras/migrations/0111_rename_content_types.py diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 8eb057020..1a5cc8435 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -1,8 +1,8 @@ -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.test import TestCase from circuits.models import * +from core.models import ObjectType from dcim.choices import * from dcim.models import * from extras.models import CustomField @@ -293,8 +293,8 @@ class DeviceTestCase(TestCase): # Create a CustomField with a default value & assign it to all component models cf1 = CustomField.objects.create(name='cf1', default='foo') - cf1.content_types.set( - ContentType.objects.filter(app_label='dcim', model__in=[ + cf1.object_types.set( + ObjectType.objects.filter(app_label='dcim', model__in=[ 'consoleport', 'consoleserverport', 'powerport', diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 6cd3a245e..77c3a170e 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -26,7 +26,7 @@ class CustomFieldDefaultValues: # Retrieve the CustomFields for the parent model content_type = ContentType.objects.get_for_model(self.model) - fields = CustomField.objects.filter(content_types=content_type) + fields = CustomField.objects.filter(object_types=content_type) # Populate the default value for each CustomField value = {} @@ -48,7 +48,7 @@ class CustomFieldsDataField(Field): """ if not hasattr(self, '_custom_fields'): content_type = ContentType.objects.get_for_model(self.parent.Meta.model) - self._custom_fields = CustomField.objects.filter(content_types=content_type) + self._custom_fields = CustomField.objects.filter(object_types=content_type) return self._custom_fields def to_representation(self, obj): diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index a87947d68..370450712 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -117,7 +117,7 @@ class WebhookSerializer(NetBoxModelSerializer): class CustomFieldSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail') - content_types = ContentTypeField( + object_types = ContentTypeField( queryset=ObjectType.objects.with_feature('custom_fields'), many=True ) @@ -139,7 +139,7 @@ class CustomFieldSerializer(ValidatedModelSerializer): class Meta: model = CustomField fields = [ - 'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', + 'id', 'url', 'display', 'object_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'created', 'last_updated', diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 734f7db50..290670f5c 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -124,10 +124,12 @@ class CustomFieldFilterSet(BaseFilterSet): type = django_filters.MultipleChoiceFilter( choices=CustomFieldTypeChoices ) - content_type_id = MultiValueNumberFilter( - field_name='content_types__id' + object_types_id = MultiValueNumberFilter( + field_name='object_types__id' + ) + object_types = ContentTypeFilter( + field_name='object_types' ) - content_types = ContentTypeFilter() choice_set_id = django_filters.ModelMultipleChoiceFilter( queryset=CustomFieldChoiceSet.objects.all() ) @@ -140,7 +142,7 @@ class CustomFieldFilterSet(BaseFilterSet): class Meta: model = CustomField fields = [ - 'id', 'content_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible', + 'id', 'object_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', 'description', ] diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 133233ccc..1d1b27617 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -30,8 +30,8 @@ __all__ = ( class CustomFieldImportForm(CSVModelForm): - content_types = CSVMultipleContentTypeField( - label=_('Content types'), + object_types = CSVMultipleContentTypeField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_fields'), help_text=_("One or more assigned object types") ) @@ -69,7 +69,7 @@ class CustomFieldImportForm(CSVModelForm): class Meta: model = CustomField fields = ( - 'name', 'label', 'group_name', 'type', 'content_types', 'object_type', 'required', 'description', + 'name', 'label', 'group_name', 'type', 'object_types', 'object_type', 'required', 'description', 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 075144d12..ccfeb8c1d 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -38,11 +38,11 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), (_('Attributes'), ( - 'type', 'content_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable', + 'type', 'object_types_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable', 'is_cloneable', )), ) - content_type_id = ContentTypeMultipleChoiceField( + object_types_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('custom_fields'), required=False, label=_('Object type') diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 443ba0a30..776265878 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -2,7 +2,6 @@ import json import re from django import forms -from django.contrib.contenttypes.models import ContentType from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ @@ -39,8 +38,8 @@ __all__ = ( class CustomFieldForm(forms.ModelForm): - content_types = ContentTypeMultipleChoiceField( - label=_('Content types'), + object_types = ContentTypeMultipleChoiceField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_fields') ) object_type = ContentTypeChoiceField( @@ -56,7 +55,7 @@ class CustomFieldForm(forms.ModelForm): fieldsets = ( (_('Custom Field'), ( - 'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description', + 'object_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description', )), (_('Behavior'), ('search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable')), (_('Values'), ('default', 'choice_set')), diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 4981ddd72..643568eaa 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -39,7 +39,7 @@ class CustomFieldType(ObjectType): class Meta: model = models.CustomField - exclude = ('content_types', ) + exclude = ('object_types', 'object_type') filterset_class = filtersets.CustomFieldFilterSet diff --git a/netbox/extras/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py new file mode 100644 index 000000000..2b7b2358a --- /dev/null +++ b/netbox/extras/migrations/0111_rename_content_types.py @@ -0,0 +1,28 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_gfk_indexes'), + ('extras', '0110_remove_eventrule_action_parameters'), + ] + + operations = [ + migrations.RenameField( + model_name='customfield', + old_name='content_types', + new_name='object_types', + ), + migrations.AlterField( + model_name='customfield', + name='object_types', + field=models.ManyToManyField(related_name='custom_fields', to='core.objecttype'), + ), + migrations.AlterField( + model_name='customfield', + name='object_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.objecttype'), + ), + ] diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 34b58e712..311ccce76 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -53,7 +53,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)): Return all CustomFields assigned to the given model. """ content_type = ObjectType.objects.get_for_model(model._meta.concrete_model) - return self.get_queryset().filter(content_types=content_type) + return self.get_queryset().filter(object_types=content_type) def get_defaults_for_model(self, model): """ @@ -66,8 +66,8 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)): class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): - content_types = models.ManyToManyField( - to='contenttypes.ContentType', + object_types = models.ManyToManyField( + to='core.ObjectType', related_name='custom_fields', help_text=_('The object(s) to which this field applies.') ) @@ -79,7 +79,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): help_text=_('The type of data this custom field holds') ) object_type = models.ForeignKey( - to='contenttypes.ContentType', + to='core.ObjectType', on_delete=models.PROTECT, blank=True, null=True, @@ -284,7 +284,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): """ Called when a CustomField has been renamed. Updates all assigned object data. """ - for ct in self.content_types.all(): + for ct in self.object_types.all(): model = ct.model_class() params = {f'custom_field_data__{old_name}__isnull': False} instances = model.objects.filter(**params) diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index f8dc204e7..85c00169c 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -205,13 +205,13 @@ def handle_cf_deleted(instance, **kwargs): """ Handle the cleanup of old custom field data when a CustomField is deleted. """ - instance.remove_stale_data(instance.content_types.all()) + instance.remove_stale_data(instance.object_types.all()) post_save.connect(handle_cf_renamed, sender=CustomField) pre_delete.connect(handle_cf_deleted, sender=CustomField) -m2m_changed.connect(handle_cf_added_obj_types, sender=CustomField.content_types.through) -m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through) +m2m_changed.connect(handle_cf_added_obj_types, sender=CustomField.object_types.through) +m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.object_types.through) # diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 5db906b25..01bbb312f 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -7,10 +7,10 @@ from django.utils.timezone import make_aware from rest_framework import status from core.choices import ManagedFileRootPathChoices +from core.models import ObjectType from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site from extras.choices import * from extras.models import * -from extras.reports import Report from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar from utilities.testing import APITestCase, APIViewTestCases @@ -152,17 +152,17 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase): brief_fields = ['description', 'display', 'id', 'name', 'url'] create_data = [ { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'cf4', 'type': 'date', }, { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'cf5', 'type': 'url', }, { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'cf6', 'type': 'text', }, @@ -171,14 +171,14 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase): 'description': 'New description', } update_data = { - 'content_types': ['dcim.device'], + 'object_types': ['dcim.device'], 'name': 'New_Name', 'description': 'New description', } @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_ct = ObjectType.objects.get_for_model(Site) custom_fields = ( CustomField( @@ -196,7 +196,7 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase): ) CustomField.objects.bulk_create(custom_fields) for cf in custom_fields: - cf.content_types.add(site_ct) + cf.object_types.add(site_ct) class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase): diff --git a/netbox/extras/tests/test_changelog.py b/netbox/extras/tests/test_changelog.py index e144c5dee..d9d6f1f45 100644 --- a/netbox/extras/tests/test_changelog.py +++ b/netbox/extras/tests/test_changelog.py @@ -3,6 +3,7 @@ from django.test import override_settings from django.urls import reverse from rest_framework import status +from core.models import ObjectType from dcim.choices import SiteStatusChoices from dcim.models import Site from extras.choices import * @@ -23,14 +24,14 @@ class ChangeLogViewTest(ModelViewTestCase): ) # Create a custom field on the Site model - ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) cf = CustomField( type=CustomFieldTypeChoices.TYPE_TEXT, name='cf1', required=False ) cf.save() - cf.content_types.set([ct]) + cf.object_types.set([site_type]) # Create a select custom field on the Site model cf_select = CustomField( @@ -40,7 +41,7 @@ class ChangeLogViewTest(ModelViewTestCase): choice_set=choice_set ) cf_select.save() - cf_select.content_types.set([ct]) + cf_select.object_types.set([site_type]) def test_create_object(self): tags = create_tags('Tag 1', 'Tag 2') @@ -275,14 +276,14 @@ class ChangeLogAPITest(APITestCase): def setUpTestData(cls): # Create a custom field on the Site model - ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) cf = CustomField( type=CustomFieldTypeChoices.TYPE_TEXT, name='cf1', required=False ) cf.save() - cf.content_types.set([ct]) + cf.object_types.set([site_type]) # Create a select custom field on the Site model choice_set = CustomFieldChoiceSet.objects.create( @@ -296,7 +297,7 @@ class ChangeLogAPITest(APITestCase): choice_set=choice_set ) cf_select.save() - cf_select.content_types.set([ct]) + cf_select.object_types.set([site_type]) # Create some tags tags = ( diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 574452a81..7ca18250c 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -1,11 +1,11 @@ import datetime from decimal import Decimal -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.urls import reverse from rest_framework import status +from core.models import ObjectType from dcim.filtersets import SiteFilterSet from dcim.forms import SiteImportForm from dcim.models import Manufacturer, Rack, Site @@ -28,7 +28,7 @@ class CustomFieldTest(TestCase): Site(name='Site C', slug='site-c'), ]) - cls.object_type = ContentType.objects.get_for_model(Site) + cls.object_type = ObjectType.objects.get_for_model(Site) def test_invalid_name(self): """ @@ -50,7 +50,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_TEXT, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -75,7 +75,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_LONGTEXT, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -99,7 +99,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_INTEGER, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -125,7 +125,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_DECIMAL, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -151,7 +151,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_INTEGER, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -178,7 +178,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_DATE, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -203,7 +203,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_DATETIME, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -228,7 +228,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_URL, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -253,7 +253,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_JSON, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -290,7 +290,7 @@ class CustomFieldTest(TestCase): required=False, choice_set=choice_set ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -327,7 +327,7 @@ class CustomFieldTest(TestCase): required=False, choice_set=choice_set ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -350,10 +350,10 @@ class CustomFieldTest(TestCase): cf = CustomField.objects.create( name='object_field', type=CustomFieldTypeChoices.TYPE_OBJECT, - object_type=ContentType.objects.get_for_model(VLAN), + object_type=ObjectType.objects.get_for_model(VLAN), required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -382,10 +382,10 @@ class CustomFieldTest(TestCase): cf = CustomField.objects.create( name='object_field', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, - object_type=ContentType.objects.get_for_model(VLAN), + object_type=ObjectType.objects.get_for_model(VLAN), required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -402,13 +402,13 @@ class CustomFieldTest(TestCase): self.assertIsNone(instance.custom_field_data.get(cf.name)) def test_rename_customfield(self): - obj_type = ContentType.objects.get_for_model(Site) + obj_type = ObjectType.objects.get_for_model(Site) FIELD_DATA = 'abc' # Create a custom field cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1') cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([obj_type]) # Assign custom field data to an object site = Site.objects.create( @@ -437,7 +437,7 @@ class CustomFieldTest(TestCase): ) ) site = Site.objects.create(name='Site 1', slug='site-1') - object_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) # Text CustomField(name='test', type='text', required=True, default="Default text").full_clean() @@ -524,10 +524,10 @@ class CustomFieldManagerTest(TestCase): @classmethod def setUpTestData(cls): - content_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) custom_field = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo') custom_field.save() - custom_field.content_types.set([content_type]) + custom_field.object_types.set([object_type]) def test_get_for_model(self): self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1) @@ -538,7 +538,7 @@ class CustomFieldAPITest(APITestCase): @classmethod def setUpTestData(cls): - content_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) # Create some VLANs vlans = ( @@ -581,19 +581,19 @@ class CustomFieldAPITest(APITestCase): CustomField( type=CustomFieldTypeChoices.TYPE_OBJECT, name='object_field', - object_type=ContentType.objects.get_for_model(VLAN), + object_type=ObjectType.objects.get_for_model(VLAN), default=vlans[0].pk, ), CustomField( type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, name='multiobject_field', - object_type=ContentType.objects.get_for_model(VLAN), + object_type=ObjectType.objects.get_for_model(VLAN), default=[vlans[0].pk, vlans[1].pk], ), ) for cf in custom_fields: cf.save() - cf.content_types.set([content_type]) + cf.object_types.set([object_type]) # Create some sites *after* creating the custom fields. This ensures that # default values are not set for the assigned objects. @@ -1163,7 +1163,7 @@ class CustomFieldImportTest(TestCase): ) for cf in custom_fields: cf.save() - cf.content_types.set([ContentType.objects.get_for_model(Site)]) + cf.object_types.set([ObjectType.objects.get_for_model(Site)]) def test_import(self): """ @@ -1256,11 +1256,11 @@ class CustomFieldModelTest(TestCase): def setUpTestData(cls): cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='foo') cf1.save() - cf1.content_types.set([ContentType.objects.get_for_model(Site)]) + cf1.object_types.set([ObjectType.objects.get_for_model(Site)]) cf2 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='bar') cf2.save() - cf2.content_types.set([ContentType.objects.get_for_model(Rack)]) + cf2.object_types.set([ObjectType.objects.get_for_model(Rack)]) def test_cf_data(self): """ @@ -1299,7 +1299,7 @@ class CustomFieldModelTest(TestCase): """ cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='baz', required=True) cf3.save() - cf3.content_types.set([ContentType.objects.get_for_model(Site)]) + cf3.object_types.set([ObjectType.objects.get_for_model(Site)]) site = Site(name='Test Site', slug='test-site') @@ -1318,7 +1318,7 @@ class CustomFieldModelFilterTest(TestCase): @classmethod def setUpTestData(cls): - obj_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) manufacturers = Manufacturer.objects.bulk_create(( Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), @@ -1335,17 +1335,17 @@ class CustomFieldModelFilterTest(TestCase): # Integer filtering cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Decimal filtering cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_DECIMAL) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Boolean filtering cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_BOOLEAN) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Exact text filtering cf = CustomField( @@ -1354,7 +1354,7 @@ class CustomFieldModelFilterTest(TestCase): filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Loose text filtering cf = CustomField( @@ -1363,12 +1363,12 @@ class CustomFieldModelFilterTest(TestCase): filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Date filtering cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_DATE) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Exact URL filtering cf = CustomField( @@ -1377,7 +1377,7 @@ class CustomFieldModelFilterTest(TestCase): filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Loose URL filtering cf = CustomField( @@ -1386,7 +1386,7 @@ class CustomFieldModelFilterTest(TestCase): filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Selection filtering cf = CustomField( @@ -1395,7 +1395,7 @@ class CustomFieldModelFilterTest(TestCase): choice_set=choice_set ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Multiselect filtering cf = CustomField( @@ -1404,25 +1404,25 @@ class CustomFieldModelFilterTest(TestCase): choice_set=choice_set ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Object filtering cf = CustomField( name='cf11', type=CustomFieldTypeChoices.TYPE_OBJECT, - object_type=ContentType.objects.get_for_model(Manufacturer) + object_type=ObjectType.objects.get_for_model(Manufacturer) ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Multi-object filtering cf = CustomField( name='cf12', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, - object_type=ContentType.objects.get_for_model(Manufacturer) + object_type=ObjectType.objects.get_for_model(Manufacturer) ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) Site.objects.bulk_create([ Site(name='Site 1', slug='site-1', custom_field_data={ diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index ef8aedcbd..25263fe68 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -7,6 +7,7 @@ from django.test import TestCase from circuits.models import Provider from core.choices import ManagedFileRootPathChoices +from core.models import ObjectType from dcim.filtersets import SiteFilterSet from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup from dcim.models import Location @@ -87,11 +88,11 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests): ), ) CustomField.objects.bulk_create(custom_fields) - custom_fields[0].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'site')) - custom_fields[1].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'rack')) - custom_fields[2].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device')) - custom_fields[3].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device')) - custom_fields[4].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device')) + custom_fields[0].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'site')) + custom_fields[1].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'rack')) + custom_fields[2].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device')) + custom_fields[3].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device')) + custom_fields[4].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device')) def test_q(self): params = {'q': 'foobar1'} @@ -101,10 +102,10 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests): params = {'name': ['Custom Field 1', 'Custom Field 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_content_types(self): - params = {'content_types': 'dcim.site'} + def test_object_types(self): + params = {'object_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'content_type_id': [ContentType.objects.get_by_natural_key('dcim', 'site').pk]} + params = {'object_types_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_required(self): diff --git a/netbox/extras/tests/test_forms.py b/netbox/extras/tests/test_forms.py index 9c22bf83c..5adb57fb2 100644 --- a/netbox/extras/tests/test_forms.py +++ b/netbox/extras/tests/test_forms.py @@ -1,6 +1,7 @@ from django.contrib.contenttypes.models import ContentType from django.test import TestCase +from core.models import ObjectType from dcim.forms import SiteForm from dcim.models import Site from extras.choices import CustomFieldTypeChoices @@ -12,66 +13,66 @@ class CustomFieldModelFormTest(TestCase): @classmethod def setUpTestData(cls): - obj_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) choice_set = CustomFieldChoiceSet.objects.create( name='Choice Set 1', extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C')) ) cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT) - cf_text.content_types.set([obj_type]) + cf_text.object_types.set([object_type]) cf_longtext = CustomField.objects.create(name='longtext', type=CustomFieldTypeChoices.TYPE_LONGTEXT) - cf_longtext.content_types.set([obj_type]) + cf_longtext.object_types.set([object_type]) cf_integer = CustomField.objects.create(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER) - cf_integer.content_types.set([obj_type]) + cf_integer.object_types.set([object_type]) cf_integer = CustomField.objects.create(name='decimal', type=CustomFieldTypeChoices.TYPE_DECIMAL) - cf_integer.content_types.set([obj_type]) + cf_integer.object_types.set([object_type]) cf_boolean = CustomField.objects.create(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN) - cf_boolean.content_types.set([obj_type]) + cf_boolean.object_types.set([object_type]) cf_date = CustomField.objects.create(name='date', type=CustomFieldTypeChoices.TYPE_DATE) - cf_date.content_types.set([obj_type]) + cf_date.object_types.set([object_type]) cf_datetime = CustomField.objects.create(name='datetime', type=CustomFieldTypeChoices.TYPE_DATETIME) - cf_datetime.content_types.set([obj_type]) + cf_datetime.object_types.set([object_type]) cf_url = CustomField.objects.create(name='url', type=CustomFieldTypeChoices.TYPE_URL) - cf_url.content_types.set([obj_type]) + cf_url.object_types.set([object_type]) cf_json = CustomField.objects.create(name='json', type=CustomFieldTypeChoices.TYPE_JSON) - cf_json.content_types.set([obj_type]) + cf_json.object_types.set([object_type]) cf_select = CustomField.objects.create( name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choice_set=choice_set ) - cf_select.content_types.set([obj_type]) + cf_select.object_types.set([object_type]) cf_multiselect = CustomField.objects.create( name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choice_set=choice_set ) - cf_multiselect.content_types.set([obj_type]) + cf_multiselect.object_types.set([object_type]) cf_object = CustomField.objects.create( name='object', type=CustomFieldTypeChoices.TYPE_OBJECT, object_type=ContentType.objects.get_for_model(Site) ) - cf_object.content_types.set([obj_type]) + cf_object.object_types.set([object_type]) cf_multiobject = CustomField.objects.create( name='multiobject', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, object_type=ContentType.objects.get_for_model(Site) ) - cf_multiobject.content_types.set([obj_type]) + cf_multiobject.object_types.set([object_type]) def test_empty_values(self): """ diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index d720560e4..699388c53 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -5,6 +5,7 @@ from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.urls import reverse +from core.models import ObjectType from dcim.models import DeviceType, Manufacturer, Site from extras.choices import * from extras.models import * @@ -19,7 +20,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) CustomFieldChoiceSet.objects.create( name='Choice Set 1', extra_choices=( @@ -36,13 +37,13 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) for customfield in custom_fields: customfield.save() - customfield.content_types.add(site_ct) + customfield.object_types.add(site_type) cls.form_data = { 'name': 'field_x', 'label': 'Field X', 'type': 'text', - 'content_types': [site_ct.pk], + 'object_types': [site_type.pk], 'search_weight': 2000, 'filter_logic': CustomFieldFilterLogicChoices.FILTER_EXACT, 'default': None, @@ -53,7 +54,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - 'name,label,type,content_types,object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visible,ui_editable', + 'name,label,type,object_types,object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visible,ui_editable', 'field4,Field 4,text,dcim.site,,100,1000,exact,,,,[a-z]{3},always,yes', 'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,always,yes', 'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,always,yes', diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 56840f7a9..49c6dcab3 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -46,9 +46,9 @@ class CustomFieldView(generic.ObjectView): def get_extra_context(self, request, instance): related_models = () - for content_type in instance.content_types.all(): + for object_type in instance.object_types.all(): related_models += ( - content_type.model_class().objects.restrict(request.user, 'view').exclude( + object_type.model_class().objects.restrict(request.user, 'view').exclude( Q(**{f'custom_field_data__{instance.name}': ''}) | Q(**{f'custom_field_data__{instance.name}': None}) ), diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index 9d8286968..7df498ac6 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -5,6 +5,7 @@ from django.http import Http404 from rest_framework import status from rest_framework.response import Response +from core.models import ObjectType from extras.models import ExportTemplate from netbox.api.serializers import BulkOperationSerializer @@ -26,9 +27,9 @@ class CustomFieldsMixin: context = super().get_serializer_context() if hasattr(self.queryset.model, 'custom_fields'): - content_type = ContentType.objects.get_for_model(self.queryset.model) + object_type = ObjectType.objects.get_for_model(self.queryset.model) context.update({ - 'custom_fields': content_type.custom_fields.all(), + 'custom_fields': object_type.custom_fields.all(), }) return context diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index ebb98d15f..7f07cfbfb 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -281,7 +281,7 @@ class NetBoxModelFilterSet(ChangeLoggedModelFilterSet): # Dynamically add a Filter for each CustomField applicable to the parent model custom_fields = CustomField.objects.filter( - content_types=ContentType.objects.get_for_model(self._meta.model) + object_types=ContentType.objects.get_for_model(self._meta.model) ).exclude( filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED ) diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 7e1eaa80c..12de3b12b 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -88,7 +88,7 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): def _get_custom_fields(self, content_type): return CustomField.objects.filter( - content_types=content_type, + object_types=content_type, ui_editable=CustomFieldUIEditableChoices.YES ) diff --git a/netbox/netbox/forms/mixins.py b/netbox/netbox/forms/mixins.py index 815f1f6fa..2f903db5d 100644 --- a/netbox/netbox/forms/mixins.py +++ b/netbox/netbox/forms/mixins.py @@ -2,6 +2,7 @@ from django import forms from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ +from core.models import ObjectType from extras.choices import * from extras.models import * from utilities.forms.fields import DynamicModelMultipleChoiceField @@ -32,16 +33,16 @@ class CustomFieldsMixin: def _get_content_type(self): """ - Return the ContentType of the form's model. + Return the ObjectType of the form's model. """ if not getattr(self, 'model', None): raise NotImplementedError(_("{class_name} must specify a model class.").format( class_name=self.__class__.__name__ )) - return ContentType.objects.get_for_model(self.model) + return ObjectType.objects.get_for_model(self.model) def _get_custom_fields(self, content_type): - return CustomField.objects.filter(content_types=content_type).exclude( + return CustomField.objects.filter(object_types=content_type).exclude( ui_editable=CustomFieldUIEditableChoices.HIDDEN ) diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index cb7d2c8ba..c140462ec 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -70,8 +70,8 @@ class CoreMiddleware: return # Cleanly handle exceptions that occur from REST API requests - if is_api_request(request): - return rest_api_server_error(request) + # if is_api_request(request): + # return rest_api_server_error(request) # Ignore Http404s (defer to Django's built-in 404 handling) if isinstance(exception, Http404): diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index 1fb23a37c..0d7667087 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -11,6 +11,7 @@ from django.utils.module_loading import import_string import netaddr from netaddr.core import AddrFormatError +from core.models import ObjectType from extras.models import CachedValue, CustomField from netbox.registry import registry from utilities.querysets import RestrictedPrefetch @@ -134,7 +135,7 @@ class CachedValueSearchBackend(SearchBackend): # objects). This must be done before generating the final results list, which returns # a RawQuerySet. content_type_ids = set(queryset.values_list('object_type', flat=True)) - content_types = ContentType.objects.filter(pk__in=content_type_ids) + object_types = ObjectType.objects.filter(pk__in=content_type_ids) # Construct a Prefetch to pre-fetch only those related objects for which the # user has permission to view. @@ -153,7 +154,7 @@ class CachedValueSearchBackend(SearchBackend): # Iterate through each ContentType represented in the search results and prefetch any # related objects necessary to render the prescribed display attributes (display_attrs). - for ct in content_types: + for ct in object_types: model = ct.model_class() indexer = registry['search'].get(content_type_identifier(ct)) if not (display_attrs := getattr(indexer, 'display_attrs', None)): @@ -182,7 +183,7 @@ class CachedValueSearchBackend(SearchBackend): return ret def cache(self, instances, indexer=None, remove_existing=True): - content_type = None + object_type = None custom_fields = None # Convert a single instance to an iterable @@ -204,8 +205,8 @@ class CachedValueSearchBackend(SearchBackend): break # Prefetch any associated custom fields - content_type = ContentType.objects.get_for_model(indexer.model) - custom_fields = CustomField.objects.filter(content_types=content_type).exclude(search_weight=0) + object_type = ObjectType.objects.get_for_model(indexer.model) + custom_fields = CustomField.objects.filter(object_types=object_type).exclude(search_weight=0) # Wipe out any previously cached values for the object if remove_existing: @@ -215,7 +216,7 @@ class CachedValueSearchBackend(SearchBackend): for field in indexer.to_cache(instance, custom_fields=custom_fields): buffer.append( CachedValue( - object_type=content_type, + object_type=object_type, object_id=instance.pk, field=field.name, type=field.type, diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 495e56991..597d73a16 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -3,7 +3,6 @@ from copy import deepcopy import django_tables2 as tables from django.contrib.auth.models import AnonymousUser from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist from django.db.models.fields.related import RelatedField from django.urls import reverse @@ -12,6 +11,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from django_tables2.data import TableQuerysetData +from core.models import ObjectType from extras.choices import * from extras.models import CustomField, CustomLink from netbox.registry import registry @@ -201,14 +201,14 @@ class NetBoxTable(BaseTable): ]) # Add custom field & custom link columns - content_type = ContentType.objects.get_for_model(self._meta.model) + object_type = ObjectType.objects.get_for_model(self._meta.model) custom_fields = CustomField.objects.filter( - content_types=content_type + object_types=object_type ).exclude(ui_visible=CustomFieldUIVisibleChoices.HIDDEN) extra_columns.extend([ (f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields ]) - custom_links = CustomLink.objects.filter(content_types=content_type, enabled=True) + custom_links = CustomLink.objects.filter(content_types=object_type, enabled=True) extra_columns.extend([ (f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in custom_links ]) diff --git a/netbox/utilities/testing/base.py b/netbox/utilities/testing/base.py index aa2093a9a..f16c2fbbd 100644 --- a/netbox/utilities/testing/base.py +++ b/netbox/utilities/testing/base.py @@ -10,6 +10,7 @@ from django.test import Client, TestCase as _TestCase from netaddr import IPNetwork from taggit.managers import TaggableManager +from core.models import ObjectType from users.models import ObjectPermission from utilities.permissions import resolve_permission_ct from utilities.utils import content_type_identifier @@ -112,7 +113,7 @@ class ModelTestCase(TestCase): # Handle ManyToManyFields if value and type(field) in (ManyToManyField, TaggableManager): - if field.related_model is ContentType and api: + if field.related_model in (ContentType, ObjectType) and api: model_dict[key] = sorted([content_type_identifier(ct) for ct in value]) else: model_dict[key] = sorted([obj.pk for obj in value]) @@ -120,8 +121,8 @@ class ModelTestCase(TestCase): elif api: # Replace ContentType numeric IDs with . - if type(getattr(instance, key)) is ContentType: - ct = ContentType.objects.get(pk=value) + if type(getattr(instance, key)) in (ContentType, ObjectType): + ct = ObjectType.objects.get(pk=value) model_dict[key] = content_type_identifier(ct) # Convert IPNetwork instances to strings diff --git a/netbox/utilities/tests/test_api.py b/netbox/utilities/tests/test_api.py index 1cc3487b1..81be70a34 100644 --- a/netbox/utilities/tests/test_api.py +++ b/netbox/utilities/tests/test_api.py @@ -1,10 +1,8 @@ -import urllib.parse - -from django.contrib.contenttypes.models import ContentType from django.test import Client, TestCase, override_settings from django.urls import reverse from rest_framework import status +from core.models import ObjectType from dcim.models import Region, Site from extras.choices import CustomFieldTypeChoices from extras.models import CustomField @@ -240,10 +238,10 @@ class APIDocsTestCase(TestCase): self.client = Client() # Populate a CustomField to activate CustomFieldSerializer - content_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) self.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='test') self.cf_text.save() - self.cf_text.content_types.set([content_type]) + self.cf_text.object_types.set([object_type]) self.cf_text.save() def test_api_docs(self): From 54b9d1b3f235b8212c73717173e1e7aa7e638b6e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 14:37:32 -0500 Subject: [PATCH 03/25] Disconnect search backend during test to avoid discrepancy with ContentTypes on transaction rollback --- netbox/netbox/tests/test_staging.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/netbox/netbox/tests/test_staging.py b/netbox/netbox/tests/test_staging.py index ed3a69f10..0a73b2987 100644 --- a/netbox/netbox/tests/test_staging.py +++ b/netbox/netbox/tests/test_staging.py @@ -1,9 +1,11 @@ +from django.db.models.signals import post_save from django.test import TransactionTestCase from circuits.models import Provider, Circuit, CircuitType from extras.choices import ChangeActionChoices from extras.models import Branch, StagedChange, Tag from ipam.models import ASN, RIR +from netbox.search.backends import search_backend from netbox.staging import checkout from utilities.testing import create_tags @@ -11,6 +13,10 @@ from utilities.testing import create_tags class StagingTestCase(TransactionTestCase): def setUp(self): + # Disconnect search backend to avoid issues with cached ObjectTypes being deleted + # from the database upon transaction rollback + post_save.disconnect(search_backend.caching_handler) + create_tags('Alpha', 'Bravo', 'Charlie') rir = RIR.objects.create(name='RIR 1', slug='rir-1') From ba514aceac778122049998c113e912d83b359d9b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 15:04:02 -0500 Subject: [PATCH 04/25] Rename CustomLink.content_types to object_types & use ObjectType proxy --- netbox/extras/api/serializers.py | 4 ++-- netbox/extras/filtersets.py | 8 ++++---- netbox/extras/forms/bulk_import.py | 6 +++--- netbox/extras/forms/filtersets.py | 6 +++--- netbox/extras/forms/model_forms.py | 6 +++--- netbox/extras/graphql/types.py | 2 +- .../migrations/0111_rename_content_types.py | 13 +++++++++++++ netbox/extras/models/models.py | 4 ++-- netbox/extras/tables/tables.py | 16 ++++++++-------- netbox/extras/templatetags/custom_links.py | 6 +++--- netbox/extras/tests/test_api.py | 10 +++++----- netbox/extras/tests/test_filtersets.py | 10 +++++----- netbox/extras/tests/test_views.py | 10 +++++----- netbox/netbox/tables/tables.py | 2 +- netbox/templates/extras/customfield.html | 2 +- netbox/templates/extras/customlink.html | 2 +- 16 files changed, 60 insertions(+), 47 deletions(-) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 370450712..fe66e2035 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -196,7 +196,7 @@ class CustomFieldChoiceSetSerializer(ValidatedModelSerializer): class CustomLinkSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail') - content_types = ContentTypeField( + object_types = ContentTypeField( queryset=ObjectType.objects.with_feature('custom_links'), many=True ) @@ -204,7 +204,7 @@ class CustomLinkSerializer(ValidatedModelSerializer): class Meta: model = CustomLink fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', + 'id', 'url', 'display', 'object_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'button_class', 'new_window', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name') diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 290670f5c..dd378075a 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -190,15 +190,15 @@ class CustomLinkFilterSet(BaseFilterSet): method='search', label=_('Search'), ) - content_type_id = MultiValueNumberFilter( - field_name='content_types__id' + object_types_id = MultiValueNumberFilter( + field_name='object_types__id' ) - content_types = ContentTypeFilter() + object_types = ContentTypeFilter() class Meta: model = CustomLink fields = [ - 'id', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window', + 'id', 'object_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window', ] def search(self, queryset, name, value): diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 1d1b27617..abe35e30c 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -111,8 +111,8 @@ class CustomFieldChoiceSetImportForm(CSVModelForm): class CustomLinkImportForm(CSVModelForm): - content_types = CSVMultipleContentTypeField( - label=_('Content types'), + object_types = CSVMultipleContentTypeField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_links'), help_text=_("One or more assigned object types") ) @@ -120,7 +120,7 @@ class CustomLinkImportForm(CSVModelForm): class Meta: model = CustomLink fields = ( - 'name', 'content_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', + 'name', 'object_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', 'link_url', ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index ccfeb8c1d..208f28725 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -108,10 +108,10 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm): class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), - (_('Attributes'), ('content_types', 'enabled', 'new_window', 'weight')), + (_('Attributes'), ('object_types', 'enabled', 'new_window', 'weight')), ) - content_types = ContentTypeMultipleChoiceField( - label=_('Content types'), + object_types = ContentTypeMultipleChoiceField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_links'), required=False ) diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 776265878..70f533013 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -122,13 +122,13 @@ class CustomFieldChoiceSetForm(forms.ModelForm): class CustomLinkForm(forms.ModelForm): - content_types = ContentTypeMultipleChoiceField( - label=_('Content types'), + object_types = ContentTypeMultipleChoiceField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_links') ) fieldsets = ( - (_('Custom Link'), ('name', 'content_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')), + (_('Custom Link'), ('name', 'object_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')), (_('Templates'), ('link_text', 'link_url')), ) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 643568eaa..e4cfd4be7 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -55,7 +55,7 @@ class CustomLinkType(ObjectType): class Meta: model = models.CustomLink - exclude = ('content_types', ) + exclude = ('object_types', ) filterset_class = filtersets.CustomLinkFilterSet diff --git a/netbox/extras/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py index 2b7b2358a..dc3ec9fe6 100644 --- a/netbox/extras/migrations/0111_rename_content_types.py +++ b/netbox/extras/migrations/0111_rename_content_types.py @@ -10,6 +10,7 @@ class Migration(migrations.Migration): ] operations = [ + # Custom fields migrations.RenameField( model_name='customfield', old_name='content_types', @@ -25,4 +26,16 @@ class Migration(migrations.Migration): name='object_type', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.objecttype'), ), + + # Custom links + migrations.RenameField( + model_name='customlink', + old_name='content_types', + new_name='object_types', + ), + migrations.AlterField( + model_name='customlink', + name='object_types', + field=models.ManyToManyField(related_name='custom_links', to='core.objecttype'), + ), ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 6b5a7c150..7439546fb 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -313,8 +313,8 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template code to be rendered with an object as context. """ - content_types = models.ManyToManyField( - to='contenttypes.ContentType', + object_types = models.ManyToManyField( + to='core.ObjectType', related_name='custom_links', help_text=_('The object type(s) to which this link applies.') ) diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 8482c5e24..7adbae178 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -40,8 +40,8 @@ class CustomFieldTable(NetBoxTable): verbose_name=_('Name'), linkify=True ) - content_types = columns.ContentTypesColumn( - verbose_name=_('Content Types') + object_types = columns.ContentTypesColumn( + verbose_name=_('Object Types') ) required = columns.BooleanColumn( verbose_name=_('Required') @@ -71,11 +71,11 @@ class CustomFieldTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = CustomField fields = ( - 'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description', + 'pk', 'id', 'name', 'object_types', 'label', 'type', 'group_name', 'required', 'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'weight', 'choice_set', 'choices', 'created', 'last_updated', ) - default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description') + default_columns = ('pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'description') class CustomFieldChoiceSetTable(NetBoxTable): @@ -115,8 +115,8 @@ class CustomLinkTable(NetBoxTable): verbose_name=_('Name'), linkify=True ) - content_types = columns.ContentTypesColumn( - verbose_name=_('Content Types'), + object_types = columns.ContentTypesColumn( + verbose_name=_('Object Types'), ) enabled = columns.BooleanColumn( verbose_name=_('Enabled'), @@ -128,10 +128,10 @@ class CustomLinkTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = CustomLink fields = ( - 'pk', 'id', 'name', 'content_types', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', + 'pk', 'id', 'name', 'object_types', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'button_class', 'new_window', 'created', 'last_updated', ) - default_columns = ('pk', 'name', 'content_types', 'enabled', 'group_name', 'button_class', 'new_window') + default_columns = ('pk', 'name', 'object_types', 'enabled', 'group_name', 'button_class', 'new_window') class ExportTemplateTable(NetBoxTable): diff --git a/netbox/extras/templatetags/custom_links.py b/netbox/extras/templatetags/custom_links.py index 5de95b607..31cd22815 100644 --- a/netbox/extras/templatetags/custom_links.py +++ b/netbox/extras/templatetags/custom_links.py @@ -1,7 +1,7 @@ from django import template -from django.contrib.contenttypes.models import ContentType from django.utils.safestring import mark_safe +from core.models import ObjectType from extras.models import CustomLink @@ -32,8 +32,8 @@ def custom_links(context, obj): """ Render all applicable links for the given object. """ - content_type = ContentType.objects.get_for_model(obj) - custom_links = CustomLink.objects.filter(content_types=content_type, enabled=True) + object_type = ObjectType.objects.get_for_model(obj) + custom_links = CustomLink.objects.filter(object_types=object_type, enabled=True) if not custom_links: return '' diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 01bbb312f..ae592b528 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -273,21 +273,21 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase): brief_fields = ['display', 'id', 'name', 'url'] create_data = [ { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'Custom Link 4', 'enabled': True, 'link_text': 'Link 4', 'link_url': 'http://example.com/?4', }, { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'Custom Link 5', 'enabled': True, 'link_text': 'Link 5', 'link_url': 'http://example.com/?5', }, { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'Custom Link 6', 'enabled': False, 'link_text': 'Link 6', @@ -301,7 +301,7 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) custom_links = ( CustomLink( @@ -325,7 +325,7 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase): ) CustomLink.objects.bulk_create(custom_links) for i, custom_link in enumerate(custom_links): - custom_link.content_types.set([site_ct]) + custom_link.object_types.set([site_type]) class SavedFilterTest(APIViewTestCases.APIViewTestCase): diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 25263fe68..a23e11288 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -397,7 +397,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests): @classmethod def setUpTestData(cls): - content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device']) + object_types = ObjectType.objects.filter(model__in=['site', 'rack', 'device']) custom_links = ( CustomLink( @@ -427,7 +427,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests): ) CustomLink.objects.bulk_create(custom_links) for i, custom_link in enumerate(custom_links): - custom_link.content_types.set([content_types[i]]) + custom_link.object_types.set([object_types[i]]) def test_q(self): params = {'q': 'Custom Link 1'} @@ -437,10 +437,10 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests): params = {'name': ['Custom Link 1', 'Custom Link 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_content_types(self): - params = {'content_types': 'dcim.site'} + def test_object_types(self): + params = {'object_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]} + params = {'object_types_id': [ContentType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_weight(self): diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 699388c53..890cd59de 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -138,7 +138,7 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) custom_links = ( CustomLink(name='Custom Link 1', enabled=True, link_text='Link 1', link_url='http://example.com/?1'), CustomLink(name='Custom Link 2', enabled=True, link_text='Link 2', link_url='http://example.com/?2'), @@ -146,11 +146,11 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) CustomLink.objects.bulk_create(custom_links) for i, custom_link in enumerate(custom_links): - custom_link.content_types.set([site_ct]) + custom_link.object_types.set([site_type]) cls.form_data = { 'name': 'Custom Link X', - 'content_types': [site_ct.pk], + 'object_types': [site_type.pk], 'enabled': False, 'weight': 100, 'button_class': CustomLinkButtonClassChoices.DEFAULT, @@ -159,7 +159,7 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "name,content_types,enabled,weight,button_class,link_text,link_url", + "name,object_types,enabled,weight,button_class,link_text,link_url", "Custom Link 4,dcim.site,True,100,blue,Link 4,http://exmaple.com/?4", "Custom Link 5,dcim.site,True,100,blue,Link 5,http://exmaple.com/?5", "Custom Link 6,dcim.site,False,100,blue,Link 6,http://exmaple.com/?6", @@ -652,7 +652,7 @@ class CustomLinkTest(TestCase): new_window=False ) customlink.save() - customlink.content_types.set([ContentType.objects.get_for_model(Site)]) + customlink.object_types.set([ObjectType.objects.get_for_model(Site)]) site = Site(name='Test Site', slug='test-site') site.save() diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 597d73a16..afef74752 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -208,7 +208,7 @@ class NetBoxTable(BaseTable): extra_columns.extend([ (f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields ]) - custom_links = CustomLink.objects.filter(content_types=object_type, enabled=True) + custom_links = CustomLink.objects.filter(object_types=object_type, enabled=True) extra_columns.extend([ (f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in custom_links ]) diff --git a/netbox/templates/extras/customfield.html b/netbox/templates/extras/customfield.html index ca6988152..ddc6b30f4 100644 --- a/netbox/templates/extras/customfield.html +++ b/netbox/templates/extras/customfield.html @@ -89,7 +89,7 @@
{% trans "Object Types" %}
- {% for ct in object.content_types.all %} + {% for ct in object.object_types.all %} diff --git a/netbox/templates/extras/customlink.html b/netbox/templates/extras/customlink.html index 492408396..0b9b068da 100644 --- a/netbox/templates/extras/customlink.html +++ b/netbox/templates/extras/customlink.html @@ -38,7 +38,7 @@
{% trans "Assigned Models" %}
{{ ct }}
- {% for ct in object.content_types.all %} + {% for ct in object.object_types.all %} From e51d71d7e6dca05fc5d10eca4c59366263e13179 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 15:31:03 -0500 Subject: [PATCH 05/25] Rename EventRule.content_types to object_types & use ObjectType proxy --- netbox/extras/api/serializers.py | 4 ++-- netbox/extras/events.py | 2 +- netbox/extras/filtersets.py | 6 ++--- netbox/extras/forms/bulk_import.py | 6 ++--- netbox/extras/forms/filtersets.py | 4 ++-- netbox/extras/forms/model_forms.py | 8 +++---- netbox/extras/graphql/types.py | 16 ++++++------- .../migrations/0111_rename_content_types.py | 12 ++++++++++ netbox/extras/models/models.py | 6 ++--- netbox/extras/tables/tables.py | 6 ++--- netbox/extras/tests/test_api.py | 6 ++--- netbox/extras/tests/test_event_rules.py | 23 ++++++++++--------- netbox/extras/tests/test_filtersets.py | 18 +++++++-------- netbox/extras/tests/test_views.py | 8 +++---- netbox/templates/extras/eventrule.html | 4 ++-- 15 files changed, 71 insertions(+), 58 deletions(-) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index fe66e2035..32b708fcc 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -59,7 +59,7 @@ __all__ = ( class EventRuleSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail') - content_types = ContentTypeField( + object_types = ContentTypeField( queryset=ObjectType.objects.with_feature('event_rules'), many=True ) @@ -72,7 +72,7 @@ class EventRuleSerializer(NetBoxModelSerializer): class Meta: model = EventRule fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', + 'id', 'url', 'display', 'object_types', 'name', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type', 'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated', ] diff --git a/netbox/extras/events.py b/netbox/extras/events.py index e7706ea9f..0ee4cffa8 100644 --- a/netbox/extras/events.py +++ b/netbox/extras/events.py @@ -155,7 +155,7 @@ def process_event_queue(events): if content_type not in events_cache[action_flag]: events_cache[action_flag][content_type] = EventRule.objects.filter( **{action_flag: True}, - content_types=content_type, + object_types=content_type, enabled=True ) event_rules = events_cache[action_flag][content_type] diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index dd378075a..d03d134be 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -89,10 +89,10 @@ class EventRuleFilterSet(NetBoxModelFilterSet): method='search', label=_('Search'), ) - content_type_id = MultiValueNumberFilter( - field_name='content_types__id' + object_types_id = MultiValueNumberFilter( + field_name='object_types__id' ) - content_types = ContentTypeFilter() + object_types = ContentTypeFilter() action_type = django_filters.MultipleChoiceFilter( choices=EventRuleActionChoices ) diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index abe35e30c..cbb7b4e44 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -173,8 +173,8 @@ class WebhookImportForm(NetBoxModelImportForm): class EventRuleImportForm(NetBoxModelImportForm): - content_types = CSVMultipleContentTypeField( - label=_('Content types'), + object_types = CSVMultipleContentTypeField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('event_rules'), help_text=_("One or more assigned object types") ) @@ -187,7 +187,7 @@ class EventRuleImportForm(NetBoxModelImportForm): class Meta: model = EventRule fields = ( - 'name', 'description', 'enabled', 'conditions', 'content_types', 'type_create', 'type_update', + 'name', 'description', 'enabled', 'conditions', 'object_types', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'action_type', 'action_object', 'comments', 'tags' ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 208f28725..f19695f57 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -250,10 +250,10 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm): fieldsets = ( (None, ('q', 'filter_id', 'tag')), - (_('Attributes'), ('content_type_id', 'action_type', 'enabled')), + (_('Attributes'), ('object_types_id', 'action_type', 'enabled')), (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')), ) - content_type_id = ContentTypeMultipleChoiceField( + object_types_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('event_rules'), required=False, label=_('Object type') diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 70f533013..9423e35e2 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -248,8 +248,8 @@ class WebhookForm(NetBoxModelForm): class EventRuleForm(NetBoxModelForm): - content_types = ContentTypeMultipleChoiceField( - label=_('Content types'), + object_types = ContentTypeMultipleChoiceField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('event_rules'), ) action_choice = forms.ChoiceField( @@ -266,7 +266,7 @@ class EventRuleForm(NetBoxModelForm): ) fieldsets = ( - (_('Event Rule'), ('name', 'description', 'content_types', 'enabled', 'tags')), + (_('Event Rule'), ('name', 'description', 'object_types', 'enabled', 'tags')), (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')), (_('Conditions'), ('conditions',)), (_('Action'), ( @@ -277,7 +277,7 @@ class EventRuleForm(NetBoxModelForm): class Meta: model = EventRule fields = ( - 'content_types', 'name', 'description', 'type_create', 'type_update', 'type_delete', 'type_job_start', + 'object_types', 'name', 'description', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type', 'action_object_id', 'action_data', 'comments', 'tags' ) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index e4cfd4be7..15ef76b6b 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -59,6 +59,14 @@ class CustomLinkType(ObjectType): filterset_class = filtersets.CustomLinkFilterSet +class EventRuleType(OrganizationalObjectType): + + class Meta: + model = models.EventRule + exclude = ('object_types',) + filterset_class = filtersets.EventRuleFilterSet + + class ExportTemplateType(ObjectType): class Meta: @@ -112,11 +120,3 @@ class WebhookType(OrganizationalObjectType): class Meta: model = models.Webhook filterset_class = filtersets.WebhookFilterSet - - -class EventRuleType(OrganizationalObjectType): - - class Meta: - model = models.EventRule - exclude = ('content_types', ) - filterset_class = filtersets.EventRuleFilterSet diff --git a/netbox/extras/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py index dc3ec9fe6..f44b01e34 100644 --- a/netbox/extras/migrations/0111_rename_content_types.py +++ b/netbox/extras/migrations/0111_rename_content_types.py @@ -38,4 +38,16 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='custom_links', to='core.objecttype'), ), + + # Event rules + migrations.RenameField( + model_name='eventrule', + old_name='content_types', + new_name='object_types', + ), + migrations.AlterField( + model_name='eventrule', + name='object_types', + field=models.ManyToManyField(related_name='event_rules', to='core.objecttype'), + ), ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 7439546fb..bfc11b7c5 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -43,9 +43,9 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged specific type of object is created, modified, or deleted. The action to be taken might entail transmitting a webhook or executing a custom script. """ - content_types = models.ManyToManyField( - to='contenttypes.ContentType', - related_name='eventrules', + object_types = models.ManyToManyField( + to='core.ObjectType', + related_name='event_rules', verbose_name=_('object types'), help_text=_("The object(s) to which this rule applies.") ) diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 7adbae178..7775916b9 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -281,8 +281,8 @@ class EventRuleTable(NetBoxTable): linkify=True, verbose_name=_('Object'), ) - content_types = columns.ContentTypesColumn( - verbose_name=_('Content Types'), + object_types = columns.ContentTypesColumn( + verbose_name=_('Object Types'), ) enabled = columns.BooleanColumn( verbose_name=_('Enabled'), @@ -309,7 +309,7 @@ class EventRuleTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = EventRule fields = ( - 'pk', 'id', 'name', 'enabled', 'description', 'action_type', 'action_object', 'content_types', + 'pk', 'id', 'name', 'enabled', 'description', 'action_type', 'action_object', 'object_types', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'tags', 'created', 'last_updated', ) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index ae592b528..7bed486a0 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -122,7 +122,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase): cls.create_data = [ { 'name': 'EventRule 4', - 'content_types': ['dcim.device', 'dcim.devicetype'], + 'object_types': ['dcim.device', 'dcim.devicetype'], 'type_create': True, 'action_type': EventRuleActionChoices.WEBHOOK, 'action_object_type': 'extras.webhook', @@ -130,7 +130,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase): }, { 'name': 'EventRule 5', - 'content_types': ['dcim.device', 'dcim.devicetype'], + 'object_types': ['dcim.device', 'dcim.devicetype'], 'type_create': True, 'action_type': EventRuleActionChoices.WEBHOOK, 'action_object_type': 'extras.webhook', @@ -138,7 +138,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase): }, { 'name': 'EventRule 6', - 'content_types': ['dcim.device', 'dcim.devicetype'], + 'object_types': ['dcim.device', 'dcim.devicetype'], 'type_create': True, 'action_type': EventRuleActionChoices.WEBHOOK, 'action_object_type': 'extras.webhook', diff --git a/netbox/extras/tests/test_event_rules.py b/netbox/extras/tests/test_event_rules.py index 549c33478..8cea2078a 100644 --- a/netbox/extras/tests/test_event_rules.py +++ b/netbox/extras/tests/test_event_rules.py @@ -3,17 +3,18 @@ import uuid from unittest.mock import patch import django_rq -from dcim.choices import SiteStatusChoices -from dcim.models import Site -from django.contrib.contenttypes.models import ContentType from django.http import HttpResponse from django.urls import reverse +from requests import Session +from rest_framework import status + +from core.models import ObjectType +from dcim.choices import SiteStatusChoices +from dcim.models import Site from extras.choices import EventRuleActionChoices, ObjectChangeActionChoices from extras.events import enqueue_object, flush_events, serialize_for_event from extras.models import EventRule, Tag, Webhook from extras.webhooks import generate_signature, send_webhook -from requests import Session -from rest_framework import status from utilities.testing import APITestCase @@ -29,7 +30,7 @@ class EventRuleTest(APITestCase): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) DUMMY_URL = 'http://localhost:9000/' DUMMY_SECRET = 'LOOKATMEIMASECRETSTRING' @@ -39,32 +40,32 @@ class EventRuleTest(APITestCase): Webhook(name='Webhook 3', payload_url=DUMMY_URL, secret=DUMMY_SECRET), )) - ct = ContentType.objects.get(app_label='extras', model='webhook') + webhook_type = ObjectType.objects.get(app_label='extras', model='webhook') event_rules = EventRule.objects.bulk_create(( EventRule( name='Webhook Event 1', type_create=True, action_type=EventRuleActionChoices.WEBHOOK, - action_object_type=ct, + action_object_type=webhook_type, action_object_id=webhooks[0].id ), EventRule( name='Webhook Event 2', type_update=True, action_type=EventRuleActionChoices.WEBHOOK, - action_object_type=ct, + action_object_type=webhook_type, action_object_id=webhooks[0].id ), EventRule( name='Webhook Event 3', type_delete=True, action_type=EventRuleActionChoices.WEBHOOK, - action_object_type=ct, + action_object_type=webhook_type, action_object_id=webhooks[0].id ), )) for event_rule in event_rules: - event_rule.content_types.set([site_ct]) + event_rule.object_types.set([site_type]) Tag.objects.bulk_create(( Tag(name='Foo', slug='foo'), diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index a23e11288..43ee605fb 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -241,7 +241,7 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests): @classmethod def setUpTestData(cls): - content_types = ContentType.objects.filter( + object_types = ObjectType.objects.filter( model__in=['region', 'site', 'rack', 'location', 'device'] ) @@ -334,11 +334,11 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests): ), ) EventRule.objects.bulk_create(event_rules) - event_rules[0].content_types.add(content_types[0]) - event_rules[1].content_types.add(content_types[1]) - event_rules[2].content_types.add(content_types[2]) - event_rules[3].content_types.add(content_types[3]) - event_rules[4].content_types.add(content_types[4]) + event_rules[0].object_types.add(object_types[0]) + event_rules[1].object_types.add(object_types[1]) + event_rules[2].object_types.add(object_types[2]) + event_rules[3].object_types.add(object_types[3]) + event_rules[4].object_types.add(object_types[4]) def test_q(self): params = {'q': 'foobar1'} @@ -352,10 +352,10 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_content_types(self): - params = {'content_types': 'dcim.region'} + def test_object_types(self): + params = {'object_types': 'dcim.region'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'content_type_id': [ContentType.objects.get_for_model(Region).pk]} + params = {'object_types_id': [ContentType.objects.get_for_model(Region).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_action_type(self): diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 890cd59de..ba7cd0818 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -397,7 +397,7 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase): for webhook in webhooks: webhook.save() - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) event_rules = ( EventRule(name='EventRule 1', type_create=True, action_object=webhooks[0]), EventRule(name='EventRule 2', type_create=True, action_object=webhooks[1]), @@ -405,12 +405,12 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) for event in event_rules: event.save() - event.content_types.add(site_ct) + event.object_types.add(site_type) webhook_ct = ContentType.objects.get_for_model(Webhook) cls.form_data = { 'name': 'Event X', - 'content_types': [site_ct.pk], + 'object_types': [site_type.pk], 'type_create': False, 'type_update': True, 'type_delete': True, @@ -423,7 +423,7 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "name,content_types,type_create,action_type,action_object", + "name,object_types,type_create,action_type,action_object", "Webhook 4,dcim.site,True,webhook,Webhook 1", ) diff --git a/netbox/templates/extras/eventrule.html b/netbox/templates/extras/eventrule.html index d3c483819..844fbf9c6 100644 --- a/netbox/templates/extras/eventrule.html +++ b/netbox/templates/extras/eventrule.html @@ -26,9 +26,9 @@
{% trans "Object Types" %}
{{ ct }}
- {% for ct in object.content_types.all %} + {% for object_type in object.object_types.all %} - + {% endfor %}
{{ ct }}{{ object_type }}
From bef17e5a9511fce2646d669be7f4ce6f583e7319 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 15:54:21 -0500 Subject: [PATCH 06/25] Rename ExportTemplate.content_types to object_types & use ObjectType proxy --- netbox/extras/api/serializers.py | 4 ++-- netbox/extras/filtersets.py | 8 ++++---- netbox/extras/forms/bulk_import.py | 6 +++--- netbox/extras/forms/filtersets.py | 4 ++-- netbox/extras/forms/model_forms.py | 6 +++--- netbox/extras/graphql/types.py | 2 +- .../extras/migrations/0111_rename_content_types.py | 12 ++++++++++++ netbox/extras/models/models.py | 4 ++-- netbox/extras/tables/tables.py | 8 ++++---- netbox/extras/tests/test_api.py | 8 ++++---- netbox/extras/tests/test_filtersets.py | 10 +++++----- netbox/extras/tests/test_views.py | 8 ++++---- netbox/templates/extras/exporttemplate.html | 4 ++-- netbox/utilities/templates/buttons/export.html | 2 +- netbox/utilities/templatetags/buttons.py | 9 +++++---- 15 files changed, 54 insertions(+), 41 deletions(-) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 32b708fcc..3871fcc70 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -216,7 +216,7 @@ class CustomLinkSerializer(ValidatedModelSerializer): class ExportTemplateSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail') - content_types = ContentTypeField( + object_types = ContentTypeField( queryset=ObjectType.objects.with_feature('export_templates'), many=True ) @@ -230,7 +230,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer): class Meta: model = ExportTemplate fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'description', 'template_code', 'mime_type', + 'id', 'url', 'display', 'object_types', 'name', 'description', 'template_code', 'mime_type', 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created', 'last_updated', ] diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index d03d134be..83978806a 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -217,10 +217,10 @@ class ExportTemplateFilterSet(BaseFilterSet): method='search', label=_('Search'), ) - content_type_id = MultiValueNumberFilter( - field_name='content_types__id' + object_types_id = MultiValueNumberFilter( + field_name='object_types__id' ) - content_types = ContentTypeFilter() + object_types = ContentTypeFilter() data_source_id = django_filters.ModelMultipleChoiceFilter( queryset=DataSource.objects.all(), label=_('Data source (ID)'), @@ -232,7 +232,7 @@ class ExportTemplateFilterSet(BaseFilterSet): class Meta: model = ExportTemplate - fields = ['id', 'content_types', 'name', 'description', 'data_synced'] + fields = ['id', 'object_types', 'name', 'description', 'data_synced'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index cbb7b4e44..9c68c7ba3 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -126,8 +126,8 @@ class CustomLinkImportForm(CSVModelForm): class ExportTemplateImportForm(CSVModelForm): - content_types = CSVMultipleContentTypeField( - label=_('Content types'), + object_types = CSVMultipleContentTypeField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('export_templates'), help_text=_("One or more assigned object types") ) @@ -135,7 +135,7 @@ class ExportTemplateImportForm(CSVModelForm): class Meta: model = ExportTemplate fields = ( - 'name', 'content_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code', + 'name', 'object_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code', ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index f19695f57..daad010c1 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -139,7 +139,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), (_('Data'), ('data_source_id', 'data_file_id')), - (_('Attributes'), ('content_type_id', 'mime_type', 'file_extension', 'as_attachment')), + (_('Attributes'), ('object_types_id', 'mime_type', 'file_extension', 'as_attachment')), ) data_source_id = DynamicModelMultipleChoiceField( queryset=DataSource.objects.all(), @@ -154,7 +154,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): 'source_id': '$data_source_id' } ) - content_type_id = ContentTypeMultipleChoiceField( + object_types_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('export_templates'), required=False, label=_('Content types') diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 9423e35e2..95af8fb3b 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -151,8 +151,8 @@ class CustomLinkForm(forms.ModelForm): class ExportTemplateForm(SyncedDataMixin, forms.ModelForm): - content_types = ContentTypeMultipleChoiceField( - label=_('Content types'), + object_types = ContentTypeMultipleChoiceField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('export_templates') ) template_code = forms.CharField( @@ -162,7 +162,7 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm): ) fieldsets = ( - (_('Export Template'), ('name', 'content_types', 'description', 'template_code')), + (_('Export Template'), ('name', 'object_types', 'description', 'template_code')), (_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')), (_('Rendering'), ('mime_type', 'file_extension', 'as_attachment')), ) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 15ef76b6b..1000925bb 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -71,7 +71,7 @@ class ExportTemplateType(ObjectType): class Meta: model = models.ExportTemplate - exclude = ('content_types', ) + exclude = ('object_types', ) filterset_class = filtersets.ExportTemplateFilterSet diff --git a/netbox/extras/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py index f44b01e34..87f721589 100644 --- a/netbox/extras/migrations/0111_rename_content_types.py +++ b/netbox/extras/migrations/0111_rename_content_types.py @@ -50,4 +50,16 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='event_rules', to='core.objecttype'), ), + + # Export templates + migrations.RenameField( + model_name='exporttemplate', + old_name='content_types', + new_name='object_types', + ), + migrations.AlterField( + model_name='exporttemplate', + name='object_types', + field=models.ManyToManyField(related_name='export_templates', to='core.objecttype'), + ), ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index bfc11b7c5..77e703dc0 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -409,8 +409,8 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): - content_types = models.ManyToManyField( - to='contenttypes.ContentType', + object_types = models.ManyToManyField( + to='core.ObjectType', related_name='export_templates', help_text=_('The object type(s) to which this template applies.') ) diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 7775916b9..abc73c5ba 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -139,8 +139,8 @@ class ExportTemplateTable(NetBoxTable): verbose_name=_('Name'), linkify=True ) - content_types = columns.ContentTypesColumn( - verbose_name=_('Content Types'), + object_types = columns.ContentTypesColumn( + verbose_name=_('Object Types'), ) as_attachment = columns.BooleanColumn( verbose_name=_('As Attachment'), @@ -161,11 +161,11 @@ class ExportTemplateTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = ExportTemplate fields = ( - 'pk', 'id', 'name', 'content_types', 'description', 'mime_type', 'file_extension', 'as_attachment', + 'pk', 'id', 'name', 'object_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'data_source', 'data_file', 'data_synced', 'created', 'last_updated', ) default_columns = ( - 'pk', 'name', 'content_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'is_synced', + 'pk', 'name', 'object_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'is_synced', ) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 7bed486a0..34a1bdc40 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -458,17 +458,17 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase): brief_fields = ['description', 'display', 'id', 'name', 'url'] create_data = [ { - 'content_types': ['dcim.device'], + 'object_types': ['dcim.device'], 'name': 'Test Export Template 4', 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}', }, { - 'content_types': ['dcim.device'], + 'object_types': ['dcim.device'], 'name': 'Test Export Template 5', 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}', }, { - 'content_types': ['dcim.device'], + 'object_types': ['dcim.device'], 'name': 'Test Export Template 6', 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}', }, @@ -495,7 +495,7 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase): ) ExportTemplate.objects.bulk_create(export_templates) for et in export_templates: - et.content_types.set([ContentType.objects.get_for_model(Device)]) + et.object_types.set([ObjectType.objects.get_for_model(Device)]) class TagTest(APIViewTestCases.APIViewTestCase): diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 43ee605fb..af079786b 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -639,7 +639,7 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests): @classmethod def setUpTestData(cls): - content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device']) + object_types = ObjectType.objects.filter(model__in=['site', 'rack', 'device']) export_templates = ( ExportTemplate(name='Export Template 1', template_code='TESTING', description='foobar1'), @@ -648,7 +648,7 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests): ) ExportTemplate.objects.bulk_create(export_templates) for i, et in enumerate(export_templates): - et.content_types.set([content_types[i]]) + et.object_types.set([object_types[i]]) def test_q(self): params = {'q': 'foobar1'} @@ -658,10 +658,10 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests): params = {'name': ['Export Template 1', 'Export Template 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_content_types(self): - params = {'content_types': 'dcim.site'} + def test_object_types(self): + params = {'object_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]} + params = {'object_types_id': [ContentType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_description(self): diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index ba7cd0818..02114d035 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -303,7 +303,7 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) TEMPLATE_CODE = """{% for object in queryset %}{{ object }}{% endfor %}""" export_templates = ( @@ -313,16 +313,16 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) ExportTemplate.objects.bulk_create(export_templates) for et in export_templates: - et.content_types.set([site_ct]) + et.object_types.set([site_type]) cls.form_data = { 'name': 'Export Template X', - 'content_types': [site_ct.pk], + 'object_types': [site_type.pk], 'template_code': TEMPLATE_CODE, } cls.csv_data = ( - "name,content_types,template_code", + "name,object_types,template_code", f"Export Template 4,dcim.site,{TEMPLATE_CODE}", f"Export Template 5,dcim.site,{TEMPLATE_CODE}", f"Export Template 6,dcim.site,{TEMPLATE_CODE}", diff --git a/netbox/templates/extras/exporttemplate.html b/netbox/templates/extras/exporttemplate.html index 0648a8191..16dd49ee2 100644 --- a/netbox/templates/extras/exporttemplate.html +++ b/netbox/templates/extras/exporttemplate.html @@ -70,9 +70,9 @@
{% trans "Assigned Models" %}
- {% for ct in object.content_types.all %} + {% for object_type in object.object_types.all %} - + {% endfor %}
{{ ct }}{{ object_type }}
diff --git a/netbox/utilities/templates/buttons/export.html b/netbox/utilities/templates/buttons/export.html index d24be88f7..2085356fa 100644 --- a/netbox/utilities/templates/buttons/export.html +++ b/netbox/utilities/templates/buttons/export.html @@ -25,7 +25,7 @@
  • - {% trans "Add export template" %}... + {% trans "Add export template" %}...
  • {% endif %} diff --git a/netbox/utilities/templatetags/buttons.py b/netbox/utilities/templatetags/buttons.py index 828af3b43..c0870d585 100644 --- a/netbox/utilities/templatetags/buttons.py +++ b/netbox/utilities/templatetags/buttons.py @@ -2,6 +2,7 @@ from django import template from django.contrib.contenttypes.models import ContentType from django.urls import NoReverseMatch, reverse +from core.models import ObjectType from extras.models import Bookmark, ExportTemplate from utilities.utils import get_viewname, prepare_cloned_fields @@ -132,18 +133,18 @@ def import_button(model, action='import'): @register.inclusion_tag('buttons/export.html', takes_context=True) def export_button(context, model): - content_type = ContentType.objects.get_for_model(model) + object_type = ObjectType.objects.get_for_model(model) user = context['request'].user # Determine if the "all data" export returns CSV or YAML - data_format = 'YAML' if hasattr(content_type.model_class(), 'to_yaml') else 'CSV' + data_format = 'YAML' if hasattr(object_type.model_class(), 'to_yaml') else 'CSV' # Retrieve all export templates for this model - export_templates = ExportTemplate.objects.restrict(user, 'view').filter(content_types=content_type) + export_templates = ExportTemplate.objects.restrict(user, 'view').filter(object_types=object_type) return { 'perms': context['perms'], - 'content_type': content_type, + 'object_type': object_type, 'url_params': context['request'].GET.urlencode() if context['request'].GET else '', 'export_templates': export_templates, 'data_format': data_format, From ce6b2666a99f11127827dd6c17f5da58ed979c23 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 16:08:01 -0500 Subject: [PATCH 07/25] Rename SavedFilter.content_types to object_types & use ObjectType proxy --- netbox/extras/api/serializers.py | 4 ++-- netbox/extras/filtersets.py | 8 ++++---- netbox/extras/forms/bulk_import.py | 6 +++--- netbox/extras/forms/filtersets.py | 6 +++--- netbox/extras/forms/model_forms.py | 6 +++--- netbox/extras/graphql/types.py | 2 +- .../extras/migrations/0111_rename_content_types.py | 12 ++++++++++++ netbox/extras/models/models.py | 4 ++-- netbox/extras/tables/tables.py | 8 ++++---- netbox/extras/tests/test_api.py | 10 +++++----- netbox/extras/tests/test_filtersets.py | 10 +++++----- netbox/extras/tests/test_forms.py | 2 +- netbox/extras/tests/test_views.py | 8 ++++---- netbox/templates/extras/savedfilter.html | 4 ++-- 14 files changed, 51 insertions(+), 39 deletions(-) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 3871fcc70..d2e296ffa 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -243,7 +243,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer): class SavedFilterSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail') - content_types = ContentTypeField( + object_types = ContentTypeField( queryset=ObjectType.objects.all(), many=True ) @@ -251,7 +251,7 @@ class SavedFilterSerializer(ValidatedModelSerializer): class Meta: model = SavedFilter fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled', + 'id', 'url', 'display', 'object_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled', 'shared', 'parameters', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 83978806a..fb5f972d1 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -248,10 +248,10 @@ class SavedFilterFilterSet(BaseFilterSet): method='search', label=_('Search'), ) - content_type_id = MultiValueNumberFilter( - field_name='content_types__id' + object_types_id = MultiValueNumberFilter( + field_name='object_types__id' ) - content_types = ContentTypeFilter() + object_types = ContentTypeFilter() user_id = django_filters.ModelMultipleChoiceFilter( queryset=get_user_model().objects.all(), label=_('User (ID)'), @@ -268,7 +268,7 @@ class SavedFilterFilterSet(BaseFilterSet): class Meta: model = SavedFilter - fields = ['id', 'content_types', 'name', 'slug', 'description', 'enabled', 'shared', 'weight'] + fields = ['id', 'object_types', 'name', 'slug', 'description', 'enabled', 'shared', 'weight'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 9c68c7ba3..39d2933a7 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -149,8 +149,8 @@ class ConfigTemplateImportForm(CSVModelForm): class SavedFilterImportForm(CSVModelForm): - content_types = CSVMultipleContentTypeField( - label=_('Content types'), + object_types = CSVMultipleContentTypeField( + label=_('Object types'), queryset=ObjectType.objects.all(), help_text=_("One or more assigned object types") ) @@ -158,7 +158,7 @@ class SavedFilterImportForm(CSVModelForm): class Meta: model = SavedFilter fields = ( - 'name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared', 'parameters', + 'name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared', 'parameters', ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index daad010c1..42ba5618c 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -195,10 +195,10 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), - (_('Attributes'), ('content_types', 'enabled', 'shared', 'weight')), + (_('Attributes'), ('object_types', 'enabled', 'shared', 'weight')), ) - content_types = ContentTypeMultipleChoiceField( - label=_('Content types'), + object_types = ContentTypeMultipleChoiceField( + label=_('Object types'), queryset=ObjectType.objects.public(), required=False ) diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 95af8fb3b..7f36db657 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -192,14 +192,14 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm): class SavedFilterForm(forms.ModelForm): slug = SlugField() - content_types = ContentTypeMultipleChoiceField( - label=_('Content types'), + object_types = ContentTypeMultipleChoiceField( + label=_('Object types'), queryset=ObjectType.objects.all() ) parameters = JSONField() fieldsets = ( - (_('Saved Filter'), ('name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared')), + (_('Saved Filter'), ('name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared')), (_('Parameters'), ('parameters',)), ) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 1000925bb..9e0444da6 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -103,7 +103,7 @@ class SavedFilterType(ObjectType): class Meta: model = models.SavedFilter - exclude = ('content_types', ) + exclude = ('object_types', ) filterset_class = filtersets.SavedFilterFilterSet diff --git a/netbox/extras/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py index 87f721589..df347dffd 100644 --- a/netbox/extras/migrations/0111_rename_content_types.py +++ b/netbox/extras/migrations/0111_rename_content_types.py @@ -62,4 +62,16 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='export_templates', to='core.objecttype'), ), + + # Saved filters + migrations.RenameField( + model_name='savedfilter', + old_name='content_types', + new_name='object_types', + ), + migrations.AlterField( + model_name='savedfilter', + name='object_types', + field=models.ManyToManyField(related_name='saved_filters', to='core.objecttype'), + ), ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 77e703dc0..c5e35c9c7 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -518,8 +518,8 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): """ A set of predefined keyword parameters that can be reused to filter for specific objects. """ - content_types = models.ManyToManyField( - to='contenttypes.ContentType', + object_types = models.ManyToManyField( + to='core.ObjectType', related_name='saved_filters', help_text=_('The object type(s) to which this filter applies.') ) diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index abc73c5ba..479cb568e 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -204,8 +204,8 @@ class SavedFilterTable(NetBoxTable): verbose_name=_('Name'), linkify=True ) - content_types = columns.ContentTypesColumn( - verbose_name=_('Content Types'), + object_types = columns.ContentTypesColumn( + verbose_name=_('Object Types'), ) enabled = columns.BooleanColumn( verbose_name=_('Enabled'), @@ -220,11 +220,11 @@ class SavedFilterTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = SavedFilter fields = ( - 'pk', 'id', 'name', 'slug', 'content_types', 'description', 'user', 'weight', 'enabled', 'shared', + 'pk', 'id', 'name', 'slug', 'object_types', 'description', 'user', 'weight', 'enabled', 'shared', 'created', 'last_updated', 'parameters' ) default_columns = ( - 'pk', 'name', 'content_types', 'user', 'description', 'enabled', 'shared', + 'pk', 'name', 'object_types', 'user', 'description', 'enabled', 'shared', ) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 34a1bdc40..eaa031837 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -333,7 +333,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase): brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url'] create_data = [ { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'Saved Filter 4', 'slug': 'saved-filter-4', 'weight': 100, @@ -342,7 +342,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase): 'parameters': {'status': ['active']}, }, { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'Saved Filter 5', 'slug': 'saved-filter-5', 'weight': 200, @@ -351,7 +351,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase): 'parameters': {'status': ['planned']}, }, { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'Saved Filter 6', 'slug': 'saved-filter-6', 'weight': 300, @@ -368,7 +368,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) saved_filters = ( SavedFilter( @@ -398,7 +398,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase): ) SavedFilter.objects.bulk_create(saved_filters) for i, savedfilter in enumerate(saved_filters): - savedfilter.content_types.set([site_ct]) + savedfilter.object_types.set([site_type]) class BookmarkTest( diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index af079786b..a45da8e20 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -466,7 +466,7 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests): @classmethod def setUpTestData(cls): - content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device']) + object_types = ObjectType.objects.filter(model__in=['site', 'rack', 'device']) users = ( User(username='User 1'), @@ -509,7 +509,7 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests): ) SavedFilter.objects.bulk_create(saved_filters) for i, savedfilter in enumerate(saved_filters): - savedfilter.content_types.set([content_types[i]]) + savedfilter.object_types.set([object_types[i]]) def test_q(self): params = {'q': 'foobar1'} @@ -527,10 +527,10 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_content_types(self): - params = {'content_types': 'dcim.site'} + def test_object_types(self): + params = {'object_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]} + params = {'object_types_id': [ContentType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_user(self): diff --git a/netbox/extras/tests/test_forms.py b/netbox/extras/tests/test_forms.py index 5adb57fb2..7642ee2a8 100644 --- a/netbox/extras/tests/test_forms.py +++ b/netbox/extras/tests/test_forms.py @@ -100,7 +100,7 @@ class SavedFilterFormTest(TestCase): form = SavedFilterForm({ 'name': 'test-sf', 'slug': 'test-sf', - 'content_types': [ContentType.objects.get_for_model(Site).pk], + 'object_types': [ContentType.objects.get_for_model(Site).pk], 'weight': 100, 'parameters': { "status": [ diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 02114d035..ca6ad9174 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -184,7 +184,7 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) users = ( User(username='User 1'), @@ -218,12 +218,12 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) SavedFilter.objects.bulk_create(saved_filters) for i, savedfilter in enumerate(saved_filters): - savedfilter.content_types.set([site_ct]) + savedfilter.object_types.set([site_type]) cls.form_data = { 'name': 'Saved Filter X', 'slug': 'saved-filter-x', - 'content_types': [site_ct.pk], + 'object_types': [site_type.pk], 'description': 'Foo', 'weight': 1000, 'enabled': True, @@ -232,7 +232,7 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - 'name,slug,content_types,weight,enabled,shared,parameters', + 'name,slug,object_types,weight,enabled,shared,parameters', 'Saved Filter 4,saved-filter-4,dcim.device,400,True,True,{"foo": "a"}', 'Saved Filter 5,saved-filter-5,dcim.device,500,True,True,{"foo": "b"}', 'Saved Filter 6,saved-filter-6,dcim.device,600,True,True,{"foo": "c"}', diff --git a/netbox/templates/extras/savedfilter.html b/netbox/templates/extras/savedfilter.html index 840852c7f..9b10f1375 100644 --- a/netbox/templates/extras/savedfilter.html +++ b/netbox/templates/extras/savedfilter.html @@ -38,9 +38,9 @@
    {% trans "Assigned Models" %}
    - {% for ct in object.content_types.all %} + {% for object_type in object.object_types.all %} - + {% endfor %}
    {{ ct }}{{ object_type }}
    From e0165539b31119d3ef0f670e7636d7255bfc3a11 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 16:34:52 -0500 Subject: [PATCH 08/25] Rename ImageAttachment.content_type to object_type --- netbox/extras/api/serializers.py | 8 ++++---- netbox/extras/filtersets.py | 4 ++-- netbox/extras/forms/filtersets.py | 6 +++--- .../migrations/0111_rename_content_types.py | 15 +++++++++++++++ netbox/extras/models/models.py | 12 ++++++------ netbox/extras/tables/tables.py | 8 ++++---- netbox/extras/tests/test_api.py | 6 +++--- netbox/extras/tests/test_filtersets.py | 16 ++++++++-------- netbox/extras/utils.py | 2 +- netbox/netbox/models/features.py | 4 +++- netbox/tenancy/forms/bulk_import.py | 4 ++-- 11 files changed, 51 insertions(+), 34 deletions(-) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index d2e296ffa..46189cf4e 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -312,7 +312,7 @@ class TagSerializer(ValidatedModelSerializer): class ImageAttachmentSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail') - content_type = ContentTypeField( + object_type = ContentTypeField( queryset=ObjectType.objects.all() ) parent = serializers.SerializerMethodField(read_only=True) @@ -320,7 +320,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer): class Meta: model = ImageAttachment fields = [ - 'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height', + 'id', 'url', 'display', 'object_type', 'object_id', 'parent', 'name', 'image', 'image_height', 'image_width', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'image') @@ -329,10 +329,10 @@ class ImageAttachmentSerializer(ValidatedModelSerializer): # Validate that the parent object exists try: - data['content_type'].get_object_for_this_type(id=data['object_id']) + data['object_type'].get_object_for_this_type(id=data['object_id']) except ObjectDoesNotExist: raise serializers.ValidationError( - "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id']) + "Invalid parent object: {} ID {}".format(data['object_type'], data['object_id']) ) # Enforce model validation diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index fb5f972d1..797c76b51 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -318,11 +318,11 @@ class ImageAttachmentFilterSet(BaseFilterSet): label=_('Search'), ) created = django_filters.DateTimeFilter() - content_type = ContentTypeFilter() + object_type = ContentTypeFilter() class Meta: model = ImageAttachment - fields = ['id', 'content_type_id', 'object_id', 'name'] + fields = ['id', 'object_type_id', 'object_id', 'name'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 42ba5618c..75724b108 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -179,10 +179,10 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), - (_('Attributes'), ('content_type_id', 'name',)), + (_('Attributes'), ('object_type_id', 'name',)), ) - content_type_id = ContentTypeChoiceField( - label=_('Content type'), + object_type_id = ContentTypeChoiceField( + label=_('Object type'), queryset=ObjectType.objects.with_feature('image_attachments'), required=False ) diff --git a/netbox/extras/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py index df347dffd..3d6529692 100644 --- a/netbox/extras/migrations/0111_rename_content_types.py +++ b/netbox/extras/migrations/0111_rename_content_types.py @@ -74,4 +74,19 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='saved_filters', to='core.objecttype'), ), + + # Image attachments + migrations.RemoveIndex( + model_name='imageattachment', + name='extras_imag_content_94728e_idx', + ), + migrations.RenameField( + model_name='imageattachment', + old_name='content_type', + new_name='object_type', + ), + migrations.AddIndex( + model_name='imageattachment', + index=models.Index(fields=['object_type', 'object_id'], name='extras_imag_object__96bebc_idx'), + ), ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index c5e35c9c7..4a57c6ada 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -598,13 +598,13 @@ class ImageAttachment(ChangeLoggedModel): """ An uploaded image which is associated with an object. """ - content_type = models.ForeignKey( + object_type = models.ForeignKey( to='contenttypes.ContentType', on_delete=models.CASCADE ) object_id = models.PositiveBigIntegerField() parent = GenericForeignKey( - ct_field='content_type', + ct_field='object_type', fk_field='object_id' ) image = models.ImageField( @@ -626,12 +626,12 @@ class ImageAttachment(ChangeLoggedModel): objects = RestrictedQuerySet.as_manager() - clone_fields = ('content_type', 'object_id') + clone_fields = ('object_type', 'object_id') class Meta: ordering = ('name', 'pk') # name may be non-unique indexes = ( - models.Index(fields=('content_type', 'object_id')), + models.Index(fields=('object_type', 'object_id')), ) verbose_name = _('image attachment') verbose_name_plural = _('image attachments') @@ -646,9 +646,9 @@ class ImageAttachment(ChangeLoggedModel): super().clean() # Validate the assigned object type - if self.content_type not in ObjectType.objects.with_feature('image_attachments'): + if self.object_type not in ObjectType.objects.with_feature('image_attachments'): raise ValidationError( - _("Image attachments cannot be assigned to this object type ({type}).").format(type=self.content_type) + _("Image attachments cannot be assigned to this object type ({type}).").format(type=self.object_type) ) def delete(self, *args, **kwargs): diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 479cb568e..5bf4f1892 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -174,8 +174,8 @@ class ImageAttachmentTable(NetBoxTable): verbose_name=_('ID'), linkify=False ) - content_type = columns.ContentTypeColumn( - verbose_name=_('Content Type'), + object_type = columns.ContentTypeColumn( + verbose_name=_('Object Type'), ) parent = tables.Column( verbose_name=_('Parent'), @@ -193,10 +193,10 @@ class ImageAttachmentTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = ImageAttachment fields = ( - 'pk', 'content_type', 'parent', 'image', 'name', 'image_height', 'image_width', 'size', 'created', + 'pk', 'object_type', 'parent', 'image', 'name', 'image_height', 'image_width', 'size', 'created', 'last_updated', ) - default_columns = ('content_type', 'parent', 'image', 'name', 'size', 'created') + default_columns = ('object_type', 'parent', 'image', 'name', 'size', 'created') class SavedFilterTable(NetBoxTable): diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index eaa031837..53d981123 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -548,7 +548,7 @@ class ImageAttachmentTest( image_attachments = ( ImageAttachment( - content_type=ct, + object_type=ct, object_id=site.pk, name='Image Attachment 1', image='http://example.com/image1.png', @@ -556,7 +556,7 @@ class ImageAttachmentTest( image_width=100 ), ImageAttachment( - content_type=ct, + object_type=ct, object_id=site.pk, name='Image Attachment 2', image='http://example.com/image2.png', @@ -564,7 +564,7 @@ class ImageAttachmentTest( image_width=100 ), ImageAttachment( - content_type=ct, + object_type=ct, object_id=site.pk, name='Image Attachment 3', image='http://example.com/image3.png', diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index a45da8e20..762818d6d 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -693,7 +693,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests): image_attachments = ( ImageAttachment( - content_type=site_ct, + object_type=site_ct, object_id=sites[0].pk, name='Image Attachment 1', image='http://example.com/image1.png', @@ -701,7 +701,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests): image_width=100 ), ImageAttachment( - content_type=site_ct, + object_type=site_ct, object_id=sites[1].pk, name='Image Attachment 2', image='http://example.com/image2.png', @@ -709,7 +709,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests): image_width=100 ), ImageAttachment( - content_type=rack_ct, + object_type=rack_ct, object_id=racks[0].pk, name='Image Attachment 3', image='http://example.com/image3.png', @@ -717,7 +717,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests): image_width=100 ), ImageAttachment( - content_type=rack_ct, + object_type=rack_ct, object_id=racks[1].pk, name='Image Attachment 4', image='http://example.com/image4.png', @@ -735,13 +735,13 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests): params = {'name': ['Image Attachment 1', 'Image Attachment 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_content_type(self): - params = {'content_type': 'dcim.site'} + def test_object_type(self): + params = {'object_type': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_content_type_id_and_object_id(self): + def test_object_type_id_and_object_id(self): params = { - 'content_type_id': ContentType.objects.get(app_label='dcim', model='site').pk, + 'object_type_id': ContentType.objects.get(app_label='dcim', model='site').pk, 'object_id': [Site.objects.first().pk], } self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) diff --git a/netbox/extras/utils.py b/netbox/extras/utils.py index 4464af718..e67b9b50c 100644 --- a/netbox/extras/utils.py +++ b/netbox/extras/utils.py @@ -24,7 +24,7 @@ def image_upload(instance, filename): elif instance.name: filename = instance.name - return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename) + return '{}{}_{}_{}'.format(path, instance.object_type.name, instance.object_id, filename) def is_script(obj): diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index fb6cf8498..c8137ec66 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -329,7 +329,9 @@ class ImageAttachmentsMixin(models.Model): Enables the assignments of ImageAttachments. """ images = GenericRelation( - to='extras.ImageAttachment' + to='extras.ImageAttachment', + content_type_field='object_type', + object_id_field='object_id' ) class Meta: diff --git a/netbox/tenancy/forms/bulk_import.py b/netbox/tenancy/forms/bulk_import.py index f38b3293d..f37317549 100644 --- a/netbox/tenancy/forms/bulk_import.py +++ b/netbox/tenancy/forms/bulk_import.py @@ -91,7 +91,7 @@ class ContactImportForm(NetBoxModelImportForm): class ContactAssignmentImportForm(NetBoxModelImportForm): - content_type = CSVContentTypeField( + object_type = CSVContentTypeField( queryset=ContentType.objects.all(), help_text=_("One or more assigned object types") ) @@ -108,4 +108,4 @@ class ContactAssignmentImportForm(NetBoxModelImportForm): class Meta: model = ContactAssignment - fields = ('content_type', 'object_id', 'contact', 'priority', 'role') + fields = ('object_type', 'object_id', 'contact', 'priority', 'role') From 5f43eabab1a5f727fc98ec6ba297b52d09788917 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 16:54:01 -0500 Subject: [PATCH 09/25] Rename ContactAssignment.content_type to object_type --- netbox/netbox/models/features.py | 4 +- netbox/tenancy/api/serializers.py | 6 +-- netbox/tenancy/filtersets.py | 4 +- netbox/tenancy/forms/filtersets.py | 4 +- netbox/tenancy/forms/model_forms.py | 4 +- ...5_contactassignment_rename_content_type.py | 40 +++++++++++++++++++ netbox/tenancy/models/contacts.py | 14 +++---- netbox/tenancy/tables/contacts.py | 6 +-- netbox/tenancy/tests/test_api.py | 6 +-- netbox/tenancy/tests/test_filtersets.py | 4 +- netbox/tenancy/tests/test_views.py | 6 +-- netbox/tenancy/views.py | 8 ++-- 12 files changed, 74 insertions(+), 32 deletions(-) create mode 100644 netbox/tenancy/migrations/0015_contactassignment_rename_content_type.py diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index c8137ec66..74b24aa85 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -343,7 +343,9 @@ class ContactsMixin(models.Model): Enables the assignments of Contacts (via ContactAssignment). """ contacts = GenericRelation( - to='tenancy.ContactAssignment' + to='tenancy.ContactAssignment', + content_type_field='object_type', + object_id_field='object_id' ) class Meta: diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 325d3b439..5e7382b70 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -100,7 +100,7 @@ class ContactSerializer(NetBoxModelSerializer): class ContactAssignmentSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail') - content_type = ContentTypeField( + object_type = ContentTypeField( queryset=ContentType.objects.all() ) object = serializers.SerializerMethodField(read_only=True) @@ -111,13 +111,13 @@ class ContactAssignmentSerializer(NetBoxModelSerializer): class Meta: model = ContactAssignment fields = [ - 'id', 'url', 'display', 'content_type', 'object_id', 'object', 'contact', 'role', 'priority', 'tags', + 'id', 'url', 'display', 'object_type', 'object_id', 'object', 'contact', 'role', 'priority', 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'contact', 'role', 'priority') @extend_schema_field(OpenApiTypes.OBJECT) def get_object(self, instance): - serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) + serializer = get_serializer_for_model(instance.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} return serializer(instance.object, context=context).data diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index 8079b4035..295d20774 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -86,7 +86,7 @@ class ContactAssignmentFilterSet(NetBoxModelFilterSet): method='search', label=_('Search'), ) - content_type = ContentTypeFilter() + object_type = ContentTypeFilter() contact_id = django_filters.ModelMultipleChoiceFilter( queryset=Contact.objects.all(), label=_('Contact (ID)'), @@ -118,7 +118,7 @@ class ContactAssignmentFilterSet(NetBoxModelFilterSet): class Meta: model = ContactAssignment - fields = ['id', 'content_type_id', 'object_id', 'priority', 'tag'] + fields = ['id', 'object_type_id', 'object_id', 'priority', 'tag'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/tenancy/forms/filtersets.py b/netbox/tenancy/forms/filtersets.py index e5f038923..fbd0f2ad0 100644 --- a/netbox/tenancy/forms/filtersets.py +++ b/netbox/tenancy/forms/filtersets.py @@ -83,9 +83,9 @@ class ContactAssignmentFilterForm(NetBoxModelFilterSetForm): model = ContactAssignment fieldsets = ( (None, ('q', 'filter_id', 'tag')), - (_('Assignment'), ('content_type_id', 'group_id', 'contact_id', 'role_id', 'priority')), + (_('Assignment'), ('object_type_id', 'group_id', 'contact_id', 'role_id', 'priority')), ) - content_type_id = ContentTypeMultipleChoiceField( + object_type_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('contacts'), required=False, label=_('Object type') diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py index 9a53eba17..140d9cf9a 100644 --- a/netbox/tenancy/forms/model_forms.py +++ b/netbox/tenancy/forms/model_forms.py @@ -143,9 +143,9 @@ class ContactAssignmentForm(NetBoxModelForm): class Meta: model = ContactAssignment fields = ( - 'content_type', 'object_id', 'group', 'contact', 'role', 'priority', 'tags' + 'object_type', 'object_id', 'group', 'contact', 'role', 'priority', 'tags' ) widgets = { - 'content_type': forms.HiddenInput(), + 'object_type': forms.HiddenInput(), 'object_id': forms.HiddenInput(), } diff --git a/netbox/tenancy/migrations/0015_contactassignment_rename_content_type.py b/netbox/tenancy/migrations/0015_contactassignment_rename_content_type.py new file mode 100644 index 000000000..58b14e10f --- /dev/null +++ b/netbox/tenancy/migrations/0015_contactassignment_rename_content_type.py @@ -0,0 +1,40 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('extras', '0111_rename_content_types'), + ('tenancy', '0014_contactassignment_ordering'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='contactassignment', + name='tenancy_contactassignment_unique_object_contact_role', + ), + migrations.RemoveIndex( + model_name='contactassignment', + name='tenancy_con_content_693ff4_idx', + ), + migrations.RenameField( + model_name='contactassignment', + old_name='content_type', + new_name='object_type', + ), + migrations.AddIndex( + model_name='contactassignment', + index=models.Index( + fields=['object_type', 'object_id'], + name='tenancy_con_object__6f20f7_idx' + ), + ), + migrations.AddConstraint( + model_name='contactassignment', + constraint=models.UniqueConstraint( + fields=('object_type', 'object_id', 'contact', 'role'), + name='tenancy_contactassignment_unique_object_contact_role' + ), + ), + ] diff --git a/netbox/tenancy/models/contacts.py b/netbox/tenancy/models/contacts.py index 1ea62db0c..e31330657 100644 --- a/netbox/tenancy/models/contacts.py +++ b/netbox/tenancy/models/contacts.py @@ -111,13 +111,13 @@ class Contact(PrimaryModel): class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): - content_type = models.ForeignKey( + object_type = models.ForeignKey( to='contenttypes.ContentType', on_delete=models.CASCADE ) object_id = models.PositiveBigIntegerField() object = GenericForeignKey( - ct_field='content_type', + ct_field='object_type', fk_field='object_id' ) contact = models.ForeignKey( @@ -137,16 +137,16 @@ class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, Chan blank=True ) - clone_fields = ('content_type', 'object_id', 'role', 'priority') + clone_fields = ('object_type', 'object_id', 'role', 'priority') class Meta: ordering = ('contact', 'priority', 'role', 'pk') indexes = ( - models.Index(fields=('content_type', 'object_id')), + models.Index(fields=('object_type', 'object_id')), ) constraints = ( models.UniqueConstraint( - fields=('content_type', 'object_id', 'contact', 'role'), + fields=('object_type', 'object_id', 'contact', 'role'), name='%(app_label)s_%(class)s_unique_object_contact_role' ), ) @@ -165,9 +165,9 @@ class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, Chan super().clean() # Validate the assigned object type - if self.content_type not in ObjectType.objects.with_feature('contacts'): + if self.object_type not in ObjectType.objects.with_feature('contacts'): raise ValidationError( - _("Contacts cannot be assigned to this object type ({type}).").format(type=self.content_type) + _("Contacts cannot be assigned to this object type ({type}).").format(type=self.object_type) ) def to_objectchange(self, action): diff --git a/netbox/tenancy/tables/contacts.py b/netbox/tenancy/tables/contacts.py index a22c04569..946058218 100644 --- a/netbox/tenancy/tables/contacts.py +++ b/netbox/tenancy/tables/contacts.py @@ -86,7 +86,7 @@ class ContactTable(NetBoxTable): class ContactAssignmentTable(NetBoxTable): - content_type = columns.ContentTypeColumn( + object_type = columns.ContentTypeColumn( verbose_name=_('Object Type') ) object = tables.Column( @@ -141,10 +141,10 @@ class ContactAssignmentTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = ContactAssignment fields = ( - 'pk', 'content_type', 'object', 'contact', 'role', 'priority', 'contact_title', 'contact_phone', + 'pk', 'object_type', 'object', 'contact', 'role', 'priority', 'contact_title', 'contact_phone', 'contact_email', 'contact_address', 'contact_link', 'contact_description', 'contact_group', 'tags', 'actions' ) default_columns = ( - 'pk', 'content_type', 'object', 'contact', 'role', 'priority', 'contact_email', 'contact_phone' + 'pk', 'object_type', 'object', 'contact', 'role', 'priority', 'contact_email', 'contact_phone' ) diff --git a/netbox/tenancy/tests/test_api.py b/netbox/tenancy/tests/test_api.py index 175bfa947..de6b36fc6 100644 --- a/netbox/tenancy/tests/test_api.py +++ b/netbox/tenancy/tests/test_api.py @@ -246,21 +246,21 @@ class ContactAssignmentTest(APIViewTestCases.APIViewTestCase): cls.create_data = [ { - 'content_type': 'dcim.site', + 'object_type': 'dcim.site', 'object_id': sites[1].pk, 'contact': contacts[3].pk, 'role': contact_roles[0].pk, 'priority': ContactPriorityChoices.PRIORITY_PRIMARY, }, { - 'content_type': 'dcim.site', + 'object_type': 'dcim.site', 'object_id': sites[1].pk, 'contact': contacts[4].pk, 'role': contact_roles[1].pk, 'priority': ContactPriorityChoices.PRIORITY_SECONDARY, }, { - 'content_type': 'dcim.site', + 'object_type': 'dcim.site', 'object_id': sites[1].pk, 'contact': contacts[5].pk, 'role': contact_roles[2].pk, diff --git a/netbox/tenancy/tests/test_filtersets.py b/netbox/tenancy/tests/test_filtersets.py index ab72bd39f..729dd7204 100644 --- a/netbox/tenancy/tests/test_filtersets.py +++ b/netbox/tenancy/tests/test_filtersets.py @@ -295,8 +295,8 @@ class ContactAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests): ) ContactAssignment.objects.bulk_create(assignments) - def test_content_type(self): - params = {'content_type_id': ContentType.objects.get_by_natural_key('dcim', 'site')} + def test_object_type(self): + params = {'object_type_id': ContentType.objects.get_by_natural_key('dcim', 'site')} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) def test_contact(self): diff --git a/netbox/tenancy/tests/test_views.py b/netbox/tenancy/tests/test_views.py index 2151a6e8b..cbdecc0d0 100644 --- a/netbox/tenancy/tests/test_views.py +++ b/netbox/tenancy/tests/test_views.py @@ -292,7 +292,7 @@ class ContactAssignmentTestCase( tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { - 'content_type': ContentType.objects.get_for_model(Site).pk, + 'object_type': ContentType.objects.get_for_model(Site).pk, 'object_id': sites[3].pk, 'contact': contacts[3].pk, 'role': contact_roles[3].pk, @@ -306,11 +306,11 @@ class ContactAssignmentTestCase( } def _get_url(self, action, instance=None): - # Override creation URL to append content_type & object_id parameters + # Override creation URL to append object_type & object_id parameters if action == 'add': url = reverse('tenancy:contactassignment_add') content_type = ContentType.objects.get_for_model(Site).pk object_id = Site.objects.first().pk - return f"{url}?content_type={content_type}&object_id={object_id}" + return f"{url}?object_type={content_type}&object_id={object_id}" return super()._get_url(action, instance=instance) diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 1d2fceb04..4c4d263df 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -23,7 +23,7 @@ class ObjectContactsView(generic.ObjectChildrenView): def get_children(self, request, parent): return ContactAssignment.objects.restrict(request.user, 'view').filter( - content_type=ContentType.objects.get_for_model(parent), + object_type=ContentType.objects.get_for_model(parent), object_id=parent.pk ).order_by('priority', 'contact', 'role') @@ -31,7 +31,7 @@ class ObjectContactsView(generic.ObjectChildrenView): table = super().get_table(*args, **kwargs) # Hide object columns - table.columns.hide('content_type') + table.columns.hide('object_type') table.columns.hide('object') return table @@ -374,8 +374,8 @@ class ContactAssignmentEditView(generic.ObjectEditView): def alter_object(self, instance, request, args, kwargs): if not instance.pk: # Assign the object based on URL kwargs - content_type = get_object_or_404(ContentType, pk=request.GET.get('content_type')) - instance.object = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id')) + object_type = get_object_or_404(ContentType, pk=request.GET.get('object_type')) + instance.object = get_object_or_404(object_type.model_class(), pk=request.GET.get('object_id')) return instance def get_extra_addanother_params(self, request): From 570f64784fd100bedbca9ab8b113677670ab10b0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 09:23:06 -0500 Subject: [PATCH 10/25] Update Tag.object_types to reference ObjectType --- netbox/extras/graphql/types.py | 2 +- .../migrations/0112_tag_update_object_types.py | 17 +++++++++++++++++ netbox/extras/models/tags.py | 2 +- netbox/extras/signals.py | 5 +++-- netbox/extras/tests/test_filtersets.py | 10 +++++----- netbox/extras/tests/test_models.py | 4 ++-- 6 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 netbox/extras/migrations/0112_tag_update_object_types.py diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 9e0444da6..65819a75a 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -111,7 +111,7 @@ class TagType(ObjectType): class Meta: model = models.Tag - exclude = ('extras_taggeditem_items',) + exclude = ('object_types', 'extras_taggeditem_items',) filterset_class = filtersets.TagFilterSet diff --git a/netbox/extras/migrations/0112_tag_update_object_types.py b/netbox/extras/migrations/0112_tag_update_object_types.py new file mode 100644 index 000000000..87ec117a4 --- /dev/null +++ b/netbox/extras/migrations/0112_tag_update_object_types.py @@ -0,0 +1,17 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_gfk_indexes'), + ('extras', '0111_rename_content_types'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='object_types', + field=models.ManyToManyField(blank=True, related_name='+', to='core.objecttype'), + ), + ] diff --git a/netbox/extras/models/tags.py b/netbox/extras/models/tags.py index 3aba6df60..27b05638e 100644 --- a/netbox/extras/models/tags.py +++ b/netbox/extras/models/tags.py @@ -34,7 +34,7 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase): blank=True, ) object_types = models.ManyToManyField( - to='contenttypes.ContentType', + to='core.ObjectType', related_name='+', blank=True, help_text=_("The object type(s) to which this this tag can be applied.") diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index 85c00169c..cacc5a83a 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -8,6 +8,7 @@ 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.models import ObjectType from core.signals import job_end, job_start from extras.constants import EVENT_JOB_END, EVENT_JOB_START from extras.events import process_event_rules @@ -240,8 +241,8 @@ def validate_assigned_tags(sender, instance, action, model, pk_set, **kwargs): """ if action != 'pre_add': return - ct = ContentType.objects.get_for_model(instance) - # Retrieve any applied Tags that are restricted to certain object_types + ct = ObjectType.objects.get_for_model(instance) + # Retrieve any applied Tags that are restricted to certain object types for tag in model.objects.filter(pk__in=pk_set, object_types__isnull=False).prefetch_related('object_types'): if ct not in tag.object_types.all(): raise AbortRequest(f"Tag {tag} cannot be assigned to {ct.model} objects.") diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 762818d6d..7be00a5a3 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -1114,9 +1114,9 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests): @classmethod def setUpTestData(cls): - content_types = { - 'site': ContentType.objects.get_by_natural_key('dcim', 'site'), - 'provider': ContentType.objects.get_by_natural_key('circuits', 'provider'), + object_types = { + 'site': ObjectType.objects.get_by_natural_key('dcim', 'site'), + 'provider': ObjectType.objects.get_by_natural_key('circuits', 'provider'), } tags = ( @@ -1125,8 +1125,8 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests): Tag(name='Tag 3', slug='tag-3', color='0000ff'), ) Tag.objects.bulk_create(tags) - tags[0].object_types.add(content_types['site']) - tags[1].object_types.add(content_types['provider']) + tags[0].object_types.add(object_types['site']) + tags[1].object_types.add(object_types['provider']) # Apply some tags so we can filter by content type site = Site.objects.create(name='Site 1', slug='site-1') diff --git a/netbox/extras/tests/test_models.py b/netbox/extras/tests/test_models.py index cb3f08acb..c92a1bc54 100644 --- a/netbox/extras/tests/test_models.py +++ b/netbox/extras/tests/test_models.py @@ -1,6 +1,6 @@ -from django.contrib.contenttypes.models import ContentType from django.test import TestCase +from core.models import ObjectType from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup from extras.models import ConfigContext, Tag from tenancy.models import Tenant, TenantGroup @@ -22,7 +22,7 @@ class TagTest(TestCase): # Create a Tag that can only be applied to Regions tag = Tag.objects.create(name='Tag 1', slug='tag-1') - tag.object_types.add(ContentType.objects.get_by_natural_key('dcim', 'region')) + tag.object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'region')) # Apply the Tag to a Region region.tags.add(tag) From 01ee9c87b8c1c0e2edc19fcf0f17ce54ce0bdb6a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 09:52:45 -0500 Subject: [PATCH 11/25] Update ObjectPermission.object_types to reference ObjectType --- netbox/netbox/tests/test_authentication.py | 12 ++--- netbox/netbox/tests/test_import.py | 6 +-- netbox/users/api/serializers.py | 4 +- netbox/users/forms/model_forms.py | 4 +- ...07_objectpermission_update_object_types.py | 19 ++++++++ netbox/users/models.py | 2 +- netbox/users/tests/test_api.py | 6 +-- netbox/users/tests/test_filtersets.py | 7 +-- netbox/utilities/permissions.py | 8 ++-- netbox/utilities/testing/api.py | 21 ++++----- netbox/utilities/testing/views.py | 45 ++++++++++--------- 11 files changed, 78 insertions(+), 56 deletions(-) create mode 100644 netbox/users/migrations/0007_objectpermission_update_object_types.py diff --git a/netbox/netbox/tests/test_authentication.py b/netbox/netbox/tests/test_authentication.py index 6a894edcd..6e049dcaf 100644 --- a/netbox/netbox/tests/test_authentication.py +++ b/netbox/netbox/tests/test_authentication.py @@ -2,13 +2,13 @@ import datetime from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from django.test import Client from django.test.utils import override_settings from django.urls import reverse from netaddr import IPNetwork 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 @@ -452,7 +452,7 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix)) # Retrieve permitted object url = reverse('ipam-api:prefix-detail', @@ -482,7 +482,7 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix)) # Retrieve all objects. Only permitted objects should be returned. response = self.client.get(url, **self.header) @@ -510,7 +510,7 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix)) # Attempt to create a non-permitted object response = self.client.post(url, data, format='json', **self.header) @@ -541,7 +541,7 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix)) # Attempt to edit a non-permitted object data = {'site': self.sites[0].pk} @@ -581,7 +581,7 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix)) # Attempt to delete a non-permitted object url = reverse('ipam-api:prefix-detail', diff --git a/netbox/netbox/tests/test_import.py b/netbox/netbox/tests/test_import.py index bd07886e8..b0b21a07d 100644 --- a/netbox/netbox/tests/test_import.py +++ b/netbox/netbox/tests/test_import.py @@ -1,6 +1,6 @@ -from django.contrib.contenttypes.models import ContentType from django.test import override_settings +from core.models import ObjectType from dcim.models import * from users.models import ObjectPermission from utilities.choices import CSVDelimiterChoices, ImportFormatChoices @@ -67,7 +67,7 @@ class CSVImportTestCase(ModelViewTestCase): obj_perm = ObjectPermission(name='Test permission', actions=['add']) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('import')), 200) @@ -108,7 +108,7 @@ class CSVImportTestCase(ModelViewTestCase): obj_perm = ObjectPermission(name='Test permission', actions=['add']) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('import')), 200) diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index b9bd55e75..b99371b6c 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -1,12 +1,12 @@ from django.conf import settings from django.contrib.auth import authenticate from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes from rest_framework import serializers from rest_framework.exceptions import AuthenticationFailed, PermissionDenied +from core.models import ObjectType from netbox.api.fields import ContentTypeField, IPNetworkSerializer, SerializedPKRelatedField from netbox.api.serializers import ValidatedModelSerializer from users.models import Group, ObjectPermission, Token @@ -161,7 +161,7 @@ class TokenProvisionSerializer(TokenSerializer): class ObjectPermissionSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail') object_types = ContentTypeField( - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), many=True ) groups = SerializedPKRelatedField( diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index 2a024bf47..6c717d1ea 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -1,12 +1,12 @@ from django import forms from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.forms import SimpleArrayField from django.core.exceptions import FieldError from django.utils.html import mark_safe from django.utils.translation import gettext_lazy as _ +from core.models import ObjectType from ipam.formfields import IPNetworkFormField from ipam.validators import prefix_validator from netbox.preferences import PREFERENCES @@ -278,7 +278,7 @@ class GroupForm(forms.ModelForm): class ObjectPermissionForm(forms.ModelForm): object_types = ContentTypeMultipleChoiceField( label=_('Object types'), - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), limit_choices_to=OBJECTPERMISSION_OBJECT_TYPES, widget=forms.SelectMultiple(attrs={'size': 6}) ) diff --git a/netbox/users/migrations/0007_objectpermission_update_object_types.py b/netbox/users/migrations/0007_objectpermission_update_object_types.py new file mode 100644 index 000000000..d3018a602 --- /dev/null +++ b/netbox/users/migrations/0007_objectpermission_update_object_types.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.1 on 2024-03-04 14:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_gfk_indexes'), + ('users', '0006_custom_group_model'), + ] + + operations = [ + migrations.AlterField( + model_name='objectpermission', + name='object_types', + field=models.ManyToManyField(limit_choices_to=models.Q(models.Q(models.Q(('app_label__in', ['account', 'admin', 'auth', 'contenttypes', 'sessions', 'taggit', 'users']), _negated=True), models.Q(('app_label', 'auth'), ('model__in', ['group', 'user'])), models.Q(('app_label', 'users'), ('model__in', ['objectpermission', 'token'])), _connector='OR')), related_name='object_permissions', to='core.objecttype'), + ), + ] diff --git a/netbox/users/models.py b/netbox/users/models.py index 94eb0ad58..d2ee16e5e 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -383,7 +383,7 @@ class ObjectPermission(models.Model): default=True ) object_types = models.ManyToManyField( - to='contenttypes.ContentType', + to='core.ObjectType', limit_choices_to=OBJECTPERMISSION_OBJECT_TYPES, related_name='object_permissions' ) diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index 51fc21c97..2ff3545a6 100644 --- a/netbox/users/tests/test_api.py +++ b/netbox/users/tests/test_api.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from django.urls import reverse +from core.models import ObjectType from users.models import Group, ObjectPermission, Token from utilities.testing import APIViewTestCases, APITestCase, create_test_user from utilities.utils import deepmerge @@ -64,7 +64,7 @@ class UserTest(APIViewTestCases.APIViewTestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) user_credentials = { 'username': 'user1', @@ -261,7 +261,7 @@ class ObjectPermissionTest( ) User.objects.bulk_create(users) - object_type = ContentType.objects.get(app_label='dcim', model='device') + object_type = ObjectType.objects.get(app_label='dcim', model='device') for i in range(3): objectpermission = ObjectPermission( diff --git a/netbox/users/tests/test_filtersets.py b/netbox/users/tests/test_filtersets.py index 5d373628f..d42133d8d 100644 --- a/netbox/users/tests/test_filtersets.py +++ b/netbox/users/tests/test_filtersets.py @@ -5,6 +5,7 @@ from django.contrib.contenttypes.models import ContentType 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 utilities.testing import BaseFilterSetTests @@ -151,9 +152,9 @@ class ObjectPermissionTestCase(TestCase, BaseFilterSetTests): User.objects.bulk_create(users) object_types = ( - ContentType.objects.get(app_label='dcim', model='site'), - ContentType.objects.get(app_label='dcim', model='rack'), - ContentType.objects.get(app_label='dcim', model='device'), + ObjectType.objects.get(app_label='dcim', model='site'), + ObjectType.objects.get(app_label='dcim', model='rack'), + ObjectType.objects.get(app_label='dcim', model='device'), ) permissions = ( diff --git a/netbox/utilities/permissions.py b/netbox/utilities/permissions.py index c72a72db7..f4b7061ee 100644 --- a/netbox/utilities/permissions.py +++ b/netbox/utilities/permissions.py @@ -1,5 +1,4 @@ from django.conf import settings -from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.utils.translation import gettext_lazy as _ @@ -50,13 +49,14 @@ def resolve_permission_ct(name): :param name: Permission name in the format ._ """ + from core.models import ObjectType app_label, action, model_name = resolve_permission(name) try: - content_type = ContentType.objects.get(app_label=app_label, model=model_name) - except ContentType.DoesNotExist: + object_type = ObjectType.objects.get(app_label=app_label, model=model_name) + except ObjectType.DoesNotExist: raise ValueError(_("Unknown app_label/model_name for {name}").format(name=name)) - return content_type, action + return object_type, action def permission_is_exempt(name): diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 20c607906..f5e12246b 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -10,6 +10,7 @@ from graphene.types import Dynamic as GQLDynamic, List as GQLList, Union as GQLU from rest_framework import status from rest_framework.test import APIClient +from core.models import ObjectType from extras.choices import ObjectChangeActionChoices from extras.models import ObjectChange from users.models import ObjectPermission, Token @@ -109,7 +110,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET to permitted object url = self._get_detail_url(instance1) @@ -183,7 +184,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET to permitted objects response = self.client.get(self._get_list_url(), **self.header) @@ -224,7 +225,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) initial_count = self._get_queryset().count() response = self.client.post(self._get_list_url(), self.create_data[0], format='json', **self.header) @@ -258,7 +259,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) initial_count = self._get_queryset().count() response = self.client.post(self._get_list_url(), self.create_data, format='json', **self.header) @@ -309,7 +310,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) response = self.client.patch(url, update_data, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) @@ -344,7 +345,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) id_list = list(self._get_queryset().values_list('id', flat=True)[:3]) self.assertEqual(len(id_list), 3, "Insufficient number of objects to test bulk update") @@ -387,7 +388,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) response = self.client.delete(url, **self.header) self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) @@ -413,7 +414,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Target the three most recently created objects to avoid triggering recursive deletions # (e.g. with MPTT objects) @@ -504,7 +505,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) response = self.client.post(url, data={'query': query}, **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) @@ -529,7 +530,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) response = self.client.post(url, data={'query': query}, **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) diff --git a/netbox/utilities/testing/views.py b/netbox/utilities/testing/views.py index daa44b905..7bc776b1e 100644 --- a/netbox/utilities/testing/views.py +++ b/netbox/utilities/testing/views.py @@ -8,6 +8,7 @@ from django.test import override_settings from django.urls import reverse from django.utils.translation import gettext as _ +from core.models import ObjectType from extras.choices import ObjectChangeActionChoices from extras.models import ObjectChange from netbox.models.features import ChangeLoggingMixin @@ -93,7 +94,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(instance.get_absolute_url()), 200) @@ -109,7 +110,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET to permitted object self.assertHttpStatus(self.client.get(instance1.get_absolute_url()), 200) @@ -161,7 +162,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('add')), 200) @@ -197,7 +198,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with object-level permission self.assertHttpStatus(self.client.get(self._get_url('add')), 200) @@ -260,7 +261,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('edit', instance)), 200) @@ -295,7 +296,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with a permitted object self.assertHttpStatus(self.client.get(self._get_url('edit', instance1)), 200) @@ -349,7 +350,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('delete', instance)), 200) @@ -384,7 +385,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with a permitted object self.assertHttpStatus(self.client.get(self._get_url('delete', instance1)), 200) @@ -442,7 +443,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('list')), 200) @@ -458,7 +459,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with object-level permission response = self.client.get(self._get_url('list')) @@ -477,7 +478,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Test default CSV export response = self.client.get(f'{url}?export') @@ -524,7 +525,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Bulk create objects response = self.client.post(**request) @@ -548,7 +549,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Attempt to make the request with unmet constraints self.assertHttpStatus(self.client.post(**request), 200) @@ -610,7 +611,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('import')), 200) @@ -639,7 +640,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Test POST with permission self.assertHttpStatus(self.client.post(self._get_url('import'), data), 302) @@ -674,7 +675,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Attempt to import non-permitted objects self.assertHttpStatus(self.client.post(self._get_url('import'), data), 200) @@ -730,7 +731,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try POST with model-level permission self.assertHttpStatus(self.client.post(self._get_url('bulk_edit'), data), 302) @@ -761,7 +762,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Attempt to bulk edit permitted objects into a non-permitted state response = self.client.post(self._get_url('bulk_edit'), data) @@ -811,7 +812,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try POST with model-level permission self.assertHttpStatus(self.client.post(self._get_url('bulk_delete'), data), 302) @@ -833,7 +834,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Attempt to bulk delete non-permitted objects initial_count = self._get_queryset().count() @@ -891,7 +892,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try POST with model-level permission self.assertHttpStatus(self.client.post(self._get_url('bulk_rename'), data), 302) @@ -916,7 +917,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Attempt to bulk edit permitted objects into a non-permitted state response = self.client.post(self._get_url('bulk_rename'), data) From d5380100698cfc6a6165fcbd8cdb4e27f096e67a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 10:06:28 -0500 Subject: [PATCH 12/25] Add GraphQL type for ObjectType --- netbox/extras/graphql/types.py | 12 ++++++------ netbox/netbox/graphql/types.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 65819a75a..d99e54976 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -39,7 +39,7 @@ class CustomFieldType(ObjectType): class Meta: model = models.CustomField - exclude = ('object_types', 'object_type') + fields = '__all__' filterset_class = filtersets.CustomFieldFilterSet @@ -55,7 +55,7 @@ class CustomLinkType(ObjectType): class Meta: model = models.CustomLink - exclude = ('object_types', ) + fields = '__all__' filterset_class = filtersets.CustomLinkFilterSet @@ -63,7 +63,7 @@ class EventRuleType(OrganizationalObjectType): class Meta: model = models.EventRule - exclude = ('object_types',) + fields = '__all__' filterset_class = filtersets.EventRuleFilterSet @@ -71,7 +71,7 @@ class ExportTemplateType(ObjectType): class Meta: model = models.ExportTemplate - exclude = ('object_types', ) + fields = '__all__' filterset_class = filtersets.ExportTemplateFilterSet @@ -103,7 +103,7 @@ class SavedFilterType(ObjectType): class Meta: model = models.SavedFilter - exclude = ('object_types', ) + fields = '__all__' filterset_class = filtersets.SavedFilterFilterSet @@ -111,7 +111,7 @@ class TagType(ObjectType): class Meta: model = models.Tag - exclude = ('object_types', 'extras_taggeditem_items',) + exclude = ('extras_taggeditem_items',) filterset_class = filtersets.TagFilterSet diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index 10847742b..f131f07cf 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -1,5 +1,6 @@ import graphene +from core.models import ObjectType as ObjectType_ from django.contrib.contenttypes.models import ContentType from extras.graphql.mixins import ( ChangelogMixin, @@ -11,7 +12,9 @@ from graphene_django import DjangoObjectType __all__ = ( 'BaseObjectType', + 'ContentTypeType', 'ObjectType', + 'ObjectTypeType', 'OrganizationalObjectType', 'NetBoxObjectType', ) @@ -90,3 +93,10 @@ class ContentTypeType(DjangoObjectType): class Meta: model = ContentType fields = ('id', 'app_label', 'model') + + +class ObjectTypeType(DjangoObjectType): + + class Meta: + model = ObjectType_ + fields = ('id', 'app_label', 'model') From 0419a69ae8236f367cae26bf8fca1eed7fe2b7d5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 10:46:34 -0500 Subject: [PATCH 13/25] Clean up outdated references to ContentType --- netbox/dcim/tests/test_views.py | 2 -- netbox/extras/api/customfields.py | 10 +++++----- netbox/extras/signals.py | 4 ++-- netbox/extras/tests/test_filtersets.py | 14 ++++++-------- netbox/extras/tests/test_forms.py | 7 +++---- netbox/netbox/api/serializers/features.py | 2 -- netbox/netbox/api/viewsets/mixins.py | 5 ++--- netbox/netbox/forms/base.py | 11 ++++++----- netbox/netbox/forms/mixins.py | 7 +++---- netbox/netbox/search/backends.py | 16 ++++++++-------- netbox/netbox/views/generic/bulk_views.py | 6 +++--- netbox/tenancy/tests/test_filtersets.py | 4 ++-- netbox/users/api/nested_serializers.py | 4 ++-- netbox/users/tests/test_filtersets.py | 3 +-- netbox/users/tests/test_views.py | 7 +++---- netbox/utilities/templatetags/helpers.py | 8 ++++---- netbox/utilities/tests/test_counters.py | 4 +--- 17 files changed, 51 insertions(+), 63 deletions(-) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 53d7f3d34..e9e5a557b 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -3,7 +3,6 @@ from zoneinfo import ZoneInfo import yaml 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 from netaddr import EUI @@ -2982,7 +2981,6 @@ class CableTestCase( tags = create_tags('Alpha', 'Bravo', 'Charlie') - interface_ct = ContentType.objects.get_for_model(Interface) cls.form_data = { # TODO: Revisit this limitation # Changing terminations not supported when editing an existing Cable diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 77c3a170e..7ecee01f8 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -1,10 +1,10 @@ -from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes from rest_framework.fields import Field from rest_framework.serializers import ValidationError +from core.models import ObjectType from extras.choices import CustomFieldTypeChoices from extras.models import CustomField from netbox.constants import NESTED_SERIALIZER_PREFIX @@ -25,8 +25,8 @@ class CustomFieldDefaultValues: self.model = serializer_field.parent.Meta.model # Retrieve the CustomFields for the parent model - content_type = ContentType.objects.get_for_model(self.model) - fields = CustomField.objects.filter(object_types=content_type) + object_type = ObjectType.objects.get_for_model(self.model) + fields = CustomField.objects.filter(object_types=object_type) # Populate the default value for each CustomField value = {} @@ -47,8 +47,8 @@ class CustomFieldsDataField(Field): Cache CustomFields assigned to this model to avoid redundant database queries """ if not hasattr(self, '_custom_fields'): - content_type = ContentType.objects.get_for_model(self.parent.Meta.model) - self._custom_fields = CustomField.objects.filter(object_types=content_type) + object_type = ObjectType.objects.get_for_model(self.parent.Meta.model) + self._custom_fields = CustomField.objects.filter(object_types=object_type) return self._custom_fields def to_representation(self, obj): diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index cacc5a83a..833ce0036 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -257,7 +257,7 @@ def process_job_start_event_rules(sender, **kwargs): """ Process event rules for jobs starting. """ - event_rules = EventRule.objects.filter(type_job_start=True, enabled=True, content_types=sender.object_type) + event_rules = EventRule.objects.filter(type_job_start=True, enabled=True, object_types=sender.object_type) username = sender.user.username if sender.user else None process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_START, sender.data, username) @@ -267,6 +267,6 @@ def process_job_end_event_rules(sender, **kwargs): """ Process event rules for jobs terminating. """ - event_rules = EventRule.objects.filter(type_job_end=True, enabled=True, content_types=sender.object_type) + event_rules = EventRule.objects.filter(type_job_end=True, enabled=True, object_types=sender.object_type) username = sender.user.username if sender.user else None process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_END, sender.data, username) diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 7be00a5a3..cec0ffe94 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -175,8 +175,6 @@ class WebhookTestCase(TestCase, BaseFilterSetTests): @classmethod def setUpTestData(cls): - content_types = ContentType.objects.filter(model__in=['region', 'site', 'rack', 'location', 'device']) - webhooks = ( Webhook( name='Webhook 1', @@ -355,7 +353,7 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests): def test_object_types(self): params = {'object_types': 'dcim.region'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'object_types_id': [ContentType.objects.get_for_model(Region).pk]} + params = {'object_types_id': [ObjectType.objects.get_for_model(Region).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_action_type(self): @@ -440,7 +438,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests): def test_object_types(self): params = {'object_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'object_types_id': [ContentType.objects.get_for_model(Site).pk]} + params = {'object_types_id': [ObjectType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_weight(self): @@ -530,7 +528,7 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests): def test_object_types(self): params = {'object_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'object_types_id': [ContentType.objects.get_for_model(Site).pk]} + params = {'object_types_id': [ObjectType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_user(self): @@ -661,7 +659,7 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests): def test_object_types(self): params = {'object_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'object_types_id': [ContentType.objects.get_for_model(Site).pk]} + params = {'object_types_id': [ObjectType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_description(self): @@ -1164,12 +1162,12 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_object_types(self): - params = {'for_object_type_id': [ContentType.objects.get_by_natural_key('dcim', 'site').pk]} + params = {'for_object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]} self.assertEqual( list(self.filterset(params, self.queryset).qs.values_list('name', flat=True)), ['Tag 1', 'Tag 3'] ) - params = {'for_object_type_id': [ContentType.objects.get_by_natural_key('circuits', 'provider').pk]} + params = {'for_object_type_id': [ObjectType.objects.get_by_natural_key('circuits', 'provider').pk]} self.assertEqual( list(self.filterset(params, self.queryset).qs.values_list('name', flat=True)), ['Tag 2', 'Tag 3'] diff --git a/netbox/extras/tests/test_forms.py b/netbox/extras/tests/test_forms.py index 7642ee2a8..4c96e72d6 100644 --- a/netbox/extras/tests/test_forms.py +++ b/netbox/extras/tests/test_forms.py @@ -1,4 +1,3 @@ -from django.contrib.contenttypes.models import ContentType from django.test import TestCase from core.models import ObjectType @@ -63,14 +62,14 @@ class CustomFieldModelFormTest(TestCase): cf_object = CustomField.objects.create( name='object', type=CustomFieldTypeChoices.TYPE_OBJECT, - object_type=ContentType.objects.get_for_model(Site) + object_type=ObjectType.objects.get_for_model(Site) ) cf_object.object_types.set([object_type]) cf_multiobject = CustomField.objects.create( name='multiobject', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, - object_type=ContentType.objects.get_for_model(Site) + object_type=ObjectType.objects.get_for_model(Site) ) cf_multiobject.object_types.set([object_type]) @@ -100,7 +99,7 @@ class SavedFilterFormTest(TestCase): form = SavedFilterForm({ 'name': 'test-sf', 'slug': 'test-sf', - 'object_types': [ContentType.objects.get_for_model(Site).pk], + 'object_types': [ObjectType.objects.get_for_model(Site).pk], 'weight': 100, 'parameters': { "status": [ diff --git a/netbox/netbox/api/serializers/features.py b/netbox/netbox/api/serializers/features.py index 1374ba526..3bd5c8a2d 100644 --- a/netbox/netbox/api/serializers/features.py +++ b/netbox/netbox/api/serializers/features.py @@ -1,9 +1,7 @@ -from django.contrib.contenttypes.models import ContentType from rest_framework import serializers from rest_framework.fields import CreateOnlyDefault from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues -from extras.models import CustomField from .nested import NestedTagSerializer __all__ = ( diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index 7df498ac6..e07e2c78b 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -1,4 +1,3 @@ -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.db import transaction from django.http import Http404 @@ -41,8 +40,8 @@ class ExportTemplatesMixin: """ def list(self, request, *args, **kwargs): if 'export' in request.GET: - content_type = ContentType.objects.get_for_model(self.get_serializer_class().Meta.model) - et = ExportTemplate.objects.filter(content_types=content_type, name=request.GET['export']).first() + object_type = ObjectType.objects.get_for_model(self.get_serializer_class().Meta.model) + et = ExportTemplate.objects.filter(object_types=object_type, name=request.GET['export']).first() if et is None: raise Http404 queryset = self.filter_queryset(self.get_queryset()) diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 12de3b12b..85064e79d 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.utils.translation import gettext_lazy as _ +from core.models import ObjectType from extras.choices import * from extras.models import CustomField, Tag from utilities.forms import CSVModelForm @@ -129,9 +130,9 @@ class NetBoxModelBulkEditForm(CustomFieldsMixin, forms.Form): self.fields['pk'].queryset = self.model.objects.all() # Restrict tag fields by model - ct = ContentType.objects.get_for_model(self.model) - self.fields['add_tags'].widget.add_query_param('for_object_type_id', ct.pk) - self.fields['remove_tags'].widget.add_query_param('for_object_type_id', ct.pk) + object_type = ObjectType.objects.get_for_model(self.model) + self.fields['add_tags'].widget.add_query_param('for_object_type_id', object_type.pk) + self.fields['remove_tags'].widget.add_query_param('for_object_type_id', object_type.pk) self._extend_nullable_fields() @@ -169,9 +170,9 @@ class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form) super().__init__(*args, **kwargs) # Limit saved filters to those applicable to the form's model - content_type = ContentType.objects.get_for_model(self.model) + object_type = ObjectType.objects.get_for_model(self.model) self.fields['filter_id'].widget.add_query_params({ - 'content_type_id': content_type.pk, + 'object_types_id': object_type.pk, }) def _get_custom_fields(self, content_type): diff --git a/netbox/netbox/forms/mixins.py b/netbox/netbox/forms/mixins.py index 2f903db5d..6b1f31265 100644 --- a/netbox/netbox/forms/mixins.py +++ b/netbox/netbox/forms/mixins.py @@ -1,5 +1,4 @@ from django import forms -from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ from core.models import ObjectType @@ -86,6 +85,6 @@ class TagsMixin(forms.Form): super().__init__(*args, **kwargs) # Limit tags to those applicable to the object type - content_type = ContentType.objects.get_for_model(self._meta.model) - if content_type and hasattr(self.fields['tags'].widget, 'add_query_param'): - self.fields['tags'].widget.add_query_param('for_object_type_id', content_type.pk) + object_type = ObjectType.objects.get_for_model(self._meta.model) + if object_type and hasattr(self.fields['tags'].widget, 'add_query_param'): + self.fields['tags'].widget.add_query_param('for_object_type_id', object_type.pk) diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index 0d7667087..a9e867b9f 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -131,11 +131,11 @@ class CachedValueSearchBackend(SearchBackend): ) )[:MAX_RESULTS] - # Gather all ContentTypes present in the search results (used for prefetching related + # Gather all ObjectTypes present in the search results (used for prefetching related # objects). This must be done before generating the final results list, which returns # a RawQuerySet. - content_type_ids = set(queryset.values_list('object_type', flat=True)) - object_types = ObjectType.objects.filter(pk__in=content_type_ids) + object_type_ids = set(queryset.values_list('object_type', flat=True)) + object_types = ObjectType.objects.filter(pk__in=object_type_ids) # Construct a Prefetch to pre-fetch only those related objects for which the # user has permission to view. @@ -152,11 +152,11 @@ class CachedValueSearchBackend(SearchBackend): params ) - # Iterate through each ContentType represented in the search results and prefetch any + # Iterate through each ObjectType represented in the search results and prefetch any # related objects necessary to render the prescribed display attributes (display_attrs). - for ct in object_types: - model = ct.model_class() - indexer = registry['search'].get(content_type_identifier(ct)) + for object_type in object_types: + model = object_type.model_class() + indexer = registry['search'].get(content_type_identifier(object_type)) if not (display_attrs := getattr(indexer, 'display_attrs', None)): continue @@ -170,7 +170,7 @@ class CachedValueSearchBackend(SearchBackend): # Compile a list of all CachedValues referencing this object type, and prefetch # any related objects if prefetch_fields: - objects = [r for r in results if r.object_type == ct] + objects = [r for r in results if r.object_type == object_type] prefetch_related_objects(objects, *prefetch_fields) # Omit any results pertaining to an object the user does not have permission to view diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index f1f4e90dd..022059e51 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -4,7 +4,6 @@ from copy import deepcopy from django.contrib import messages from django.contrib.contenttypes.fields import GenericRel -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError from django.db import transaction, IntegrityError from django.db.models import ManyToManyField, ProtectedError, RestrictedError @@ -17,6 +16,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext as _ from django_tables2.export import TableExport +from core.models import ObjectType from extras.models import ExportTemplate from extras.signals import clear_events from utilities.error_handlers import handle_protectederror @@ -124,7 +124,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): request: The current request """ model = self.queryset.model - content_type = ContentType.objects.get_for_model(model) + object_type = ObjectType.objects.get_for_model(model) if self.filterset: self.queryset = self.filterset(request.GET, self.queryset, request=request).qs @@ -143,7 +143,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): # Render an ExportTemplate elif request.GET['export']: - template = get_object_or_404(ExportTemplate, content_types=content_type, name=request.GET['export']) + template = get_object_or_404(ExportTemplate, object_types=object_type, name=request.GET['export']) return self.export_template(template, request) # Check for YAML export support on the model diff --git a/netbox/tenancy/tests/test_filtersets.py b/netbox/tenancy/tests/test_filtersets.py index 729dd7204..3bcbddd4b 100644 --- a/netbox/tenancy/tests/test_filtersets.py +++ b/netbox/tenancy/tests/test_filtersets.py @@ -1,6 +1,6 @@ -from django.contrib.contenttypes.models import ContentType from django.test import TestCase +from core.models import ObjectType from dcim.models import Manufacturer, Site from tenancy.filtersets import * from tenancy.models import * @@ -296,7 +296,7 @@ class ContactAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests): ContactAssignment.objects.bulk_create(assignments) def test_object_type(self): - params = {'object_type_id': ContentType.objects.get_by_natural_key('dcim', 'site')} + params = {'object_type_id': ObjectType.objects.get_by_natural_key('dcim', 'site')} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) def test_contact(self): diff --git a/netbox/users/api/nested_serializers.py b/netbox/users/api/nested_serializers.py index 552c24906..2ab5d3aa5 100644 --- a/netbox/users/api/nested_serializers.py +++ b/netbox/users/api/nested_serializers.py @@ -1,9 +1,9 @@ from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes 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 @@ -49,7 +49,7 @@ class NestedTokenSerializer(WritableNestedSerializer): class NestedObjectPermissionSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail') object_types = ContentTypeField( - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), many=True ) groups = serializers.SerializerMethodField(read_only=True) diff --git a/netbox/users/tests/test_filtersets.py b/netbox/users/tests/test_filtersets.py index d42133d8d..5930285a9 100644 --- a/netbox/users/tests/test_filtersets.py +++ b/netbox/users/tests/test_filtersets.py @@ -1,7 +1,6 @@ import datetime from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from django.test import TestCase from django.utils.timezone import make_aware @@ -199,7 +198,7 @@ class ObjectPermissionTestCase(TestCase, BaseFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_object_types(self): - object_types = ContentType.objects.filter(model__in=['site', 'rack']) + object_types = ObjectType.objects.filter(model__in=['site', 'rack']) params = {'object_types': [object_types[0].pk, object_types[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/users/tests/test_views.py b/netbox/users/tests/test_views.py index 27d2aeab1..588730dbd 100644 --- a/netbox/users/tests/test_views.py +++ b/netbox/users/tests/test_views.py @@ -1,5 +1,4 @@ -from django.contrib.contenttypes.models import ContentType - +from core.models import ObjectType from users.models import * from utilities.testing import ViewTestCases, create_test_user @@ -115,7 +114,7 @@ class ObjectPermissionTestCase( @classmethod def setUpTestData(cls): - ct = ContentType.objects.get_by_natural_key('dcim', 'site') + object_type = ObjectType.objects.get_by_natural_key('dcim', 'site') permissions = ( ObjectPermission(name='Permission 1', actions=['view', 'add', 'delete']), @@ -127,7 +126,7 @@ class ObjectPermissionTestCase( cls.form_data = { 'name': 'Permission X', 'description': 'A new permission', - 'object_types': [ct.pk], + 'object_types': [object_type.pk], 'actions': 'view,edit,delete', } diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index aaee9679c..b71848411 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -1,16 +1,16 @@ import datetime import json -from urllib.parse import quote from typing import Dict, Any +from urllib.parse import quote from django import template from django.conf import settings -from django.contrib.contenttypes.models import ContentType from django.template.defaultfilters import date from django.urls import NoReverseMatch, reverse from django.utils import timezone from django.utils.safestring import mark_safe +from core.models import ObjectType from utilities.forms import get_selected_values, TableConfigForm from utilities.utils import get_viewname @@ -322,10 +322,10 @@ def applied_filters(context, model, form, query_params): save_link = None if user.has_perm('extras.add_savedfilter') and 'filter_id' not in context['request'].GET: - content_type = ContentType.objects.get_for_model(model).pk + object_type = ObjectType.objects.get_for_model(model).pk parameters = json.dumps(dict(context['request'].GET.lists())) url = reverse('extras:savedfilter_add') - save_link = f"{url}?content_types={content_type}¶meters={quote(parameters)}" + save_link = f"{url}?object_types={object_type}¶meters={quote(parameters)}" return { 'applied_filters': applied_filters, diff --git a/netbox/utilities/tests/test_counters.py b/netbox/utilities/tests/test_counters.py index 014c758e9..b87e73ace 100644 --- a/netbox/utilities/tests/test_counters.py +++ b/netbox/utilities/tests/test_counters.py @@ -1,11 +1,9 @@ -from django.contrib.contenttypes.models import ContentType from django.test import override_settings from django.urls import reverse from dcim.models import * -from users.models import ObjectPermission from utilities.testing.base import TestCase -from utilities.testing.utils import create_test_device, create_test_user +from utilities.testing.utils import create_test_device class CountersTest(TestCase): From 0e89f4660143de4b1a2a5f5375d16eba26feb9fa Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 11:49:39 -0500 Subject: [PATCH 14/25] #15277: Clean up references to object types in templates --- netbox/extras/models/customfields.py | 2 +- netbox/extras/models/models.py | 6 +++--- netbox/extras/tables/tables.py | 2 +- netbox/extras/views.py | 6 +++--- netbox/templates/extras/exporttemplate.html | 5 ----- netbox/templates/inc/panels/image_attachments.html | 2 +- netbox/templates/tenancy/object_contacts.html | 2 +- 7 files changed, 10 insertions(+), 15 deletions(-) diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 311ccce76..681bd4f2a 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -209,7 +209,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): objects = CustomFieldManager() clone_fields = ( - 'content_types', 'type', 'object_type', 'group_name', 'description', 'required', 'search_weight', + 'object_types', 'type', 'object_type', 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable', ) diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 4a57c6ada..b55aaa11d 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -359,7 +359,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): ) clone_fields = ( - 'content_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', + 'object_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', ) class Meta: @@ -448,7 +448,7 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change ) clone_fields = ( - 'content_types', 'template_code', 'mime_type', 'file_extension', 'as_attachment', + 'object_types', 'template_code', 'mime_type', 'file_extension', 'as_attachment', ) class Meta: @@ -561,7 +561,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): ) clone_fields = ( - 'content_types', 'weight', 'enabled', 'parameters', + 'object_types', 'weight', 'enabled', 'parameters', ) class Meta: diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 5bf4f1892..fee0c9f29 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -314,7 +314,7 @@ class EventRuleTable(NetBoxTable): 'last_updated', ) default_columns = ( - 'pk', 'name', 'enabled', 'action_type', 'action_object', 'content_types', 'type_create', 'type_update', + 'pk', 'name', 'enabled', 'action_type', 'action_object', 'object_types', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', ) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 49c6dcab3..73fdb6b83 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -762,8 +762,8 @@ class ImageAttachmentEditView(generic.ObjectEditView): def alter_object(self, instance, request, args, kwargs): if not instance.pk: # Assign the parent object based on URL kwargs - content_type = get_object_or_404(ContentType, pk=request.GET.get('content_type')) - instance.parent = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id')) + object_type = get_object_or_404(ContentType, pk=request.GET.get('object_type')) + instance.parent = get_object_or_404(object_type.model_class(), pk=request.GET.get('object_id')) return instance def get_return_url(self, request, obj=None): @@ -771,7 +771,7 @@ class ImageAttachmentEditView(generic.ObjectEditView): def get_extra_addanother_params(self, request): return { - 'content_type': request.GET.get('content_type'), + 'object_type': request.GET.get('object_type'), 'object_id': request.GET.get('object_id'), } diff --git a/netbox/templates/extras/exporttemplate.html b/netbox/templates/extras/exporttemplate.html index 16dd49ee2..8d14e3ffb 100644 --- a/netbox/templates/extras/exporttemplate.html +++ b/netbox/templates/extras/exporttemplate.html @@ -5,11 +5,6 @@ {% block title %}{{ object.name }}{% endblock %} -{% block breadcrumbs %} - {{ block.super }} - -{% endblock %} - {% block content %}
    diff --git a/netbox/templates/inc/panels/image_attachments.html b/netbox/templates/inc/panels/image_attachments.html index a5f2ac18f..c3c7cf7e3 100644 --- a/netbox/templates/inc/panels/image_attachments.html +++ b/netbox/templates/inc/panels/image_attachments.html @@ -6,7 +6,7 @@ {% trans "Images" %} {% if perms.extras.add_imageattachment %} diff --git a/netbox/templates/tenancy/object_contacts.html b/netbox/templates/tenancy/object_contacts.html index 4b78e53d3..304972e4a 100644 --- a/netbox/templates/tenancy/object_contacts.html +++ b/netbox/templates/tenancy/object_contacts.html @@ -5,7 +5,7 @@ {% block extra_controls %} {% if perms.tenancy.add_contactassignment %} {% with viewname=object|viewname:"contacts" %} - + {% trans "Add a contact" %} {% endwith %} From 4533c8dae069fc98324ae92edd333a888df65540 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 12:17:32 -0500 Subject: [PATCH 15/25] Rename sequences for ObjectType M2M tables --- .../migrations/0111_rename_content_types.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/netbox/extras/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py index 3d6529692..7b0fa9459 100644 --- a/netbox/extras/migrations/0111_rename_content_types.py +++ b/netbox/extras/migrations/0111_rename_content_types.py @@ -26,6 +26,9 @@ class Migration(migrations.Migration): name='object_type', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.objecttype'), ), + migrations.RunSQL( + "ALTER TABLE extras_customfield_content_types_id_seq RENAME TO extras_customfield_object_types_id_seq" + ), # Custom links migrations.RenameField( @@ -38,6 +41,9 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='custom_links', to='core.objecttype'), ), + migrations.RunSQL( + "ALTER TABLE extras_customlink_content_types_id_seq RENAME TO extras_customlink_object_types_id_seq" + ), # Event rules migrations.RenameField( @@ -50,6 +56,9 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='event_rules', to='core.objecttype'), ), + migrations.RunSQL( + "ALTER TABLE extras_eventrule_content_types_id_seq RENAME TO extras_eventrule_object_types_id_seq" + ), # Export templates migrations.RenameField( @@ -62,6 +71,9 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='export_templates', to='core.objecttype'), ), + migrations.RunSQL( + "ALTER TABLE extras_exporttemplate_content_types_id_seq RENAME TO extras_exporttemplate_object_types_id_seq" + ), # Saved filters migrations.RenameField( @@ -74,6 +86,9 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='saved_filters', to='core.objecttype'), ), + migrations.RunSQL( + "ALTER TABLE extras_savedfilter_content_types_id_seq RENAME TO extras_savedfilter_object_types_id_seq" + ), # Image attachments migrations.RemoveIndex( From 5552f2a7dd8c67daf5f110271e577b48cc6341ba Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 6 Mar 2024 15:30:59 -0500 Subject: [PATCH 16/25] Rename ContentTypes REST API endpoint & resources --- netbox/extras/api/serializers.py | 2 +- .../{contenttypes.py => objecttypes.py} | 6 +++--- netbox/extras/api/urls.py | 2 +- netbox/extras/api/views.py | 15 +++++++-------- netbox/extras/filtersets.py | 8 ++++---- netbox/extras/tests/test_api.py | 12 ++++++------ 6 files changed, 22 insertions(+), 23 deletions(-) rename netbox/extras/api/serializers_/{contenttypes.py => objecttypes.py} (75%) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 809bd78ed..bd19b3184 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -1,7 +1,7 @@ +from .serializers_.objecttypes import * from .serializers_.attachments import * from .serializers_.bookmarks import * from .serializers_.change_logging import * -from .serializers_.contenttypes import * from .serializers_.customfields import * from .serializers_.customlinks import * from .serializers_.dashboard import * diff --git a/netbox/extras/api/serializers_/contenttypes.py b/netbox/extras/api/serializers_/objecttypes.py similarity index 75% rename from netbox/extras/api/serializers_/contenttypes.py rename to netbox/extras/api/serializers_/objecttypes.py index 75d25a5bf..8e4806652 100644 --- a/netbox/extras/api/serializers_/contenttypes.py +++ b/netbox/extras/api/serializers_/objecttypes.py @@ -4,12 +4,12 @@ from core.models import ObjectType from netbox.api.serializers import BaseModelSerializer __all__ = ( - 'ContentTypeSerializer', + 'ObjectTypeSerializer', ) -class ContentTypeSerializer(BaseModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail') +class ObjectTypeSerializer(BaseModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:objecttype-detail') class Meta: model = ObjectType diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py index 68b4488bc..301cc1b0a 100644 --- a/netbox/extras/api/urls.py +++ b/netbox/extras/api/urls.py @@ -22,7 +22,7 @@ router.register('config-contexts', views.ConfigContextViewSet) router.register('config-templates', views.ConfigTemplateViewSet) router.register('scripts', views.ScriptViewSet, basename='script') router.register('object-changes', views.ObjectChangeViewSet) -router.register('content-types', views.ContentTypeViewSet) +router.register('object-types', views.ObjectTypeViewSet) app_name = 'extras-api' urlpatterns = [ diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 72450f9c9..3439f6f3f 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -1,4 +1,3 @@ -from django.contrib.contenttypes.models import ContentType from django.shortcuts import get_object_or_404 from django_rq.queues import get_connection from rest_framework import status @@ -11,7 +10,7 @@ from rest_framework.routers import APIRootView from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rq import Worker -from core.models import Job +from core.models import Job, ObjectType from extras import filtersets from extras.models import * from extras.scripts import run_script @@ -275,17 +274,17 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet): # -# ContentTypes +# Object types # -class ContentTypeViewSet(ReadOnlyModelViewSet): +class ObjectTypeViewSet(ReadOnlyModelViewSet): """ - Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects. + Read-only list of ObjectTypes. """ permission_classes = [IsAuthenticatedOrLoginNotRequired] - queryset = ContentType.objects.order_by('app_label', 'model') - serializer_class = serializers.ContentTypeSerializer - filterset_class = filtersets.ContentTypeFilterSet + queryset = ObjectType.objects.order_by('app_label', 'model') + serializer_class = serializers.ObjectTypeSerializer + filterset_class = filtersets.ObjectTypeFilterSet # diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 797c76b51..450514e66 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.utils.translation import gettext as _ -from core.models import DataSource +from core.models import DataSource, ObjectType from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet from tenancy.models import Tenant, TenantGroup @@ -18,7 +18,6 @@ __all__ = ( 'BookmarkFilterSet', 'ConfigContextFilterSet', 'ConfigTemplateFilterSet', - 'ContentTypeFilterSet', 'CustomFieldChoiceSetFilterSet', 'CustomFieldFilterSet', 'CustomLinkFilterSet', @@ -28,6 +27,7 @@ __all__ = ( 'JournalEntryFilterSet', 'LocalConfigContextFilterSet', 'ObjectChangeFilterSet', + 'ObjectTypeFilterSet', 'SavedFilterFilterSet', 'ScriptFilterSet', 'TagFilterSet', @@ -662,14 +662,14 @@ class ObjectChangeFilterSet(BaseFilterSet): # ContentTypes # -class ContentTypeFilterSet(django_filters.FilterSet): +class ObjectTypeFilterSet(django_filters.FilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), ) class Meta: - model = ContentType + model = ObjectType fields = ['id', 'app_label', 'model'] def search(self, queryset, name, value): diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 53d981123..5d243ae1a 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -876,17 +876,17 @@ class CreatedUpdatedFilterTest(APITestCase): self.assertEqual(response.data['results'][0]['id'], rack2.pk) -class ContentTypeTest(APITestCase): +class ObjectTypeTest(APITestCase): def test_list_objects(self): - contenttype_count = ContentType.objects.count() + object_type_count = ObjectType.objects.count() - response = self.client.get(reverse('extras-api:contenttype-list'), **self.header) + response = self.client.get(reverse('extras-api:objecttype-list'), **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) - self.assertEqual(response.data['count'], contenttype_count) + self.assertEqual(response.data['count'], object_type_count) def test_get_object(self): - contenttype = ContentType.objects.first() + object_type = ObjectType.objects.first() - url = reverse('extras-api:contenttype-detail', kwargs={'pk': contenttype.pk}) + url = reverse('extras-api:objecttype-detail', kwargs={'pk': object_type.pk}) self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_200_OK) From 40a654b21e3d5caac7e5e331756b81b13d1a753f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 6 Mar 2024 15:41:16 -0500 Subject: [PATCH 17/25] Use singular names for M2M field filters --- netbox/extras/filtersets.py | 38 ++++++++++++++++---------- netbox/extras/forms/filtersets.py | 20 +++++++------- netbox/extras/tests/test_filtersets.py | 30 ++++++++++---------- 3 files changed, 48 insertions(+), 40 deletions(-) diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 450514e66..6cb309580 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -89,10 +89,12 @@ class EventRuleFilterSet(NetBoxModelFilterSet): method='search', label=_('Search'), ) - object_types_id = MultiValueNumberFilter( + object_type_id = MultiValueNumberFilter( field_name='object_types__id' ) - object_types = ContentTypeFilter() + object_type = ContentTypeFilter( + field_name='object_types' + ) action_type = django_filters.MultipleChoiceFilter( choices=EventRuleActionChoices ) @@ -124,10 +126,10 @@ class CustomFieldFilterSet(BaseFilterSet): type = django_filters.MultipleChoiceFilter( choices=CustomFieldTypeChoices ) - object_types_id = MultiValueNumberFilter( + object_type_id = MultiValueNumberFilter( field_name='object_types__id' ) - object_types = ContentTypeFilter( + object_type = ContentTypeFilter( field_name='object_types' ) choice_set_id = django_filters.ModelMultipleChoiceFilter( @@ -142,8 +144,8 @@ class CustomFieldFilterSet(BaseFilterSet): class Meta: model = CustomField fields = [ - 'id', 'object_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible', - 'ui_editable', 'weight', 'is_cloneable', 'description', + 'id', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', + 'weight', 'is_cloneable', 'description', ] def search(self, queryset, name, value): @@ -190,15 +192,17 @@ class CustomLinkFilterSet(BaseFilterSet): method='search', label=_('Search'), ) - object_types_id = MultiValueNumberFilter( + object_type_id = MultiValueNumberFilter( field_name='object_types__id' ) - object_types = ContentTypeFilter() + object_type = ContentTypeFilter( + field_name='object_types' + ) class Meta: model = CustomLink fields = [ - 'id', 'object_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window', + 'id', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window', ] def search(self, queryset, name, value): @@ -217,10 +221,12 @@ class ExportTemplateFilterSet(BaseFilterSet): method='search', label=_('Search'), ) - object_types_id = MultiValueNumberFilter( + object_type_id = MultiValueNumberFilter( field_name='object_types__id' ) - object_types = ContentTypeFilter() + object_type = ContentTypeFilter( + field_name='object_types' + ) data_source_id = django_filters.ModelMultipleChoiceFilter( queryset=DataSource.objects.all(), label=_('Data source (ID)'), @@ -232,7 +238,7 @@ class ExportTemplateFilterSet(BaseFilterSet): class Meta: model = ExportTemplate - fields = ['id', 'object_types', 'name', 'description', 'data_synced'] + fields = ['id', 'name', 'description', 'data_synced'] def search(self, queryset, name, value): if not value.strip(): @@ -248,10 +254,12 @@ class SavedFilterFilterSet(BaseFilterSet): method='search', label=_('Search'), ) - object_types_id = MultiValueNumberFilter( + object_type_id = MultiValueNumberFilter( field_name='object_types__id' ) - object_types = ContentTypeFilter() + object_type = ContentTypeFilter( + field_name='object_types' + ) user_id = django_filters.ModelMultipleChoiceFilter( queryset=get_user_model().objects.all(), label=_('User (ID)'), @@ -268,7 +276,7 @@ class SavedFilterFilterSet(BaseFilterSet): class Meta: model = SavedFilter - fields = ['id', 'object_types', 'name', 'slug', 'description', 'enabled', 'shared', 'weight'] + fields = ['id', 'name', 'slug', 'description', 'enabled', 'shared', 'weight'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 75724b108..285e7618f 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -38,11 +38,11 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), (_('Attributes'), ( - 'type', 'object_types_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable', + 'type', 'object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable', 'is_cloneable', )), ) - object_types_id = ContentTypeMultipleChoiceField( + object_type_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('custom_fields'), required=False, label=_('Object type') @@ -108,9 +108,9 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm): class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), - (_('Attributes'), ('object_types', 'enabled', 'new_window', 'weight')), + (_('Attributes'), ('object_type', 'enabled', 'new_window', 'weight')), ) - object_types = ContentTypeMultipleChoiceField( + object_type = ContentTypeMultipleChoiceField( label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_links'), required=False @@ -139,7 +139,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), (_('Data'), ('data_source_id', 'data_file_id')), - (_('Attributes'), ('object_types_id', 'mime_type', 'file_extension', 'as_attachment')), + (_('Attributes'), ('object_type_id', 'mime_type', 'file_extension', 'as_attachment')), ) data_source_id = DynamicModelMultipleChoiceField( queryset=DataSource.objects.all(), @@ -154,7 +154,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): 'source_id': '$data_source_id' } ) - object_types_id = ContentTypeMultipleChoiceField( + object_type_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('export_templates'), required=False, label=_('Content types') @@ -195,9 +195,9 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), - (_('Attributes'), ('object_types', 'enabled', 'shared', 'weight')), + (_('Attributes'), ('object_type', 'enabled', 'shared', 'weight')), ) - object_types = ContentTypeMultipleChoiceField( + object_type = ContentTypeMultipleChoiceField( label=_('Object types'), queryset=ObjectType.objects.public(), required=False @@ -250,10 +250,10 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm): fieldsets = ( (None, ('q', 'filter_id', 'tag')), - (_('Attributes'), ('object_types_id', 'action_type', 'enabled')), + (_('Attributes'), ('object_type_id', 'action_type', 'enabled')), (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')), ) - object_types_id = ContentTypeMultipleChoiceField( + object_type_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('event_rules'), required=False, label=_('Object type') diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index cec0ffe94..4f9279831 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -102,10 +102,10 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests): params = {'name': ['Custom Field 1', 'Custom Field 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_object_types(self): - params = {'object_types': 'dcim.site'} + def test_object_type(self): + params = {'object_type': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'object_types_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]} + params = {'object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_required(self): @@ -350,10 +350,10 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_object_types(self): - params = {'object_types': 'dcim.region'} + def test_object_type(self): + params = {'object_type': 'dcim.region'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'object_types_id': [ObjectType.objects.get_for_model(Region).pk]} + params = {'object_type_id': [ObjectType.objects.get_for_model(Region).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_action_type(self): @@ -435,10 +435,10 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests): params = {'name': ['Custom Link 1', 'Custom Link 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_object_types(self): - params = {'object_types': 'dcim.site'} + def test_object_type(self): + params = {'object_type': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'object_types_id': [ObjectType.objects.get_for_model(Site).pk]} + params = {'object_type_id': [ObjectType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_weight(self): @@ -525,10 +525,10 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_object_types(self): - params = {'object_types': 'dcim.site'} + def test_object_type(self): + params = {'object_type': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'object_types_id': [ObjectType.objects.get_for_model(Site).pk]} + params = {'object_type_id': [ObjectType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_user(self): @@ -656,10 +656,10 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests): params = {'name': ['Export Template 1', 'Export Template 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_object_types(self): - params = {'object_types': 'dcim.site'} + def test_object_type(self): + params = {'object_type': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'object_types_id': [ObjectType.objects.get_for_model(Site).pk]} + params = {'object_type_id': [ObjectType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_description(self): From 0c22e38006d6da62cb1080f266d288e0280344e2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 6 Mar 2024 16:08:08 -0500 Subject: [PATCH 18/25] Re-enable error handling in middleware (disabled for testing) --- netbox/netbox/middleware.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index c140462ec..cb7d2c8ba 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -70,8 +70,8 @@ class CoreMiddleware: return # Cleanly handle exceptions that occur from REST API requests - # if is_api_request(request): - # return rest_api_server_error(request) + if is_api_request(request): + return rest_api_server_error(request) # Ignore Http404s (defer to Django's built-in 404 handling) if isinstance(exception, Http404): From e5ee8523efc67bb6508c47a9fb7ad2a5ae4e0962 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 6 Mar 2024 16:08:37 -0500 Subject: [PATCH 19/25] Misc cleanup --- netbox/netbox/models/features.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index 74b24aa85..bff9ee59f 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -494,17 +494,17 @@ class SyncedDataMixin(models.Model): ret = super().save(*args, **kwargs) # Create/delete AutoSyncRecord as needed - content_type = ObjectType.objects.get_for_model(self) + object_type = ObjectType.objects.get_for_model(self) if self.auto_sync_enabled: AutoSyncRecord.objects.update_or_create( - object_type=content_type, + object_type=object_type, object_id=self.pk, defaults={'datafile': self.data_file} ) else: AutoSyncRecord.objects.filter( datafile=self.data_file, - object_type=content_type, + object_type=object_type, object_id=self.pk ).delete() @@ -514,10 +514,10 @@ class SyncedDataMixin(models.Model): from core.models import AutoSyncRecord # Delete AutoSyncRecord - content_type = ObjectType.objects.get_for_model(self) + object_type = ObjectType.objects.get_for_model(self) AutoSyncRecord.objects.filter( datafile=self.data_file, - object_type=content_type, + object_type=object_type, object_id=self.pk ).delete() From 7567c9d2818c103868a2174837832fc2f447f5a9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 6 Mar 2024 16:47:38 -0500 Subject: [PATCH 20/25] Changelog for #12795, #15277, #15292 --- docs/release-notes/version-4.0.md | 36 ++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md index 4bae93fa8..60b3115f0 100644 --- a/docs/release-notes/version-4.0.md +++ b/docs/release-notes/version-4.0.md @@ -34,7 +34,7 @@ The REST API now supports specifying which fields to include in the response dat * [#12325](https://github.com/netbox-community/netbox/issues/12325) - The Django admin UI is now disabled by default (set `DJANGO_ADMIN_ENABLED` to True to enable it) * [#12510](https://github.com/netbox-community/netbox/issues/12510) - Dropped support for legacy reports -* [#12795](https://github.com/netbox-community/netbox/issues/12795) - NetBox now uses a custom User model rather than the stock model provided by Django +* [#12795](https://github.com/netbox-community/netbox/issues/12795) - NetBox now uses custom User and Group models rather than the stock models provided by Django * [#13647](https://github.com/netbox-community/netbox/issues/13647) - Squash all database migrations prior to v3.7 * [#14092](https://github.com/netbox-community/netbox/issues/14092) - Remove backward compatibility for importing plugin resources from `extras.plugins` (now `netbox.plugins`) * [#14638](https://github.com/netbox-community/netbox/issues/14638) - Drop support for Python 3.8 and 3.9 @@ -44,3 +44,37 @@ The REST API now supports specifying which fields to include in the response dat * [#15042](https://github.com/netbox-community/netbox/issues/15042) - Rearchitect the logic for registering models & model features * [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices * [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class +* [#15277](https://github.com/netbox-community/netbox/issues/15277) - Replace references to ContentType without ObjectType proxy model & standardize field names +* [#15292](https://github.com/netbox-community/netbox/issues/15292) - Remove obsolete `device_role` attribute from Device model (this field was renamed to `role` in v3.6) + +### REST API Changes + +* The `/api/extras/content-types/` endpoint has moved to `/api/extras/object-types/` +* dcim.Device + * The obsolete read-only attribute `device_role` has been removed (replaced by `role` in v3.6) +* extras.CustomField + * `content_types` has been renamed to `object_types` + * The `content_types` filter is now `object_type` + * The `content_type_id` filter is now `object_type_id` +* extras.CustomLink + * `content_types` has been renamed to `object_types` + * The `content_types` filter is now `object_type` + * The `content_type_id` filter is now `object_type_id` +* extras.EventRule + * `content_types` has been renamed to `object_types` + * The `content_types` filter is now `object_type` + * The `content_type_id` filter is now `object_type_id` +* extras.ExportTemplate + * `content_types` has been renamed to `object_types` + * The `content_types` filter is now `object_type` + * The `content_type_id` filter is now `object_type_id` +* extras.ImageAttachment + * `content_type` has been renamed to `object_type` + * The `content_type` filter is now `object_type` +* extras.SavedFilter + * `content_types` has been renamed to `object_types` + * The `content_types` filter is now `object_type` + * The `content_type_id` filter is now `object_type_id` +* tenancy.ContactAssignment + * `content_type` has been renamed to `object_type` + * The `content_type_id` filter is now `object_type_id` From 663bd324641818e5bb79ae2f14d1610160860882 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Thu, 7 Mar 2024 08:41:34 -0800 Subject: [PATCH 21/25] 10587 script pagination (#15343) * 10587 temp commit * 10587 temp commit * 10587 fix migrations * 10587 pagination * 10587 pagination * 10587 pagination * 10587 review changes --- .../0108_convert_reports_to_scripts.py | 3 - netbox/extras/migrations/0109_script_model.py | 16 +- ...0110_remove_eventrule_action_parameters.py | 3 + netbox/extras/tables/tables.py | 62 ++++++- netbox/extras/views.py | 57 ++++++- .../templates/extras/htmx/script_result.html | 159 ++++++------------ netbox/templates/extras/script_result.html | 74 ++++++-- 7 files changed, 241 insertions(+), 133 deletions(-) diff --git a/netbox/extras/migrations/0108_convert_reports_to_scripts.py b/netbox/extras/migrations/0108_convert_reports_to_scripts.py index 072353550..b547c41c3 100644 --- a/netbox/extras/migrations/0108_convert_reports_to_scripts.py +++ b/netbox/extras/migrations/0108_convert_reports_to_scripts.py @@ -25,7 +25,4 @@ class Migration(migrations.Migration): migrations.DeleteModel( name='Report', ), - migrations.DeleteModel( - name='ReportModule', - ), ] diff --git a/netbox/extras/migrations/0109_script_model.py b/netbox/extras/migrations/0109_script_model.py index 89b343a82..7570077a7 100644 --- a/netbox/extras/migrations/0109_script_model.py +++ b/netbox/extras/migrations/0109_script_model.py @@ -82,10 +82,12 @@ def update_scripts(apps, schema_editor): ContentType = apps.get_model('contenttypes', 'ContentType') Script = apps.get_model('extras', 'Script') ScriptModule = apps.get_model('extras', 'ScriptModule') + ReportModule = apps.get_model('extras', 'ReportModule') Job = apps.get_model('core', 'Job') - script_ct = ContentType.objects.get_for_model(Script) - scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule) + script_ct = ContentType.objects.get_for_model(Script, for_concrete_model=False) + scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule, for_concrete_model=False) + reportmodule_ct = ContentType.objects.get_for_model(ReportModule, for_concrete_model=False) for module in ScriptModule.objects.all(): for script_name in get_module_scripts(module): @@ -96,10 +98,16 @@ def update_scripts(apps, schema_editor): # Update all Jobs associated with this ScriptModule & script name to point to the new Script object Job.objects.filter( - object_type=scriptmodule_ct, + object_type_id=scriptmodule_ct.id, object_id=module.pk, name=script_name - ).update(object_type=script_ct, object_id=script.pk) + ).update(object_type_id=script_ct.id, object_id=script.pk) + # Update all Jobs associated with this ScriptModule & script name to point to the new Script object + Job.objects.filter( + object_type_id=reportmodule_ct.id, + object_id=module.pk, + name=script_name + ).update(object_type_id=script_ct.id, object_id=script.pk) def update_event_rules(apps, schema_editor): diff --git a/netbox/extras/migrations/0110_remove_eventrule_action_parameters.py b/netbox/extras/migrations/0110_remove_eventrule_action_parameters.py index 910352462..b7373bdce 100644 --- a/netbox/extras/migrations/0110_remove_eventrule_action_parameters.py +++ b/netbox/extras/migrations/0110_remove_eventrule_action_parameters.py @@ -12,4 +12,7 @@ class Migration(migrations.Migration): model_name='eventrule', name='action_parameters', ), + migrations.DeleteModel( + name='ReportModule', + ), ] diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index fee0c9f29..819beb1a5 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -5,7 +5,7 @@ from django.conf import settings from django.utils.translation import gettext_lazy as _ from extras.models import * -from netbox.tables import NetBoxTable, columns +from netbox.tables import BaseTable, NetBoxTable, columns from .template_code import * __all__ = ( @@ -21,6 +21,8 @@ __all__ = ( 'JournalEntryTable', 'ObjectChangeTable', 'SavedFilterTable', + 'ReportResultsTable', + 'ScriptResultsTable', 'TaggedItemTable', 'TagTable', 'WebhookTable', @@ -507,3 +509,61 @@ class JournalEntryTable(NetBoxTable): default_columns = ( 'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments' ) + + +class ScriptResultsTable(BaseTable): + index = tables.Column( + verbose_name=_('Line') + ) + time = tables.Column( + verbose_name=_('Time') + ) + status = tables.TemplateColumn( + template_code="""{% load log_levels %}{% log_level record.status %}""", + verbose_name=_('Level') + ) + message = tables.Column( + verbose_name=_('Message') + ) + + class Meta(BaseTable.Meta): + empty_text = _('No results found') + fields = ( + 'index', 'time', 'status', 'message', + ) + + +class ReportResultsTable(BaseTable): + index = tables.Column( + verbose_name=_('Line') + ) + method = tables.Column( + verbose_name=_('Method') + ) + time = tables.Column( + verbose_name=_('Time') + ) + status = tables.Column( + empty_values=(), + verbose_name=_('Level') + ) + status = tables.TemplateColumn( + template_code="""{% load log_levels %}{% log_level record.status %}""", + verbose_name=_('Level') + ) + + object = tables.Column( + verbose_name=_('Object') + ) + url = tables.Column( + verbose_name=_('URL') + ) + message = tables.Column( + verbose_name=_('Message') + ) + + class Meta(BaseTable.Meta): + empty_text = _('No results found') + fields = ( + 'index', 'method', 'time', 'status', 'object', 'url', 'message', + ) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 73fdb6b83..1fa2a30aa 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -17,6 +17,7 @@ from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm from extras.dashboard.utils import get_widget_class from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.views import generic +from netbox.views.generic.mixins import TableMixin from utilities.forms import ConfirmationForm, get_field_value from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.rqworker import get_workers_for_queue @@ -26,6 +27,7 @@ from utilities.views import ContentTypePermissionRequiredMixin, register_model_v from . import filtersets, forms, tables from .models import * from .scripts import run_script +from .tables import ReportResultsTable, ScriptResultsTable # @@ -1143,19 +1145,72 @@ class LegacyScriptRedirectView(ContentTypePermissionRequiredMixin, View): return redirect(f'{url}{path}') -class ScriptResultView(generic.ObjectView): +class ScriptResultView(TableMixin, generic.ObjectView): queryset = Job.objects.all() def get_required_permission(self): return 'extras.view_script' + def get_table(self, job, request, bulk_actions=True): + data = [] + tests = None + table = None + index = 0 + if job.data: + if 'log' in job.data: + if 'tests' in job.data: + tests = job.data['tests'] + + for log in job.data['log']: + index += 1 + result = { + 'index': index, + 'time': log.get('time'), + 'status': log.get('status'), + 'message': log.get('message'), + } + data.append(result) + + table = ScriptResultsTable(data, user=request.user) + table.configure(request) + else: + # for legacy reports + tests = job.data + + if tests: + for method, test_data in tests.items(): + if 'log' in test_data: + for time, status, obj, url, message in test_data['log']: + index += 1 + result = { + 'index': index, + 'method': method, + 'time': time, + 'status': status, + 'object': obj, + 'url': url, + 'message': message, + } + data.append(result) + + table = ReportResultsTable(data, user=request.user) + table.configure(request) + + return table + def get(self, request, **kwargs): + table = None job = get_object_or_404(Job.objects.all(), pk=kwargs.get('job_pk')) + if job.completed: + table = self.get_table(job, request, bulk_actions=False) + context = { 'script': job.object, 'job': job, + 'table': table, } + if job.data and 'log' in job.data: # Script context['tests'] = job.data.get('tests', {}) diff --git a/netbox/templates/extras/htmx/script_result.html b/netbox/templates/extras/htmx/script_result.html index ed5dd9cbd..e532e07e1 100644 --- a/netbox/templates/extras/htmx/script_result.html +++ b/netbox/templates/extras/htmx/script_result.html @@ -3,124 +3,63 @@ {% load log_levels %} {% load i18n %} -

    - {% if job.started %} - {% trans "Started" %}: {{ job.started|annotated_date }} - {% elif job.scheduled %} - {% trans "Scheduled for" %}: {{ job.scheduled|annotated_date }} ({{ job.scheduled|naturaltime }}) - {% else %} - {% trans "Created" %}: {{ job.created|annotated_date }} - {% endif %} +

    +

    + {% if job.started %} + {% trans "Started" %}: {{ job.started|annotated_date }} + {% elif job.scheduled %} + {% trans "Scheduled for" %}: {{ job.scheduled|annotated_date }} ({{ job.scheduled|naturaltime }}) + {% else %} + {% trans "Created" %}: {{ job.created|annotated_date }} + {% endif %} + {% if job.completed %} + {% trans "Duration" %}: {{ job.duration }} + {% endif %} + {% badge job.get_status_display job.get_status_color %} +

    {% if job.completed %} - {% trans "Duration" %}: {{ job.duration }} - {% endif %} - {% badge job.get_status_display job.get_status_color %} -

    -{% if job.completed %} - - {# Script log. Legacy reports will not have this. #} - {% if 'log' in job.data %} -
    -
    {% trans "Log" %}
    - {% if job.data.log %} - - - - - - - - {% for log in job.data.log %} + {% if tests %} + {# Summary of test methods #} +
    +
    {% trans "Test Summary" %}
    +
    {% trans "Line" %}{% trans "Time" %}{% trans "Level" %}{% trans "Message" %}
    + {% for test, data in tests.items %} - - - - + + {% endfor %}
    {{ forloop.counter }}{{ log.time|placeholder }}{% log_level log.status %}{{ log.message|markdown }}{{ test }} + {{ data.success }} + {{ data.info }} + {{ data.warning }} + {{ data.failure }} +
    - {% else %} -
    {% trans "None" %}
    - {% endif %} -
    - {% endif %} +
    + {% endif %} - {# Script output. Legacy reports will not have this. #} - {% if 'output' in job.data %} -
    -
    {% trans "Output" %}
    - {% if job.data.output %} -
    {{ job.data.output }}
    - {% else %} -
    {% trans "None" %}
    - {% endif %} -
    - {% endif %} - - {# Test method logs (for legacy Reports) #} - {% if tests %} - - {# Summary of test methods #} + {% if table %}
    -
    {% trans "Test Summary" %}
    - - {% for test, data in tests.items %} - - - - - {% endfor %} -
    {{ test }} - {{ data.success }} - {{ data.info }} - {{ data.warning }} - {{ data.failure }} -
    +
    +
    {% trans "Log" %}
    + {% include 'htmx/table.html' %} +
    + {% endif %} - {# Detailed results for individual tests #} -
    -
    {% trans "Test Details" %}
    - - - - - - - - - - - {% for test, data in tests.items %} - - - - {% for time, level, obj, url, message in data.log %} - - - - - - - {% endfor %} - {% endfor %} - -
    {% trans "Time" %}{% trans "Level" %}{% trans "Object" %}{% trans "Message" %}
    - {{ test }} -
    {{ time }} - - - {% if obj and url %} - {{ obj }} - {% elif obj %} - {{ obj }} - {% else %} - {{ ''|placeholder }} - {% endif %} - {{ message|markdown }}
    -
    + {# Script output. Legacy reports will not have this. #} + {% if 'output' in job.data %} +
    +
    {% trans "Output" %}
    + {% if job.data.output %} +
    {{ job.data.output }}
    + {% else %} +
    {% trans "None" %}
    + {% endif %} +
    + {% endif %} + {% elif job.started %} + {% include 'extras/inc/result_pending.html' %} {% endif %} -{% elif job.started %} - {% include 'extras/inc/result_pending.html' %} -{% endif %} +
    diff --git a/netbox/templates/extras/script_result.html b/netbox/templates/extras/script_result.html index 8f6d817c7..030e73903 100644 --- a/netbox/templates/extras/script_result.html +++ b/netbox/templates/extras/script_result.html @@ -32,28 +32,74 @@ {% block tabs %} {% endblock %} {% block content %} -
    -
    -
    - {% include 'extras/htmx/script_result.html' %} + {# Object list tab #} +
    + + {# Object table controls #} +
    +
    + {% if request.user.is_authenticated %} +
    + +
    + {% endif %} +
    + +
    + {% csrf_token %} + {# "Select all" form #} + {% if table.paginator.num_pages > 1 %} +
    +
    +
    +
    + + +
    +
    +
    +
    + {% endif %} + +
    + {% csrf_token %} + + + {# Objects table #} +
    + {% include 'extras/htmx/script_result.html' %} +
    + {# /Objects table #} + +
    +
    -
    -
    -

    {{ script.filename }}

    -
    {{ script.source }}
    -
    + {# /Object list tab #} + + {# Filters tab #} + {% if filter_form %} +
    + {% include 'inc/filter_list.html' %} +
    + {% endif %} + {# /Filters tab #} + {% endblock content %} {% block modals %} - {% include 'inc/htmx_modal.html' %} + {% table_config_form table table_name="ObjectTable" %} {% endblock modals %} From 78dd65219f89293cde539e2029d9a2284a70544b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Sat, 9 Mar 2024 06:16:17 -0500 Subject: [PATCH 22/25] Closes #15357: Rename CustomField.object_type to related_object_type (#15366) --- docs/models/extras/customfield.md | 2 +- netbox/extras/api/customfields.py | 6 ++-- .../extras/api/serializers_/customfields.py | 10 +++--- netbox/extras/filtersets.py | 4 +++ netbox/extras/forms/bulk_import.py | 4 +-- netbox/extras/forms/filtersets.py | 8 ++--- netbox/extras/forms/model_forms.py | 6 ++-- .../0113_customfield_rename_object_type.py | 16 +++++++++ netbox/extras/models/customfields.py | 16 ++++----- netbox/extras/tables/tables.py | 9 +++-- netbox/extras/tests/test_customfields.py | 34 +++++++++++++------ netbox/extras/tests/test_filtersets.py | 16 +++++++++ netbox/extras/tests/test_forms.py | 4 +-- netbox/extras/tests/test_views.py | 2 +- netbox/templates/extras/customfield.html | 4 ++- 15 files changed, 97 insertions(+), 44 deletions(-) create mode 100644 netbox/extras/migrations/0113_customfield_rename_object_type.py diff --git a/docs/models/extras/customfield.md b/docs/models/extras/customfield.md index e68ddb79d..495c4e2e8 100644 --- a/docs/models/extras/customfield.md +++ b/docs/models/extras/customfield.md @@ -38,7 +38,7 @@ The type of data this field holds. This must be one of the following: | Object | A single NetBox object of the type defined by `object_type` | | Multiple object | One or more NetBox objects of the type defined by `object_type` | -### Object Type +### Related Object Type For object and multiple-object fields only. Designates the type of NetBox object being referenced. diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 81535a147..09f247929 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -57,10 +57,10 @@ class CustomFieldsDataField(Field): for cf in self._get_custom_fields(): value = cf.deserialize(obj.get(cf.name)) if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT: - serializer = get_serializer_for_model(cf.object_type.model_class()) + serializer = get_serializer_for_model(cf.related_object_type.model_class()) value = serializer(value, nested=True, context=self.parent.context).data elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: - serializer = get_serializer_for_model(cf.object_type.model_class()) + serializer = get_serializer_for_model(cf.related_object_type.model_class()) value = serializer(value, nested=True, many=True, context=self.parent.context).data data[cf.name] = value @@ -79,7 +79,7 @@ class CustomFieldsDataField(Field): CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT ): - serializer_class = get_serializer_for_model(cf.object_type.model_class()) + serializer_class = get_serializer_for_model(cf.related_object_type.model_class()) many = cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT serializer = serializer_class(data=data[cf.name], nested=True, many=many, context=self.parent.context) if serializer.is_valid(): diff --git a/netbox/extras/api/serializers_/customfields.py b/netbox/extras/api/serializers_/customfields.py index efd6db063..79bb39557 100644 --- a/netbox/extras/api/serializers_/customfields.py +++ b/netbox/extras/api/serializers_/customfields.py @@ -44,7 +44,7 @@ class CustomFieldSerializer(ValidatedModelSerializer): many=True ) type = ChoiceField(choices=CustomFieldTypeChoices) - object_type = ContentTypeField( + related_object_type = ContentTypeField( queryset=ObjectType.objects.all(), required=False, allow_null=True @@ -62,10 +62,10 @@ class CustomFieldSerializer(ValidatedModelSerializer): class Meta: model = CustomField fields = [ - 'id', 'url', 'display', 'object_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', - 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', - 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', - 'created', 'last_updated', + 'id', 'url', 'display', 'object_types', 'type', 'related_object_type', 'data_type', 'name', 'label', + 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', + 'is_cloneable', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', + 'choice_set', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 6cb309580..d88b8c9b3 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -132,6 +132,10 @@ class CustomFieldFilterSet(BaseFilterSet): object_type = ContentTypeFilter( field_name='object_types' ) + related_object_type_id = MultiValueNumberFilter( + field_name='related_object_type__id' + ) + related_object_type = ContentTypeFilter() choice_set_id = django_filters.ModelMultipleChoiceFilter( queryset=CustomFieldChoiceSet.objects.all() ) diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 39d2933a7..55f71dbd2 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -40,7 +40,7 @@ class CustomFieldImportForm(CSVModelForm): choices=CustomFieldTypeChoices, help_text=_('Field data type (e.g. text, integer, etc.)') ) - object_type = CSVContentTypeField( + related_object_type = CSVContentTypeField( label=_('Object type'), queryset=ObjectType.objects.public(), required=False, @@ -69,7 +69,7 @@ class CustomFieldImportForm(CSVModelForm): class Meta: model = CustomField fields = ( - 'name', 'label', 'group_name', 'type', 'object_types', 'object_type', 'required', 'description', + 'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'description', 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 285e7618f..73751872f 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -38,14 +38,14 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), (_('Attributes'), ( - 'type', 'object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable', - 'is_cloneable', + 'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', + 'ui_editable', 'is_cloneable', )), ) - object_type_id = ContentTypeMultipleChoiceField( + related_object_type_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('custom_fields'), required=False, - label=_('Object type') + label=_('Related object type') ) type = forms.MultipleChoiceField( choices=CustomFieldTypeChoices, diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 7f36db657..09d2d9535 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -42,8 +42,8 @@ class CustomFieldForm(forms.ModelForm): label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_fields') ) - object_type = ContentTypeChoiceField( - label=_('Object type'), + related_object_type = ContentTypeChoiceField( + label=_('Related object type'), queryset=ObjectType.objects.public(), required=False, help_text=_("Type of the related object (for object/multi-object fields only)") @@ -55,7 +55,7 @@ class CustomFieldForm(forms.ModelForm): fieldsets = ( (_('Custom Field'), ( - 'object_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description', + 'object_types', 'name', 'label', 'group_name', 'type', 'related_object_type', 'required', 'description', )), (_('Behavior'), ('search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable')), (_('Values'), ('default', 'choice_set')), diff --git a/netbox/extras/migrations/0113_customfield_rename_object_type.py b/netbox/extras/migrations/0113_customfield_rename_object_type.py new file mode 100644 index 000000000..73c4a2a61 --- /dev/null +++ b/netbox/extras/migrations/0113_customfield_rename_object_type.py @@ -0,0 +1,16 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0112_tag_update_object_types'), + ] + + operations = [ + migrations.RenameField( + model_name='customfield', + old_name='object_type', + new_name='related_object_type', + ), + ] diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 681bd4f2a..a14c71c63 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -78,7 +78,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): default=CustomFieldTypeChoices.TYPE_TEXT, help_text=_('The type of data this custom field holds') ) - object_type = models.ForeignKey( + related_object_type = models.ForeignKey( to='core.ObjectType', on_delete=models.PROTECT, blank=True, @@ -209,7 +209,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): objects = CustomFieldManager() clone_fields = ( - 'object_types', 'type', 'object_type', 'group_name', 'description', 'required', 'search_weight', + 'object_types', 'type', 'related_object_type', 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable', ) @@ -344,11 +344,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): # Object fields must define an object_type; other fields must not if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT): - if not self.object_type: + if not self.related_object_type: raise ValidationError({ 'object_type': _("Object fields must define an object type.") }) - elif self.object_type: + elif self.related_object_type: raise ValidationError({ 'object_type': _( "{type} fields may not define an object type.") @@ -388,10 +388,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): except ValueError: return value if self.type == CustomFieldTypeChoices.TYPE_OBJECT: - model = self.object_type.model_class() + model = self.related_object_type.model_class() return model.objects.filter(pk=value).first() if self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: - model = self.object_type.model_class() + model = self.related_object_type.model_class() return model.objects.filter(pk__in=value) return value @@ -488,7 +488,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): # Object elif self.type == CustomFieldTypeChoices.TYPE_OBJECT: - model = self.object_type.model_class() + model = self.related_object_type.model_class() field_class = CSVModelChoiceField if for_csv_import else DynamicModelChoiceField field = field_class( queryset=model.objects.all(), @@ -498,7 +498,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): # Multiple objects elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: - model = self.object_type.model_class() + model = self.related_object_type.model_class() field_class = CSVModelMultipleChoiceField if for_csv_import else DynamicModelMultipleChoiceField field = field_class( queryset=model.objects.all(), diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 819beb1a5..a0f504931 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -57,6 +57,9 @@ class CustomFieldTable(NetBoxTable): description = columns.MarkdownColumn( verbose_name=_('Description') ) + related_object_type = columns.ContentTypeColumn( + verbose_name=_('Related Object Type') + ) choice_set = tables.Column( linkify=True, verbose_name=_('Choice Set') @@ -73,9 +76,9 @@ class CustomFieldTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = CustomField fields = ( - 'pk', 'id', 'name', 'object_types', 'label', 'type', 'group_name', 'required', 'default', 'description', - 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'weight', 'choice_set', - 'choices', 'created', 'last_updated', + 'pk', 'id', 'name', 'object_types', 'label', 'type', 'related_object_type', 'group_name', 'required', + 'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', + 'weight', 'choice_set', 'choices', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'description') diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 7ca18250c..0c8b86f93 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -350,7 +350,7 @@ class CustomFieldTest(TestCase): cf = CustomField.objects.create( name='object_field', type=CustomFieldTypeChoices.TYPE_OBJECT, - object_type=ObjectType.objects.get_for_model(VLAN), + related_object_type=ObjectType.objects.get_for_model(VLAN), required=False ) cf.object_types.set([self.object_type]) @@ -382,7 +382,7 @@ class CustomFieldTest(TestCase): cf = CustomField.objects.create( name='object_field', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, - object_type=ObjectType.objects.get_for_model(VLAN), + related_object_type=ObjectType.objects.get_for_model(VLAN), required=False ) cf.object_types.set([self.object_type]) @@ -498,16 +498,28 @@ class CustomFieldTest(TestCase): ).full_clean() # Object - CustomField(name='test', type='object', required=True, object_type=object_type, default=site.pk).full_clean() - with self.assertRaises(ValidationError): - CustomField(name='test', type='object', required=True, object_type=object_type, default="xxx").full_clean() + CustomField( + name='test', + type='object', + required=True, + related_object_type=object_type, + default=site.pk + ).full_clean() + with (self.assertRaises(ValidationError)): + CustomField( + name='test', + type='object', + required=True, + related_object_type=object_type, + default="xxx" + ).full_clean() # Multi-object CustomField( name='test', type='multiobject', required=True, - object_type=object_type, + related_object_type=object_type, default=[site.pk] ).full_clean() with self.assertRaises(ValidationError): @@ -515,7 +527,7 @@ class CustomFieldTest(TestCase): name='test', type='multiobject', required=True, - object_type=object_type, + related_object_type=object_type, default=["xxx"] ).full_clean() @@ -581,13 +593,13 @@ class CustomFieldAPITest(APITestCase): CustomField( type=CustomFieldTypeChoices.TYPE_OBJECT, name='object_field', - object_type=ObjectType.objects.get_for_model(VLAN), + related_object_type=ObjectType.objects.get_for_model(VLAN), default=vlans[0].pk, ), CustomField( type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, name='multiobject_field', - object_type=ObjectType.objects.get_for_model(VLAN), + related_object_type=ObjectType.objects.get_for_model(VLAN), default=[vlans[0].pk, vlans[1].pk], ), ) @@ -1410,7 +1422,7 @@ class CustomFieldModelFilterTest(TestCase): cf = CustomField( name='cf11', type=CustomFieldTypeChoices.TYPE_OBJECT, - object_type=ObjectType.objects.get_for_model(Manufacturer) + related_object_type=ObjectType.objects.get_for_model(Manufacturer) ) cf.save() cf.object_types.set([object_type]) @@ -1419,7 +1431,7 @@ class CustomFieldModelFilterTest(TestCase): cf = CustomField( name='cf12', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, - object_type=ObjectType.objects.get_for_model(Manufacturer) + related_object_type=ObjectType.objects.get_for_model(Manufacturer) ) cf.save() cf.object_types.set([object_type]) diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 4f9279831..bec62c688 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -86,6 +86,16 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests): ui_editable=CustomFieldUIEditableChoices.HIDDEN, choice_set=choice_sets[1] ), + CustomField( + name='Custom Field 6', + type=CustomFieldTypeChoices.TYPE_OBJECT, + related_object_type=ObjectType.objects.get_by_natural_key('dcim', 'site'), + required=False, + weight=600, + filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED, + ui_visible=CustomFieldUIVisibleChoices.HIDDEN, + ui_editable=CustomFieldUIEditableChoices.HIDDEN + ), ) CustomField.objects.bulk_create(custom_fields) custom_fields[0].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'site')) @@ -108,6 +118,12 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests): params = {'object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_related_object_type(self): + params = {'related_object_type': 'dcim.site'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'related_object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_required(self): params = {'required': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) diff --git a/netbox/extras/tests/test_forms.py b/netbox/extras/tests/test_forms.py index 4c96e72d6..66c4e245e 100644 --- a/netbox/extras/tests/test_forms.py +++ b/netbox/extras/tests/test_forms.py @@ -62,14 +62,14 @@ class CustomFieldModelFormTest(TestCase): cf_object = CustomField.objects.create( name='object', type=CustomFieldTypeChoices.TYPE_OBJECT, - object_type=ObjectType.objects.get_for_model(Site) + related_object_type=ObjectType.objects.get_for_model(Site) ) cf_object.object_types.set([object_type]) cf_multiobject = CustomField.objects.create( name='multiobject', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, - object_type=ObjectType.objects.get_for_model(Site) + related_object_type=ObjectType.objects.get_for_model(Site) ) cf_multiobject.object_types.set([object_type]) diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index ca6ad9174..fd478acd4 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -54,7 +54,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - 'name,label,type,object_types,object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visible,ui_editable', + 'name,label,type,object_types,related_object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visible,ui_editable', 'field4,Field 4,text,dcim.site,,100,1000,exact,,,,[a-z]{3},always,yes', 'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,always,yes', 'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,always,yes', diff --git a/netbox/templates/extras/customfield.html b/netbox/templates/extras/customfield.html index ddc6b30f4..1fec35417 100644 --- a/netbox/templates/extras/customfield.html +++ b/netbox/templates/extras/customfield.html @@ -17,7 +17,9 @@ Type {{ object.get_type_display }} - {% if object.object_type %}({{ object.object_type.model|bettertitle }}){% endif %} + {% if object.related_object_type %} + ({{ object.related_object_type.model|bettertitle }}) + {% endif %} From 21de3f954fe3b2bee4e175ab6d6a892592c54c16 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 11 Mar 2024 11:49:04 -0400 Subject: [PATCH 23/25] #15357: Rename CustomField object_type to related_object_type --- netbox/users/migrations/0005_alter_user_table.py | 2 +- netbox/users/migrations/0006_custom_group_model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/users/migrations/0005_alter_user_table.py b/netbox/users/migrations/0005_alter_user_table.py index e07db6875..22c6bdd42 100644 --- a/netbox/users/migrations/0005_alter_user_table.py +++ b/netbox/users/migrations/0005_alter_user_table.py @@ -14,7 +14,7 @@ def update_content_types(apps, schema_editor): if netboxuser_ct: user_ct = ContentType.objects.filter(app_label='users', model='user').first() CustomField = apps.get_model('extras', 'CustomField') - CustomField.objects.filter(object_type_id=netboxuser_ct.id).update(object_type_id=user_ct.id) + CustomField.objects.filter(related_object_type_id=netboxuser_ct.id).update(related_object_type_id=user_ct.id) netboxuser_ct.delete() diff --git a/netbox/users/migrations/0006_custom_group_model.py b/netbox/users/migrations/0006_custom_group_model.py index 282da3ce0..04f4d0fd8 100644 --- a/netbox/users/migrations/0006_custom_group_model.py +++ b/netbox/users/migrations/0006_custom_group_model.py @@ -12,7 +12,7 @@ def update_custom_fields(apps, schema_editor): if old_ct := ContentType.objects.filter(app_label='users', model='netboxgroup').first(): new_ct = ContentType.objects.get_for_model(Group) - CustomField.objects.filter(object_type=old_ct).update(object_type=new_ct) + CustomField.objects.filter(related_object_type=old_ct).update(related_object_type=new_ct) class Migration(migrations.Migration): From d6acc18c29b776145e045309b5a45e1e05a922ce Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 11 Mar 2024 12:32:06 -0400 Subject: [PATCH 24/25] Closes #15383: Standardize filtering logic for the parents of recursively-nested models --- netbox/dcim/filtersets.py | 40 ++++++- netbox/dcim/tests/test_filtersets.py | 131 ++++++++++++++++------- netbox/tenancy/filtersets.py | 30 +++++- netbox/tenancy/tests/test_filtersets.py | 94 ++++++++++------ netbox/wireless/filtersets.py | 11 ++ netbox/wireless/tests/test_filtersets.py | 46 +++++--- 6 files changed, 262 insertions(+), 90 deletions(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 6b1611694..082659b8f 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -89,6 +89,19 @@ class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): to_field_name='slug', label=_('Parent region (slug)'), ) + ancestor_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='parent', + lookup_expr='in', + label=_('Region (ID)'), + ) + ancestor = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='parent', + lookup_expr='in', + to_field_name='slug', + label=_('Region (slug)'), + ) class Meta: model = Region @@ -106,6 +119,19 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): to_field_name='slug', label=_('Parent site group (slug)'), ) + ancestor_id = TreeNodeMultipleChoiceFilter( + queryset=SiteGroup.objects.all(), + field_name='parent', + lookup_expr='in', + label=_('Site group (ID)'), + ) + ancestor = TreeNodeMultipleChoiceFilter( + queryset=SiteGroup.objects.all(), + field_name='parent', + lookup_expr='in', + to_field_name='slug', + label=_('Site group (slug)'), + ) class Meta: model = SiteGroup @@ -214,13 +240,23 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM to_field_name='slug', label=_('Site (slug)'), ) - parent_id = TreeNodeMultipleChoiceFilter( + parent_id = django_filters.ModelMultipleChoiceFilter( + queryset=Location.objects.all(), + label=_('Parent location (ID)'), + ) + parent = django_filters.ModelMultipleChoiceFilter( + field_name='parent__slug', + queryset=Location.objects.all(), + to_field_name='slug', + label=_('Parent location (slug)'), + ) + ancestor_id = TreeNodeMultipleChoiceFilter( queryset=Location.objects.all(), field_name='parent', lookup_expr='in', label=_('Location (ID)'), ) - parent = TreeNodeMultipleChoiceFilter( + ancestor = TreeNodeMultipleChoiceFilter( queryset=Location.objects.all(), field_name='parent', lookup_expr='in', diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index b255c283e..f1eeddbb5 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -64,21 +64,32 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests): @classmethod def setUpTestData(cls): - regions = ( + parent_regions = ( Region(name='Region 1', slug='region-1', description='foobar1'), Region(name='Region 2', slug='region-2', description='foobar2'), Region(name='Region 3', slug='region-3', description='foobar3'), ) + for region in parent_regions: + region.save() + + regions = ( + Region(name='Region 1A', slug='region-1a', parent=parent_regions[0]), + Region(name='Region 1B', slug='region-1b', parent=parent_regions[0]), + Region(name='Region 2A', slug='region-2a', parent=parent_regions[1]), + Region(name='Region 2B', slug='region-2b', parent=parent_regions[1]), + Region(name='Region 3A', slug='region-3a', parent=parent_regions[2]), + Region(name='Region 3B', slug='region-3b', parent=parent_regions[2]), + ) for region in regions: region.save() child_regions = ( - Region(name='Region 1A', slug='region-1a', parent=regions[0]), - Region(name='Region 1B', slug='region-1b', parent=regions[0]), - Region(name='Region 2A', slug='region-2a', parent=regions[1]), - Region(name='Region 2B', slug='region-2b', parent=regions[1]), - Region(name='Region 3A', slug='region-3a', parent=regions[2]), - Region(name='Region 3B', slug='region-3b', parent=regions[2]), + Region(name='Region 1A1', slug='region-1a1', parent=regions[0]), + Region(name='Region 1B1', slug='region-1b1', parent=regions[1]), + Region(name='Region 2A1', slug='region-2a1', parent=regions[2]), + Region(name='Region 2B1', slug='region-2b1', parent=regions[3]), + Region(name='Region 3A1', slug='region-3a1', parent=regions[4]), + Region(name='Region 3B1', slug='region-3b1', parent=regions[5]), ) for region in child_regions: region.save() @@ -100,12 +111,19 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_parent(self): - parent_regions = Region.objects.filter(parent__isnull=True)[:2] - params = {'parent_id': [parent_regions[0].pk, parent_regions[1].pk]} + regions = Region.objects.filter(parent__isnull=True)[:2] + params = {'parent_id': [regions[0].pk, regions[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) - params = {'parent': [parent_regions[0].slug, parent_regions[1].slug]} + params = {'parent': [regions[0].slug, regions[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_ancestor(self): + regions = Region.objects.filter(parent__isnull=True)[:2] + params = {'ancestor_id': [regions[0].pk, regions[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + params = {'ancestor': [regions[0].slug, regions[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = SiteGroup.objects.all() @@ -114,24 +132,35 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests): @classmethod def setUpTestData(cls): - sitegroups = ( + parent_groups = ( SiteGroup(name='Site Group 1', slug='site-group-1', description='foobar1'), SiteGroup(name='Site Group 2', slug='site-group-2', description='foobar2'), SiteGroup(name='Site Group 3', slug='site-group-3', description='foobar3'), ) - for sitegroup in sitegroups: - sitegroup.save() + for site_group in parent_groups: + site_group.save() - child_sitegroups = ( - SiteGroup(name='Site Group 1A', slug='site-group-1a', parent=sitegroups[0]), - SiteGroup(name='Site Group 1B', slug='site-group-1b', parent=sitegroups[0]), - SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=sitegroups[1]), - SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=sitegroups[1]), - SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=sitegroups[2]), - SiteGroup(name='Site Group 3B', slug='site-group-3b', parent=sitegroups[2]), + groups = ( + SiteGroup(name='Site Group 1A', slug='site-group-1a', parent=parent_groups[0]), + SiteGroup(name='Site Group 1B', slug='site-group-1b', parent=parent_groups[0]), + SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=parent_groups[1]), + SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=parent_groups[1]), + SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=parent_groups[2]), + SiteGroup(name='Site Group 3B', slug='site-group-3b', parent=parent_groups[2]), ) - for sitegroup in child_sitegroups: - sitegroup.save() + for site_group in groups: + site_group.save() + + child_groups = ( + SiteGroup(name='Site Group 1A1', slug='site-group-1a1', parent=groups[0]), + SiteGroup(name='Site Group 1B1', slug='site-group-1b1', parent=groups[1]), + SiteGroup(name='Site Group 2A1', slug='site-group-2a1', parent=groups[2]), + SiteGroup(name='Site Group 2B1', slug='site-group-2b1', parent=groups[3]), + SiteGroup(name='Site Group 3A1', slug='site-group-3a1', parent=groups[4]), + SiteGroup(name='Site Group 3B1', slug='site-group-3b1', parent=groups[5]), + ) + for site_group in child_groups: + site_group.save() def test_q(self): params = {'q': 'foobar1'} @@ -150,12 +179,19 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_parent(self): - parent_sitegroups = SiteGroup.objects.filter(parent__isnull=True)[:2] - params = {'parent_id': [parent_sitegroups[0].pk, parent_sitegroups[1].pk]} + site_groups = SiteGroup.objects.filter(parent__isnull=True)[:2] + params = {'parent_id': [site_groups[0].pk, site_groups[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) - params = {'parent': [parent_sitegroups[0].slug, parent_sitegroups[1].slug]} + params = {'parent': [site_groups[0].slug, site_groups[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_ancestor(self): + site_groups = SiteGroup.objects.filter(parent__isnull=True)[:2] + params = {'ancestor_id': [site_groups[0].pk, site_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + params = {'ancestor': [site_groups[0].slug, site_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + class SiteTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Site.objects.all() @@ -314,21 +350,29 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests): Site.objects.bulk_create(sites) parent_locations = ( - Location(name='Parent Location 1', slug='parent-location-1', site=sites[0]), - Location(name='Parent Location 2', slug='parent-location-2', site=sites[1]), - Location(name='Parent Location 3', slug='parent-location-3', site=sites[2]), + Location(name='Location 1', slug='location-1', site=sites[0]), + Location(name='Location 2', slug='location-2', site=sites[1]), + Location(name='Location 3', slug='location-3', site=sites[2]), ) for location in parent_locations: location.save() locations = ( - Location(name='Location 1', slug='location-1', site=sites[0], parent=parent_locations[0], status=LocationStatusChoices.STATUS_PLANNED, description='foobar1'), - Location(name='Location 2', slug='location-2', site=sites[1], parent=parent_locations[1], status=LocationStatusChoices.STATUS_STAGING, description='foobar2'), - Location(name='Location 3', slug='location-3', site=sites[2], parent=parent_locations[2], status=LocationStatusChoices.STATUS_DECOMMISSIONING, description='foobar3'), + Location(name='Location 1A', slug='location-1a', site=sites[0], parent=parent_locations[0], status=LocationStatusChoices.STATUS_PLANNED, description='foobar1'), + Location(name='Location 2A', slug='location-2a', site=sites[1], parent=parent_locations[1], status=LocationStatusChoices.STATUS_STAGING, description='foobar2'), + Location(name='Location 3A', slug='location-3a', site=sites[2], parent=parent_locations[2], status=LocationStatusChoices.STATUS_DECOMMISSIONING, description='foobar3'), ) for location in locations: location.save() + child_locations = ( + Location(name='Location 1A1', slug='location-1a1', site=sites[0], parent=locations[0]), + Location(name='Location 2A1', slug='location-2a1', site=sites[1], parent=locations[1]), + Location(name='Location 3A1', slug='location-3a1', site=sites[2], parent=locations[2]), + ) + for location in child_locations: + location.save() + def test_q(self): params = {'q': 'foobar1'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -352,31 +396,38 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests): def test_region(self): regions = Region.objects.all()[:2] params = {'region_id': [regions[0].pk, regions[1].pk]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) params = {'region': [regions[0].slug, regions[1].slug]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) def test_site_group(self): site_groups = SiteGroup.objects.all()[:2] params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) params = {'site_group': [site_groups[0].slug, site_groups[1].slug]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) def test_site(self): sites = Site.objects.all()[:2] params = {'site_id': [sites[0].pk, sites[1].pk]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) params = {'site': [sites[0].slug, sites[1].slug]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) def test_parent(self): - parent_groups = Location.objects.filter(name__startswith='Parent')[:2] - params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]} + locations = Location.objects.filter(parent__isnull=True)[:2] + params = {'parent_id': [locations[0].pk, locations[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]} + params = {'parent': [locations[0].slug, locations[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_ancestor(self): + locations = Location.objects.filter(parent__isnull=True)[:2] + params = {'ancestor_id': [locations[0].pk, locations[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'ancestor': [locations[0].slug, locations[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = RackRole.objects.all() diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index 295d20774..7af3dc082 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -26,12 +26,25 @@ __all__ = ( class ContactGroupFilterSet(OrganizationalModelFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( queryset=ContactGroup.objects.all(), - label=_('Contact group (ID)'), + label=_('Parent contact group (ID)'), ) parent = django_filters.ModelMultipleChoiceFilter( field_name='parent__slug', queryset=ContactGroup.objects.all(), to_field_name='slug', + label=_('Parent contact group (slug)'), + ) + ancestor_id = TreeNodeMultipleChoiceFilter( + queryset=ContactGroup.objects.all(), + field_name='parent', + lookup_expr='in', + label=_('Contact group (ID)'), + ) + ancestor = TreeNodeMultipleChoiceFilter( + queryset=ContactGroup.objects.all(), + field_name='parent', + lookup_expr='in', + to_field_name='slug', label=_('Contact group (slug)'), ) @@ -155,12 +168,25 @@ class ContactModelFilterSet(django_filters.FilterSet): class TenantGroupFilterSet(OrganizationalModelFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( queryset=TenantGroup.objects.all(), - label=_('Tenant group (ID)'), + label=_('Parent tenant group (ID)'), ) parent = django_filters.ModelMultipleChoiceFilter( field_name='parent__slug', queryset=TenantGroup.objects.all(), to_field_name='slug', + label=_('Parent tenant group (slug)'), + ) + ancestor_id = TreeNodeMultipleChoiceFilter( + queryset=TenantGroup.objects.all(), + field_name='parent', + lookup_expr='in', + label=_('Tenant group (ID)'), + ) + ancestor = TreeNodeMultipleChoiceFilter( + queryset=TenantGroup.objects.all(), + field_name='parent', + lookup_expr='in', + to_field_name='slug', label=_('Tenant group (slug)'), ) diff --git a/netbox/tenancy/tests/test_filtersets.py b/netbox/tenancy/tests/test_filtersets.py index 3bcbddd4b..f6890a3d4 100644 --- a/netbox/tenancy/tests/test_filtersets.py +++ b/netbox/tenancy/tests/test_filtersets.py @@ -15,35 +15,43 @@ class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests): def setUpTestData(cls): parent_tenant_groups = ( - TenantGroup(name='Parent Tenant Group 1', slug='parent-tenant-group-1'), - TenantGroup(name='Parent Tenant Group 2', slug='parent-tenant-group-2'), - TenantGroup(name='Parent Tenant Group 3', slug='parent-tenant-group-3'), + TenantGroup(name='Tenant Group 1', slug='tenant-group-1'), + TenantGroup(name='Tenant Group 2', slug='tenant-group-2'), + TenantGroup(name='Tenant Group 3', slug='tenant-group-3'), ) - for tenantgroup in parent_tenant_groups: - tenantgroup.save() + for tenant_group in parent_tenant_groups: + tenant_group.save() tenant_groups = ( TenantGroup( - name='Tenant Group 1', - slug='tenant-group-1', + name='Tenant Group 1A', + slug='tenant-group-1a', parent=parent_tenant_groups[0], description='foobar1' ), TenantGroup( - name='Tenant Group 2', - slug='tenant-group-2', + name='Tenant Group 2A', + slug='tenant-group-2a', parent=parent_tenant_groups[1], description='foobar2' ), TenantGroup( - name='Tenant Group 3', - slug='tenant-group-3', + name='Tenant Group 3A', + slug='tenant-group-3a', parent=parent_tenant_groups[2], description='foobar3' ), ) - for tenantgroup in tenant_groups: - tenantgroup.save() + for tenant_group in tenant_groups: + tenant_group.save() + + child_tenant_groups = ( + TenantGroup(name='Tenant Group 1A1', slug='tenant-group-1a1', parent=tenant_groups[0]), + TenantGroup(name='Tenant Group 2A1', slug='tenant-group-2a1', parent=tenant_groups[1]), + TenantGroup(name='Tenant Group 3A1', slug='tenant-group-3a1', parent=tenant_groups[2]), + ) + for tenant_group in child_tenant_groups: + tenant_group.save() def test_q(self): params = {'q': 'foobar1'} @@ -62,12 +70,19 @@ class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_parent(self): - parent_groups = TenantGroup.objects.filter(name__startswith='Parent')[:2] - params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]} + tenant_groups = TenantGroup.objects.filter(parent__isnull=True)[:2] + params = {'parent_id': [tenant_groups[0].pk, tenant_groups[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]} + params = {'parent': [tenant_groups[0].slug, tenant_groups[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_ancestor(self): + tenant_groups = TenantGroup.objects.filter(parent__isnull=True)[:2] + params = {'ancestor_id': [tenant_groups[0].pk, tenant_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'ancestor': [tenant_groups[0].slug, tenant_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + class TenantTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Tenant.objects.all() @@ -123,35 +138,43 @@ class ContactGroupTestCase(TestCase, ChangeLoggedFilterSetTests): def setUpTestData(cls): parent_contact_groups = ( - ContactGroup(name='Parent Contact Group 1', slug='parent-contact-group-1'), - ContactGroup(name='Parent Contact Group 2', slug='parent-contact-group-2'), - ContactGroup(name='Parent Contact Group 3', slug='parent-contact-group-3'), + ContactGroup(name='Contact Group 1', slug='contact-group-1'), + ContactGroup(name='Contact Group 2', slug='contact-group-2'), + ContactGroup(name='Contact Group 3', slug='contact-group-3'), ) - for contactgroup in parent_contact_groups: - contactgroup.save() + for contact_group in parent_contact_groups: + contact_group.save() contact_groups = ( ContactGroup( - name='Contact Group 1', - slug='contact-group-1', + name='Contact Group 1A', + slug='contact-group-1a', parent=parent_contact_groups[0], description='foobar1' ), ContactGroup( - name='Contact Group 2', - slug='contact-group-2', + name='Contact Group 2A', + slug='contact-group-2a', parent=parent_contact_groups[1], description='foobar2' ), ContactGroup( - name='Contact Group 3', - slug='contact-group-3', + name='Contact Group 3A', + slug='contact-group-3a', parent=parent_contact_groups[2], description='foobar3' ), ) - for contactgroup in contact_groups: - contactgroup.save() + for contact_group in contact_groups: + contact_group.save() + + child_contact_groups = ( + ContactGroup(name='Contact Group 1A1', slug='contact-group-1a1', parent=contact_groups[0]), + ContactGroup(name='Contact Group 2A1', slug='contact-group-2a1', parent=contact_groups[1]), + ContactGroup(name='Contact Group 3A1', slug='contact-group-3a1', parent=contact_groups[2]), + ) + for contact_group in child_contact_groups: + contact_group.save() def test_q(self): params = {'q': 'foobar1'} @@ -170,12 +193,19 @@ class ContactGroupTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_parent(self): - parent_groups = ContactGroup.objects.filter(parent__isnull=True)[:2] - params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]} + contact_groups = ContactGroup.objects.filter(parent__isnull=True)[:2] + params = {'parent_id': [contact_groups[0].pk, contact_groups[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]} + params = {'parent': [contact_groups[0].slug, contact_groups[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_ancestor(self): + contact_groups = ContactGroup.objects.filter(parent__isnull=True)[:2] + params = {'ancestor_id': [contact_groups[0].pk, contact_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'ancestor': [contact_groups[0].slug, contact_groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + class ContactRoleTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ContactRole.objects.all() diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index 6ffb9cb91..50b1f78b1 100644 --- a/netbox/wireless/filtersets.py +++ b/netbox/wireless/filtersets.py @@ -25,6 +25,17 @@ class WirelessLANGroupFilterSet(OrganizationalModelFilterSet): queryset=WirelessLANGroup.objects.all(), to_field_name='slug' ) + ancestor_id = TreeNodeMultipleChoiceFilter( + queryset=WirelessLANGroup.objects.all(), + field_name='parent', + lookup_expr='in' + ) + ancestor = TreeNodeMultipleChoiceFilter( + queryset=WirelessLANGroup.objects.all(), + field_name='parent', + lookup_expr='in', + to_field_name='slug' + ) class Meta: model = WirelessLANGroup diff --git a/netbox/wireless/tests/test_filtersets.py b/netbox/wireless/tests/test_filtersets.py index 4184d5392..78e50edb7 100644 --- a/netbox/wireless/tests/test_filtersets.py +++ b/netbox/wireless/tests/test_filtersets.py @@ -17,21 +17,32 @@ class WirelessLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests): @classmethod def setUpTestData(cls): - groups = ( + parent_groups = ( WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1', description='A'), WirelessLANGroup(name='Wireless LAN Group 2', slug='wireless-lan-group-2', description='B'), WirelessLANGroup(name='Wireless LAN Group 3', slug='wireless-lan-group-3', description='C'), ) + for group in parent_groups: + group.save() + + groups = ( + WirelessLANGroup(name='Wireless LAN Group 1A', slug='wireless-lan-group-1a', parent=parent_groups[0], description='foobar1'), + WirelessLANGroup(name='Wireless LAN Group 1B', slug='wireless-lan-group-1b', parent=parent_groups[0], description='foobar2'), + WirelessLANGroup(name='Wireless LAN Group 2A', slug='wireless-lan-group-2a', parent=parent_groups[1]), + WirelessLANGroup(name='Wireless LAN Group 2B', slug='wireless-lan-group-2b', parent=parent_groups[1]), + WirelessLANGroup(name='Wireless LAN Group 3A', slug='wireless-lan-group-3a', parent=parent_groups[2]), + WirelessLANGroup(name='Wireless LAN Group 3B', slug='wireless-lan-group-3b', parent=parent_groups[2]), + ) for group in groups: group.save() child_groups = ( - WirelessLANGroup(name='Wireless LAN Group 1A', slug='wireless-lan-group-1a', parent=groups[0], description='foobar1'), - WirelessLANGroup(name='Wireless LAN Group 1B', slug='wireless-lan-group-1b', parent=groups[0], description='foobar2'), - WirelessLANGroup(name='Wireless LAN Group 2A', slug='wireless-lan-group-2a', parent=groups[1]), - WirelessLANGroup(name='Wireless LAN Group 2B', slug='wireless-lan-group-2b', parent=groups[1]), - WirelessLANGroup(name='Wireless LAN Group 3A', slug='wireless-lan-group-3a', parent=groups[2]), - WirelessLANGroup(name='Wireless LAN Group 3B', slug='wireless-lan-group-3b', parent=groups[2]), + WirelessLANGroup(name='Wireless LAN Group 1A1', slug='wireless-lan-group-1a1', parent=groups[0]), + WirelessLANGroup(name='Wireless LAN Group 1B1', slug='wireless-lan-group-1b1', parent=groups[1]), + WirelessLANGroup(name='Wireless LAN Group 2A1', slug='wireless-lan-group-2a1', parent=groups[2]), + WirelessLANGroup(name='Wireless LAN Group 2B1', slug='wireless-lan-group-2b1', parent=groups[3]), + WirelessLANGroup(name='Wireless LAN Group 3A1', slug='wireless-lan-group-3a1', parent=groups[4]), + WirelessLANGroup(name='Wireless LAN Group 3B1', slug='wireless-lan-group-3b1', parent=groups[5]), ) for group in child_groups: group.save() @@ -48,17 +59,24 @@ class WirelessLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'slug': ['wireless-lan-group-1', 'wireless-lan-group-2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_parent(self): - parent_groups = WirelessLANGroup.objects.filter(parent__isnull=True)[:2] - params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) - params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) - def test_description(self): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_parent(self): + groups = WirelessLANGroup.objects.filter(parent__isnull=True)[:2] + params = {'parent_id': [groups[0].pk, groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'parent': [groups[0].slug, groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + def test_ancestor(self): + groups = WirelessLANGroup.objects.filter(parent__isnull=True)[:2] + params = {'ancestor_id': [groups[0].pk, groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + params = {'ancestor': [groups[0].slug, groups[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = WirelessLAN.objects.all() From 52bda9c0e688952d8770985aff3d0a33be062d4d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 12 Mar 2024 11:20:23 -0400 Subject: [PATCH 25/25] Closes #15401: Rename PostgreSQL tables & indexes for L2VPN models (#15405) * Closes #15401: Rename PostgreSQL tables & indexes for L2VPN models * Account for alternate index name --- netbox/vpn/migrations/0005_rename_indexes.py | 44 ++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 netbox/vpn/migrations/0005_rename_indexes.py diff --git a/netbox/vpn/migrations/0005_rename_indexes.py b/netbox/vpn/migrations/0005_rename_indexes.py new file mode 100644 index 000000000..805b380cc --- /dev/null +++ b/netbox/vpn/migrations/0005_rename_indexes.py @@ -0,0 +1,44 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('vpn', '0004_alter_ikepolicy_mode'), + ] + + operations = [ + + # Rename vpn_l2vpn constraints + migrations.RunSQL("ALTER TABLE vpn_l2vpn RENAME CONSTRAINT ipam_l2vpn_tenant_id_bb2564a6_fk_tenancy_tenant_id TO vpn_l2vpn_tenant_id_57ec8f92_fk_tenancy_tenant_id"), + + # Rename ipam_l2vpn_* sequences + migrations.RunSQL("ALTER TABLE ipam_l2vpn_export_targets_id_seq RENAME TO vpn_l2vpn_export_targets_id_seq"), + migrations.RunSQL("ALTER TABLE ipam_l2vpn_id_seq RENAME TO vpn_l2vpn_id_seq"), + migrations.RunSQL("ALTER TABLE ipam_l2vpn_import_targets_id_seq RENAME TO vpn_l2vpn_import_targets_id_seq"), + + # Rename ipam_l2vpn_* indexes + migrations.RunSQL("ALTER INDEX ipam_l2vpn_pkey RENAME TO vpn_l2vpn_pkey"), + migrations.RunSQL("ALTER INDEX ipam_l2vpn_name_5e1c080f_like RENAME TO vpn_l2vpn_name_8824eda5_like"), + migrations.RunSQL("ALTER INDEX ipam_l2vpn_name_key RENAME TO vpn_l2vpn_name_key"), + migrations.RunSQL("ALTER INDEX ipam_l2vpn_slug_24008406_like RENAME TO vpn_l2vpn_slug_76b5a174_like"), + migrations.RunSQL("ALTER INDEX ipam_l2vpn_tenant_id_bb2564a6 RENAME TO vpn_l2vpn_tenant_id_57ec8f92"), + # The unique index for L2VPN.slug may have one of two names, depending on how it was created, + # so we check for both. + migrations.RunSQL("ALTER INDEX IF EXISTS ipam_l2vpn_slug_24008406_uniq RENAME TO vpn_l2vpn_slug_76b5a174_uniq"), + migrations.RunSQL("ALTER INDEX IF EXISTS ipam_l2vpn_slug_key RENAME TO vpn_l2vpn_slug_key"), + + # Rename vpn_l2vpntermination constraints + migrations.RunSQL("ALTER TABLE vpn_l2vpntermination RENAME CONSTRAINT ipam_l2vpntermination_assigned_object_id_check TO vpn_l2vpntermination_assigned_object_id_check"), + migrations.RunSQL("ALTER TABLE vpn_l2vpntermination RENAME CONSTRAINT ipam_l2vpnterminatio_assigned_object_type_3923c124_fk_django_co TO vpn_l2vpntermination_assigned_object_type_id_f063b865_fk_django_co"), + migrations.RunSQL("ALTER TABLE vpn_l2vpntermination RENAME CONSTRAINT ipam_l2vpntermination_l2vpn_id_9e570aa1_fk_ipam_l2vpn_id TO vpn_l2vpntermination_l2vpn_id_f5367bbe_fk_vpn_l2vpn_id"), + + # Rename ipam_l2vpn_termination_* sequences + migrations.RunSQL("ALTER TABLE ipam_l2vpntermination_id_seq RENAME TO vpn_l2vpntermination_id_seq"), + + # Rename ipam_l2vpn_* indexes + migrations.RunSQL("ALTER INDEX ipam_l2vpntermination_pkey RENAME TO vpn_l2vpntermination_pkey"), + migrations.RunSQL("ALTER INDEX ipam_l2vpntermination_assigned_object_type_id_3923c124 RENAME TO vpn_l2vpntermination_assigned_object_type_id_f063b865"), + migrations.RunSQL("ALTER INDEX ipam_l2vpntermination_l2vpn_id_9e570aa1 RENAME TO vpn_l2vpntermination_l2vpn_id_f5367bbe"), + + ]