Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #14153: Filter ContentTypes by supported feature #14191

Merged
merged 6 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions netbox/core/forms/filtersets.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _

from core.choices import *
from core.models import *
from extras.forms.mixins import SavedFiltersMixin
from extras.utils import FeatureQuery
from netbox.forms import NetBoxModelFilterSetForm
from netbox.utils import get_data_backend_choices
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
Expand Down Expand Up @@ -69,7 +67,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
)
object_type = ContentTypeChoiceField(
label=_('Object Type'),
queryset=ContentType.objects.filter(FeatureQuery('jobs').get_query()),
queryset=ContentType.objects.with_feature('jobs'),
required=False,
)
status = forms.MultipleChoiceField(
Expand Down
3 changes: 1 addition & 2 deletions netbox/core/migrations/0003_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import extras.utils


class Migration(migrations.Migration):
Expand All @@ -30,7 +29,7 @@ class Migration(migrations.Migration):
('status', models.CharField(default='pending', max_length=30)),
('data', models.JSONField(blank=True, null=True)),
('job_id', models.UUIDField(unique=True)),
('object_type', models.ForeignKey(limit_choices_to=extras.utils.FeatureQuery('jobs'), on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='contenttypes.contenttype')),
('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='contenttypes.contenttype')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
Expand Down
18 changes: 18 additions & 0 deletions netbox/core/models/contenttypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ def public(self):
q |= Q(app_label=app_label, model__in=models)
return self.get_queryset().filter(q)

def with_feature(self, feature):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a part of this PR, but I think we should have a FR to document the registry as it is getting a bit complicated now, for example for a model-feature what it is for, you need to do the following steps (like in the clean validate the content-type), etc...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a page in the developer docs that explains each store. Happy to flesh this out some. (And your comment reminded me to go add the new tables store introduced under #14173.)

"""
Return the ContentTypes only for models which are registered as supporting the specified feature. For example,
we can find all ContentTypes for models which support webhooks with

ContentType.objects.with_feature('webhooks')
"""
if feature not in registry['model_features']:
raise KeyError(
f"{feature} is not a registered model feature! Valid features are: {registry['model_features'].keys()}"
)

q = Q()
for app_label, models in registry['model_features'][feature].items():
q |= Q(app_label=app_label, model__in=models)

return self.get_queryset().filter(q)


class ContentType(ContentType_):
"""
Expand Down
3 changes: 1 addition & 2 deletions netbox/core/models/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db import models
Expand Down Expand Up @@ -368,7 +367,7 @@ class AutoSyncRecord(models.Model):
related_name='+'
)
object_type = models.ForeignKey(
to=ContentType,
to='contenttypes.ContentType',
on_delete=models.CASCADE,
related_name='+'
)
Expand Down
16 changes: 12 additions & 4 deletions netbox/core/models/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
import django_rq
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext as _

from core.choices import JobStatusChoices
from core.models import ContentType
from extras.constants import EVENT_JOB_END, EVENT_JOB_START
from extras.utils import FeatureQuery
from netbox.config import get_config
from netbox.constants import RQ_QUEUE_DEFAULT
from utilities.querysets import RestrictedQuerySet
Expand All @@ -28,9 +28,8 @@ class Job(models.Model):
Tracks the lifecycle of a job which represents a background task (e.g. the execution of a custom script).
"""
object_type = models.ForeignKey(
to=ContentType,
to='contenttypes.ContentType',
related_name='jobs',
limit_choices_to=FeatureQuery('jobs'),
on_delete=models.CASCADE,
)
object_id = models.PositiveBigIntegerField(
Expand Down Expand Up @@ -123,6 +122,15 @@ def get_absolute_url(self):
def get_status_color(self):
return JobStatusChoices.colors.get(self.status)

def clean(self):
super().clean()

# Validate the assigned object type
if self.object_type not in ContentType.objects.with_feature('jobs'):
raise ValidationError(
_("Jobs cannot be assigned to this object type ({type}).").format(type=self.object_type)
)

@property
def duration(self):
if not self.completed:
Expand Down
5 changes: 2 additions & 3 deletions netbox/dcim/models/cables.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@
from collections import defaultdict

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Sum
from django.dispatch import Signal
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

from core.models import ContentType
from dcim.choices import *
from dcim.constants import *
from dcim.fields import PathField
from dcim.utils import decompile_path_node, object_to_path_node
from netbox.models import ChangeLoggedModel, PrimaryModel

from utilities.fields import ColorField
from utilities.querysets import RestrictedQuerySet
from utilities.utils import to_meters
Expand Down Expand Up @@ -247,7 +246,7 @@ class CableTermination(ChangeLoggedModel):
verbose_name=_('end')
)
termination_type = models.ForeignKey(
to=ContentType,
to='contenttypes.ContentType',
limit_choices_to=CABLE_TERMINATION_MODELS,
on_delete=models.PROTECT,
related_name='+'
Expand Down
3 changes: 1 addition & 2 deletions netbox/dcim/models/device_component_templates.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
Expand Down Expand Up @@ -709,7 +708,7 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
db_index=True
)
component_type = models.ForeignKey(
to=ContentType,
to='contenttypes.ContentType',
limit_choices_to=MODULAR_COMPONENT_TEMPLATE_MODELS,
on_delete=models.PROTECT,
related_name='+',
Expand Down
3 changes: 1 addition & 2 deletions netbox/dcim/models/device_components.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from functools import cached_property

from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
Expand Down Expand Up @@ -1181,7 +1180,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
db_index=True
)
component_type = models.ForeignKey(
to=ContentType,
to='contenttypes.ContentType',
limit_choices_to=MODULAR_COMPONENT_MODELS,
on_delete=models.PROTECT,
related_name='+',
Expand Down
15 changes: 7 additions & 8 deletions netbox/extras/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers

from core.api.serializers import JobSerializer
from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer
from core.models import ContentType
from dcim.api.nested_serializers import (
NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer,
NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
Expand All @@ -14,7 +14,6 @@
from drf_spectacular.types import OpenApiTypes
from extras.choices import *
from extras.models import *
from extras.utils import FeatureQuery
from netbox.api.exceptions import SerializerNotFound
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
Expand Down Expand Up @@ -64,7 +63,7 @@
class WebhookSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
content_types = ContentTypeField(
queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()),
queryset=ContentType.objects.with_feature('webhooks'),
many=True
)

Expand All @@ -85,7 +84,7 @@ class Meta:
class CustomFieldSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
content_types = ContentTypeField(
queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
queryset=ContentType.objects.with_feature('custom_fields'),
many=True
)
type = ChoiceField(choices=CustomFieldTypeChoices)
Expand Down Expand Up @@ -151,7 +150,7 @@ class Meta:
class CustomLinkSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
content_types = ContentTypeField(
queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query()),
queryset=ContentType.objects.with_feature('custom_links'),
many=True
)

Expand All @@ -170,7 +169,7 @@ class Meta:
class ExportTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
content_types = ContentTypeField(
queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
queryset=ContentType.objects.with_feature('export_templates'),
many=True
)
data_source = NestedDataSourceSerializer(
Expand Down Expand Up @@ -215,7 +214,7 @@ class Meta:
class BookmarkSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
object_type = ContentTypeField(
queryset=ContentType.objects.filter(FeatureQuery('bookmarks').get_query()),
queryset=ContentType.objects.with_feature('bookmarks'),
)
object = serializers.SerializerMethodField(read_only=True)
user = NestedUserSerializer()
Expand All @@ -239,7 +238,7 @@ def get_object(self, instance):
class TagSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
object_types = ContentTypeField(
queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
queryset=ContentType.objects.with_feature('tags'),
many=True,
required=False
)
Expand Down
16 changes: 11 additions & 5 deletions netbox/extras/dashboard/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,20 @@
)


def get_content_type_labels():
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')
]


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')
]


def get_models_from_content_types(content_types):
"""
Return a list of models corresponding to the given content types, identified by natural key.
Expand Down Expand Up @@ -158,7 +165,7 @@ class ObjectCountsWidget(DashboardWidget):

class ConfigForm(WidgetConfigForm):
models = forms.MultipleChoiceField(
choices=get_content_type_labels
choices=get_object_type_choices
)
filters = forms.JSONField(
required=False,
Expand Down Expand Up @@ -207,7 +214,7 @@ class ObjectListWidget(DashboardWidget):

class ConfigForm(WidgetConfigForm):
model = forms.ChoiceField(
choices=get_content_type_labels
choices=get_object_type_choices
)
page_size = forms.IntegerField(
required=False,
Expand Down Expand Up @@ -343,8 +350,7 @@ class BookmarksWidget(DashboardWidget):

class ConfigForm(WidgetConfigForm):
object_types = forms.MultipleChoiceField(
# TODO: Restrict the choices by FeatureQuery('bookmarks')
choices=get_content_type_labels,
choices=get_bookmarks_object_type_choices,
required=False
)
order_by = forms.ChoiceField(
Expand Down
13 changes: 4 additions & 9 deletions netbox/extras/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from core.models import ContentType
from extras.choices import *
from extras.models import *
from extras.utils import FeatureQuery
from netbox.forms import NetBoxModelImportForm
from utilities.forms import CSVModelForm
from utilities.forms.fields import (
Expand All @@ -29,8 +28,7 @@
class CustomFieldImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField(
label=_('Content types'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'),
queryset=ContentType.objects.with_feature('custom_fields'),
help_text=_("One or more assigned object types")
)
type = CSVChoiceField(
Expand Down Expand Up @@ -88,8 +86,7 @@ class Meta:
class CustomLinkImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField(
label=_('Content types'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_links'),
queryset=ContentType.objects.with_feature('custom_links'),
help_text=_("One or more assigned object types")
)

Expand All @@ -104,8 +101,7 @@ class Meta:
class ExportTemplateImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField(
label=_('Content types'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('export_templates'),
queryset=ContentType.objects.with_feature('export_templates'),
help_text=_("One or more assigned object types")
)

Expand Down Expand Up @@ -142,8 +138,7 @@ class Meta:
class WebhookImportForm(NetBoxModelImportForm):
content_types = CSVMultipleContentTypeField(
label=_('Content types'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('webhooks'),
queryset=ContentType.objects.with_feature('webhooks'),
help_text=_("One or more assigned object types")
)

Expand Down
Loading