mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
* WIP * Remove FeatureQuery * Standardize use of proxy ContentType for models * Remove TODO * Correctly filter BookmarksWidget object_types choices * Add feature-specific object type validation
This commit is contained in:
@ -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
|
||||
@ -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(
|
||||
|
@ -4,7 +4,6 @@ from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import extras.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -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={
|
||||
|
@ -21,6 +21,24 @@ class ContentTypeManager(ContentTypeManager_):
|
||||
q |= Q(app_label=app_label, model__in=models)
|
||||
return self.get_queryset().filter(q)
|
||||
|
||||
def with_feature(self, feature):
|
||||
"""
|
||||
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_):
|
||||
"""
|
||||
|
@ -6,7 +6,6 @@ from urllib.parse import urlparse
|
||||
|
||||
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
|
||||
@ -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='+'
|
||||
)
|
||||
|
@ -3,7 +3,7 @@ import uuid
|
||||
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
|
||||
@ -11,8 +11,8 @@ 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
|
||||
@ -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(
|
||||
@ -123,6 +122,15 @@ class Job(models.Model):
|
||||
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:
|
||||
|
@ -2,7 +2,6 @@ import itertools
|
||||
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
|
||||
@ -10,12 +9,12 @@ 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
|
||||
@ -258,7 +257,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='+'
|
||||
|
@ -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
|
||||
@ -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='+',
|
||||
|
@ -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
|
||||
@ -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='+',
|
||||
|
@ -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,
|
||||
@ -14,7 +14,6 @@ from drf_spectacular.utils import extend_schema_field
|
||||
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
|
||||
@ -64,7 +63,7 @@ __all__ = (
|
||||
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
|
||||
)
|
||||
|
||||
@ -85,7 +84,7 @@ class WebhookSerializer(NetBoxModelSerializer):
|
||||
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)
|
||||
@ -151,7 +150,7 @@ class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
|
||||
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
|
||||
)
|
||||
|
||||
@ -170,7 +169,7 @@ class CustomLinkSerializer(ValidatedModelSerializer):
|
||||
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(
|
||||
@ -215,7 +214,7 @@ class SavedFilterSerializer(ValidatedModelSerializer):
|
||||
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()
|
||||
@ -239,7 +238,7 @@ class BookmarkSerializer(ValidatedModelSerializer):
|
||||
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
|
||||
)
|
||||
|
@ -32,13 +32,20 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
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.
|
||||
@ -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,
|
||||
@ -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,
|
||||
@ -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(
|
||||
|
@ -6,7 +6,6 @@ from django.utils.translation import gettext_lazy as _
|
||||
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 (
|
||||
@ -29,8 +28,7 @@ __all__ = (
|
||||
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(
|
||||
@ -88,8 +86,7 @@ class CustomFieldChoiceSetImportForm(CSVModelForm):
|
||||
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")
|
||||
)
|
||||
|
||||
@ -104,8 +101,7 @@ class CustomLinkImportForm(CSVModelForm):
|
||||
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")
|
||||
)
|
||||
|
||||
@ -142,8 +138,7 @@ class SavedFilterImportForm(CSVModelForm):
|
||||
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")
|
||||
)
|
||||
|
||||
|
@ -6,7 +6,6 @@ from core.models import ContentType, DataFile, DataSource
|
||||
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.forms.base import NetBoxModelFilterSetForm
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||
@ -44,7 +43,7 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
||||
)),
|
||||
)
|
||||
content_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
|
||||
queryset=ContentType.objects.with_feature('custom_fields'),
|
||||
required=False,
|
||||
label=_('Object type')
|
||||
)
|
||||
@ -108,7 +107,7 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
|
||||
)
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query()),
|
||||
queryset=ContentType.objects.with_feature('custom_links'),
|
||||
required=False
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
@ -151,7 +150,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
||||
}
|
||||
)
|
||||
content_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
|
||||
queryset=ContentType.objects.with_feature('export_templates'),
|
||||
required=False,
|
||||
label=_('Content types')
|
||||
)
|
||||
@ -179,7 +178,7 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
|
||||
)
|
||||
content_type_id = ContentTypeChoiceField(
|
||||
label=_('Content type'),
|
||||
queryset=ContentType.objects.filter(FeatureQuery('image_attachments').get_query()),
|
||||
queryset=ContentType.objects.with_feature('image_attachments'),
|
||||
required=False
|
||||
)
|
||||
name = forms.CharField(
|
||||
@ -228,7 +227,7 @@ class WebhookFilterForm(NetBoxModelFilterSetForm):
|
||||
(_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
|
||||
)
|
||||
content_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()),
|
||||
queryset=ContentType.objects.with_feature('webhooks'),
|
||||
required=False,
|
||||
label=_('Object type')
|
||||
)
|
||||
@ -284,12 +283,12 @@ class WebhookFilterForm(NetBoxModelFilterSetForm):
|
||||
class TagFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = Tag
|
||||
content_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
|
||||
queryset=ContentType.objects.with_feature('tags'),
|
||||
required=False,
|
||||
label=_('Tagged object type')
|
||||
)
|
||||
for_object_type_id = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
|
||||
queryset=ContentType.objects.with_feature('tags'),
|
||||
required=False,
|
||||
label=_('Allowed object type')
|
||||
)
|
||||
|
@ -10,7 +10,6 @@ from core.models import ContentType
|
||||
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.config import get_config, PARAMS
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
@ -43,8 +42,7 @@ __all__ = (
|
||||
class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
queryset=ContentType.objects.with_feature('custom_fields')
|
||||
)
|
||||
object_type = ContentTypeChoiceField(
|
||||
label=_('Object type'),
|
||||
@ -114,8 +112,7 @@ class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
|
||||
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_links')
|
||||
queryset=ContentType.objects.with_feature('custom_links')
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
@ -142,8 +139,7 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
||||
class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('export_templates')
|
||||
queryset=ContentType.objects.with_feature('export_templates')
|
||||
)
|
||||
template_code = forms.CharField(
|
||||
label=_('Template code'),
|
||||
@ -210,8 +206,7 @@ class SavedFilterForm(BootstrapMixin, forms.ModelForm):
|
||||
class BookmarkForm(BootstrapMixin, forms.ModelForm):
|
||||
object_type = ContentTypeChoiceField(
|
||||
label=_('Object type'),
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('bookmarks').get_query()
|
||||
queryset=ContentType.objects.with_feature('bookmarks')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -222,8 +217,7 @@ class BookmarkForm(BootstrapMixin, forms.ModelForm):
|
||||
class WebhookForm(NetBoxModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('webhooks')
|
||||
queryset=ContentType.objects.with_feature('webhooks')
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
@ -257,8 +251,7 @@ class TagForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
object_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Object types'),
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('tags'),
|
||||
queryset=ContentType.objects.with_feature('tags'),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
@ -88,7 +88,7 @@ class Migration(migrations.Migration):
|
||||
('secret', models.CharField(blank=True, max_length=255)),
|
||||
('ssl_verification', models.BooleanField(default=True)),
|
||||
('ca_file_path', models.CharField(blank=True, max_length=4096, null=True)),
|
||||
('content_types', models.ManyToManyField(limit_choices_to=extras.utils.FeatureQuery('webhooks'), related_name='webhooks', to='contenttypes.ContentType')),
|
||||
('content_types', models.ManyToManyField(related_name='webhooks', to='contenttypes.ContentType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('name',),
|
||||
@ -151,7 +151,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)),
|
||||
('obj_type', models.ForeignKey(limit_choices_to=extras.utils.FeatureQuery('jobs'), on_delete=django.db.models.deletion.CASCADE, related_name='job_results', to='contenttypes.contenttype')),
|
||||
('obj_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='job_results', 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={
|
||||
@ -184,7 +184,7 @@ class Migration(migrations.Migration):
|
||||
('mime_type', models.CharField(blank=True, max_length=50)),
|
||||
('file_extension', models.CharField(blank=True, max_length=15)),
|
||||
('as_attachment', models.BooleanField(default=True)),
|
||||
('content_type', models.ForeignKey(limit_choices_to=extras.utils.FeatureQuery('export_templates'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['content_type', 'name'],
|
||||
@ -201,7 +201,7 @@ class Migration(migrations.Migration):
|
||||
('group_name', models.CharField(blank=True, max_length=50)),
|
||||
('button_class', models.CharField(default='default', max_length=30)),
|
||||
('new_window', models.BooleanField(default=False)),
|
||||
('content_type', models.ForeignKey(limit_choices_to=extras.utils.FeatureQuery('custom_links'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['group_name', 'weight', 'name'],
|
||||
@ -223,7 +223,7 @@ class Migration(migrations.Migration):
|
||||
('validation_maximum', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('validation_regex', models.CharField(blank=True, max_length=500, validators=[utilities.validators.validate_regex])),
|
||||
('choices', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, null=True, size=None)),
|
||||
('content_types', models.ManyToManyField(limit_choices_to=extras.utils.FeatureQuery('custom_fields'), related_name='custom_fields', to='contenttypes.ContentType')),
|
||||
('content_types', models.ManyToManyField(related_name='custom_fields', to='contenttypes.ContentType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['weight', 'name'],
|
||||
|
@ -1,5 +1,4 @@
|
||||
from django.db import migrations, models
|
||||
import extras.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -13,7 +12,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='object_types',
|
||||
field=models.ManyToManyField(blank=True, limit_choices_to=extras.utils.FeatureQuery('tags'), related_name='+', to='contenttypes.contenttype'),
|
||||
field=models.ManyToManyField(blank=True, related_name='+', to='contenttypes.contenttype'),
|
||||
),
|
||||
migrations.RenameIndex(
|
||||
model_name='taggeditem',
|
||||
|
@ -1,10 +1,11 @@
|
||||
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.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ContentType
|
||||
from extras.choices import *
|
||||
from ..querysets import ObjectChangeQuerySet
|
||||
|
||||
@ -48,7 +49,7 @@ class ObjectChange(models.Model):
|
||||
choices=ObjectChangeActionChoices
|
||||
)
|
||||
changed_object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+'
|
||||
)
|
||||
@ -58,7 +59,7 @@ class ObjectChange(models.Model):
|
||||
fk_field='changed_object_id'
|
||||
)
|
||||
related_object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+',
|
||||
blank=True,
|
||||
@ -104,6 +105,17 @@ class ObjectChange(models.Model):
|
||||
self.user_name
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate the assigned object type
|
||||
if self.changed_object_type not in ContentType.objects.with_feature('change_logging'):
|
||||
raise ValidationError(
|
||||
_("Change logging is not supported for this object type ({type}).").format(
|
||||
type=self.changed_object_type
|
||||
)
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Record the user's name and the object's representation as static strings
|
||||
|
@ -5,18 +5,16 @@ from datetime import datetime, date
|
||||
import django_filters
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.validators import RegexValidator, ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ContentType
|
||||
from extras.choices import *
|
||||
from extras.data import CHOICE_SETS
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import CloningMixin, ExportTemplatesMixin
|
||||
from netbox.search import FieldTypes
|
||||
@ -60,9 +58,8 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
||||
|
||||
class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
content_types = models.ManyToManyField(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
related_name='custom_fields',
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
help_text=_('The object(s) to which this field applies.')
|
||||
)
|
||||
type = models.CharField(
|
||||
@ -73,7 +70,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
help_text=_('The type of data this custom field holds')
|
||||
)
|
||||
object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.PROTECT,
|
||||
blank=True,
|
||||
null=True,
|
||||
|
@ -3,7 +3,6 @@ import urllib.parse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.cache import cache
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
@ -14,10 +13,11 @@ from django.utils.formats import date_format
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
|
||||
from core.models import ContentType
|
||||
from extras.choices import *
|
||||
from extras.conditions import ConditionSet
|
||||
from extras.constants import *
|
||||
from extras.utils import FeatureQuery, image_upload
|
||||
from extras.utils import image_upload
|
||||
from netbox.config import get_config
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import (
|
||||
@ -45,10 +45,9 @@ class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedMo
|
||||
Each Webhook can be limited to firing only on certain actions or certain object types.
|
||||
"""
|
||||
content_types = models.ManyToManyField(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
related_name='webhooks',
|
||||
verbose_name=_('object types'),
|
||||
limit_choices_to=FeatureQuery('webhooks'),
|
||||
help_text=_("The object(s) to which this Webhook applies.")
|
||||
)
|
||||
name = models.CharField(
|
||||
@ -235,7 +234,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
code to be rendered with an object as context.
|
||||
"""
|
||||
content_types = models.ManyToManyField(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
related_name='custom_links',
|
||||
help_text=_('The object type(s) to which this link applies.')
|
||||
)
|
||||
@ -331,7 +330,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
|
||||
class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
content_types = models.ManyToManyField(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
related_name='export_templates',
|
||||
help_text=_('The object type(s) to which this template applies.')
|
||||
)
|
||||
@ -440,7 +439,7 @@ 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=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
related_name='saved_filters',
|
||||
help_text=_('The object type(s) to which this filter applies.')
|
||||
)
|
||||
@ -520,7 +519,7 @@ class ImageAttachment(ChangeLoggedModel):
|
||||
An uploaded image which is associated with an object.
|
||||
"""
|
||||
content_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
object_id = models.PositiveBigIntegerField()
|
||||
@ -560,6 +559,15 @@ class ImageAttachment(ChangeLoggedModel):
|
||||
filename = self.image.name.rsplit('/', 1)[-1]
|
||||
return filename.split('_', 2)[2]
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate the assigned object type
|
||||
if self.content_type not in ContentType.objects.with_feature('image_attachments'):
|
||||
raise ValidationError(
|
||||
_("Image attachments cannot be assigned to this object type ({type}).").format(type=self.content_type)
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
|
||||
_name = self.image.name
|
||||
@ -605,7 +613,7 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
|
||||
might record a new journal entry when a device undergoes maintenance, or when a prefix is expanded.
|
||||
"""
|
||||
assigned_object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
assigned_object_id = models.PositiveBigIntegerField()
|
||||
@ -644,9 +652,8 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Prevent the creation of journal entries on unsupported models
|
||||
permitted_types = ContentType.objects.filter(FeatureQuery('journaling').get_query())
|
||||
if self.assigned_object_type not in permitted_types:
|
||||
# Validate the assigned object type
|
||||
if self.assigned_object_type not in ContentType.objects.with_feature('journaling'):
|
||||
raise ValidationError(
|
||||
_("Journaling is not supported for this object type ({type}).").format(type=self.assigned_object_type)
|
||||
)
|
||||
@ -664,7 +671,7 @@ class Bookmark(models.Model):
|
||||
auto_now_add=True
|
||||
)
|
||||
object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
object_id = models.PositiveBigIntegerField()
|
||||
@ -695,6 +702,15 @@ class Bookmark(models.Model):
|
||||
return str(self.object)
|
||||
return super().__str__()
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate the assigned object type
|
||||
if self.object_type not in ContentType.objects.with_feature('bookmarks'):
|
||||
raise ValidationError(
|
||||
_("Bookmarks cannot be assigned to this object type ({type}).").format(type=self.object_type)
|
||||
)
|
||||
|
||||
|
||||
class ConfigRevision(models.Model):
|
||||
"""
|
||||
|
@ -1,6 +1,5 @@
|
||||
import uuid
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@ -27,7 +26,7 @@ class CachedValue(models.Model):
|
||||
editable=False
|
||||
)
|
||||
object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='+'
|
||||
)
|
||||
|
@ -2,7 +2,6 @@ import logging
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models, transaction
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@ -71,7 +70,7 @@ class StagedChange(ChangeLoggedModel):
|
||||
choices=ChangeActionChoices
|
||||
)
|
||||
object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='+'
|
||||
)
|
||||
|
@ -1,13 +1,10 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from taggit.models import TagBase, GenericTaggedItemBase
|
||||
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import CloningMixin, ExportTemplatesMixin
|
||||
from utilities.choices import ColorChoices
|
||||
@ -37,9 +34,8 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
|
||||
blank=True,
|
||||
)
|
||||
object_types = models.ManyToManyField(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
related_name='+',
|
||||
limit_choices_to=FeatureQuery('tags'),
|
||||
blank=True,
|
||||
help_text=_("The object type(s) to which this this tag can be applied.")
|
||||
)
|
||||
|
@ -1,5 +1,3 @@
|
||||
from django.db.models import Q
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from taggit.managers import _TaggableManager
|
||||
|
||||
from netbox.registry import registry
|
||||
@ -31,29 +29,6 @@ def image_upload(instance, filename):
|
||||
return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
|
||||
|
||||
|
||||
@deconstructible
|
||||
class FeatureQuery:
|
||||
"""
|
||||
Helper class that delays evaluation of the registry contents for the functionality store
|
||||
until it has been populated.
|
||||
"""
|
||||
def __init__(self, feature):
|
||||
self.feature = feature
|
||||
|
||||
def __call__(self):
|
||||
return self.get_query()
|
||||
|
||||
def get_query(self):
|
||||
"""
|
||||
Given an extras feature, return a Q object for content type lookup
|
||||
"""
|
||||
query = Q()
|
||||
for app_label, models in registry['model_features'][self.feature].items():
|
||||
query |= Q(app_label=app_label, model__in=models)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def register_features(model, features):
|
||||
"""
|
||||
Register model features in the application registry.
|
||||
|
@ -1,13 +1,12 @@
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.models import ChangeLoggedModel, PrimaryModel
|
||||
from ipam.choices import *
|
||||
from ipam.constants import *
|
||||
from netbox.models import ChangeLoggedModel, PrimaryModel
|
||||
|
||||
__all__ = (
|
||||
'FHRPGroup',
|
||||
@ -78,7 +77,7 @@ class FHRPGroup(PrimaryModel):
|
||||
|
||||
class FHRPGroupAssignment(ChangeLoggedModel):
|
||||
interface_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
interface_id = models.PositiveBigIntegerField()
|
||||
|
@ -1,6 +1,5 @@
|
||||
import netaddr
|
||||
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 F
|
||||
@ -9,6 +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 ipam.choices import *
|
||||
from ipam.constants import *
|
||||
from ipam.fields import IPNetworkField, IPAddressField
|
||||
@ -740,7 +740,7 @@ class IPAddress(PrimaryModel):
|
||||
help_text=_('The functional role of this IP')
|
||||
)
|
||||
assigned_object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
limit_choices_to=IPADDRESS_ASSIGNMENT_MODELS,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+',
|
||||
|
@ -1,11 +1,11 @@
|
||||
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.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ContentType
|
||||
from ipam.choices import L2VPNTypeChoices
|
||||
from ipam.constants import L2VPN_ASSIGNMENT_MODELS
|
||||
from netbox.models import NetBoxModel, PrimaryModel
|
||||
@ -86,7 +86,7 @@ class L2VPNTermination(NetBoxModel):
|
||||
related_name='terminations'
|
||||
)
|
||||
assigned_object_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
limit_choices_to=L2VPN_ASSIGNMENT_MODELS,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+'
|
||||
|
@ -1,5 +1,4 @@
|
||||
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
|
||||
@ -32,7 +31,7 @@ class VLANGroup(OrganizationalModel):
|
||||
max_length=100
|
||||
)
|
||||
scope_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.CASCADE,
|
||||
limit_choices_to=Q(model__in=VLANGROUP_SCOPE_TYPES),
|
||||
blank=True,
|
||||
|
@ -3,7 +3,6 @@ from collections import defaultdict
|
||||
from functools import cached_property
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models.signals import class_prepared
|
||||
@ -13,6 +12,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 extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices
|
||||
from extras.utils import is_taggable, register_features
|
||||
from netbox.registry import registry
|
||||
|
@ -1,8 +1,7 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from extras.utils import FeatureQuery
|
||||
from core.models import ContentType
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.choices import *
|
||||
from tenancy.models import *
|
||||
@ -87,8 +86,7 @@ class ContactAssignmentFilterForm(NetBoxModelFilterSetForm):
|
||||
(_('Assignment'), ('content_type_id', 'group_id', 'contact_id', 'role_id', 'priority')),
|
||||
)
|
||||
content_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('contacts'),
|
||||
queryset=ContentType.objects.with_feature('contacts'),
|
||||
required=False,
|
||||
label=_('Object type')
|
||||
)
|
||||
|
@ -1,9 +1,10 @@
|
||||
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.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ContentType
|
||||
from netbox.models import ChangeLoggedModel, NestedGroupModel, OrganizationalModel, PrimaryModel
|
||||
from netbox.models.features import CustomFieldsMixin, TagsMixin
|
||||
from tenancy.choices import *
|
||||
@ -111,7 +112,7 @@ class Contact(PrimaryModel):
|
||||
|
||||
class ContactAssignment(CustomFieldsMixin, TagsMixin, ChangeLoggedModel):
|
||||
content_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
object_id = models.PositiveBigIntegerField()
|
||||
@ -157,6 +158,15 @@ class ContactAssignment(CustomFieldsMixin, TagsMixin, ChangeLoggedModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('tenancy:contact', args=[self.contact.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate the assigned object type
|
||||
if self.content_type not in ContentType.objects.with_feature('contacts'):
|
||||
raise ValidationError(
|
||||
_("Contacts cannot be assigned to this object type ({type}).").format(type=self.content_type)
|
||||
)
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
objectchange.related_object = self.object
|
||||
|
@ -3,7 +3,6 @@ import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, GroupManager, User, UserManager
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinLengthValidator
|
||||
@ -15,6 +14,7 @@ from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netaddr import IPNetwork
|
||||
|
||||
from core.models import ContentType
|
||||
from ipam.fields import IPNetworkField
|
||||
from netbox.config import get_config
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
@ -353,7 +353,7 @@ class ObjectPermission(models.Model):
|
||||
default=True
|
||||
)
|
||||
object_types = models.ManyToManyField(
|
||||
to=ContentType,
|
||||
to='contenttypes.ContentType',
|
||||
limit_choices_to=OBJECTPERMISSION_OBJECT_TYPES,
|
||||
related_name='object_permissions'
|
||||
)
|
||||
|
Reference in New Issue
Block a user