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/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`
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/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/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/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/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/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 fd99ce703..09f247929 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.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
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 utilities.api import get_serializer_for_model
@@ -24,8 +24,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(content_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 = {}
@@ -46,8 +46,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(content_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):
@@ -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.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_/attachments.py b/netbox/extras/api/serializers_/attachments.py
index e26d8516b..bcf3a24ec 100644
--- a/netbox/extras/api/serializers_/attachments.py
+++ b/netbox/extras/api/serializers_/attachments.py
@@ -2,7 +2,7 @@ from django.core.exceptions import ObjectDoesNotExist
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
-from core.models import ContentType
+from core.models import ObjectType
from extras.models import ImageAttachment
from netbox.api.fields import ContentTypeField
from netbox.api.serializers import ValidatedModelSerializer
@@ -15,15 +15,15 @@ __all__ = (
class ImageAttachmentSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
- content_type = ContentTypeField(
- queryset=ContentType.objects.all()
+ object_type = ContentTypeField(
+ queryset=ObjectType.objects.all()
)
parent = serializers.SerializerMethodField(read_only=True)
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')
@@ -32,10 +32,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/api/serializers_/bookmarks.py b/netbox/extras/api/serializers_/bookmarks.py
index 8140d2d84..7a2d4d6aa 100644
--- a/netbox/extras/api/serializers_/bookmarks.py
+++ b/netbox/extras/api/serializers_/bookmarks.py
@@ -1,7 +1,7 @@
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
-from core.models import ContentType
+from core.models import ObjectType
from extras.models import Bookmark
from netbox.api.fields import ContentTypeField
from netbox.api.serializers import ValidatedModelSerializer
@@ -16,7 +16,7 @@ __all__ = (
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 = UserSerializer(nested=True)
diff --git a/netbox/extras/api/serializers_/customfields.py b/netbox/extras/api/serializers_/customfields.py
index 668e780d9..79bb39557 100644
--- a/netbox/extras/api/serializers_/customfields.py
+++ b/netbox/extras/api/serializers_/customfields.py
@@ -3,7 +3,7 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
-from core.models import ContentType
+from core.models import ObjectType
from extras.choices import *
from extras.models import CustomField, CustomFieldChoiceSet
from netbox.api.fields import ChoiceField, ContentTypeField
@@ -39,13 +39,13 @@ class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
class CustomFieldSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
- content_types = ContentTypeField(
- queryset=ContentType.objects.with_feature('custom_fields'),
+ object_types = ContentTypeField(
+ queryset=ObjectType.objects.with_feature('custom_fields'),
many=True
)
type = ChoiceField(choices=CustomFieldTypeChoices)
- object_type = ContentTypeField(
- queryset=ContentType.objects.all(),
+ 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', 'content_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/api/serializers_/customlinks.py b/netbox/extras/api/serializers_/customlinks.py
index 8f53db2ca..8635ea2a0 100644
--- a/netbox/extras/api/serializers_/customlinks.py
+++ b/netbox/extras/api/serializers_/customlinks.py
@@ -1,6 +1,6 @@
from rest_framework import serializers
-from core.models import ContentType
+from core.models import ObjectType
from extras.models import CustomLink
from netbox.api.fields import ContentTypeField
from netbox.api.serializers import ValidatedModelSerializer
@@ -12,15 +12,15 @@ __all__ = (
class CustomLinkSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
- content_types = ContentTypeField(
- queryset=ContentType.objects.with_feature('custom_links'),
+ object_types = ContentTypeField(
+ queryset=ObjectType.objects.with_feature('custom_links'),
many=True
)
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/api/serializers_/events.py b/netbox/extras/api/serializers_/events.py
index 6f369d63d..4285b12e6 100644
--- a/netbox/extras/api/serializers_/events.py
+++ b/netbox/extras/api/serializers_/events.py
@@ -2,7 +2,7 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
-from core.models import ContentType
+from core.models import ObjectType
from extras.choices import *
from extras.models import EventRule, Webhook
from netbox.api.fields import ChoiceField, ContentTypeField
@@ -22,20 +22,20 @@ __all__ = (
class EventRuleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
- content_types = ContentTypeField(
- queryset=ContentType.objects.with_feature('event_rules'),
+ object_types = ContentTypeField(
+ 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)
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/api/serializers_/exporttemplates.py b/netbox/extras/api/serializers_/exporttemplates.py
index 37e36fd55..43cc061a7 100644
--- a/netbox/extras/api/serializers_/exporttemplates.py
+++ b/netbox/extras/api/serializers_/exporttemplates.py
@@ -1,7 +1,7 @@
from rest_framework import serializers
from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
-from core.models import ContentType
+from core.models import ObjectType
from extras.models import ExportTemplate
from netbox.api.fields import ContentTypeField
from netbox.api.serializers import ValidatedModelSerializer
@@ -13,8 +13,8 @@ __all__ = (
class ExportTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
- content_types = ContentTypeField(
- queryset=ContentType.objects.with_feature('export_templates'),
+ object_types = ContentTypeField(
+ queryset=ObjectType.objects.with_feature('export_templates'),
many=True
)
data_source = DataSourceSerializer(
@@ -29,7 +29,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/api/serializers_/journaling.py b/netbox/extras/api/serializers_/journaling.py
index 848b2842a..46ab0477b 100644
--- a/netbox/extras/api/serializers_/journaling.py
+++ b/netbox/extras/api/serializers_/journaling.py
@@ -3,7 +3,7 @@ from django.core.exceptions import ObjectDoesNotExist
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
-from core.models import ContentType
+from core.models import ObjectType
from extras.choices import *
from extras.models import JournalEntry
from netbox.api.fields import ChoiceField, ContentTypeField
@@ -18,7 +18,7 @@ __all__ = (
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(
diff --git a/netbox/extras/api/serializers_/contenttypes.py b/netbox/extras/api/serializers_/objecttypes.py
similarity index 60%
rename from netbox/extras/api/serializers_/contenttypes.py
rename to netbox/extras/api/serializers_/objecttypes.py
index cc11e88b6..8e4806652 100644
--- a/netbox/extras/api/serializers_/contenttypes.py
+++ b/netbox/extras/api/serializers_/objecttypes.py
@@ -1,16 +1,16 @@
from rest_framework import serializers
-from core.models import ContentType
+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 = ContentType
+ model = ObjectType
fields = ['id', 'url', 'display', 'app_label', 'model']
diff --git a/netbox/extras/api/serializers_/savedfilters.py b/netbox/extras/api/serializers_/savedfilters.py
index cb27c0b0d..9e26f0c30 100644
--- a/netbox/extras/api/serializers_/savedfilters.py
+++ b/netbox/extras/api/serializers_/savedfilters.py
@@ -1,6 +1,6 @@
from rest_framework import serializers
-from core.models import ContentType
+from core.models import ObjectType
from extras.models import SavedFilter
from netbox.api.fields import ContentTypeField
from netbox.api.serializers import ValidatedModelSerializer
@@ -12,15 +12,15 @@ __all__ = (
class SavedFilterSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
- content_types = ContentTypeField(
- queryset=ContentType.objects.all(),
+ object_types = ContentTypeField(
+ queryset=ObjectType.objects.all(),
many=True
)
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/api/serializers_/tags.py b/netbox/extras/api/serializers_/tags.py
index 28a021f29..9d91ba5e1 100644
--- a/netbox/extras/api/serializers_/tags.py
+++ b/netbox/extras/api/serializers_/tags.py
@@ -1,6 +1,6 @@
from rest_framework import serializers
-from core.models import ContentType
+from core.models import ObjectType
from extras.models import Tag
from netbox.api.fields import ContentTypeField, RelatedObjectCountField
from netbox.api.serializers import ValidatedModelSerializer
@@ -13,7 +13,7 @@ __all__ = (
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
)
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/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/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 734f7db50..d88b8c9b3 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',
@@ -89,10 +89,12 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
method='search',
label=_('Search'),
)
- content_type_id = MultiValueNumberFilter(
- field_name='content_types__id'
+ object_type_id = MultiValueNumberFilter(
+ field_name='object_types__id'
+ )
+ object_type = ContentTypeFilter(
+ field_name='object_types'
)
- content_types = ContentTypeFilter()
action_type = django_filters.MultipleChoiceFilter(
choices=EventRuleActionChoices
)
@@ -124,10 +126,16 @@ class CustomFieldFilterSet(BaseFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=CustomFieldTypeChoices
)
- content_type_id = MultiValueNumberFilter(
- field_name='content_types__id'
+ object_type_id = MultiValueNumberFilter(
+ field_name='object_types__id'
)
- content_types = ContentTypeFilter()
+ 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()
)
@@ -140,8 +148,8 @@ class CustomFieldFilterSet(BaseFilterSet):
class Meta:
model = CustomField
fields = [
- 'id', 'content_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):
@@ -188,15 +196,17 @@ class CustomLinkFilterSet(BaseFilterSet):
method='search',
label=_('Search'),
)
- content_type_id = MultiValueNumberFilter(
- field_name='content_types__id'
+ object_type_id = MultiValueNumberFilter(
+ field_name='object_types__id'
+ )
+ object_type = ContentTypeFilter(
+ field_name='object_types'
)
- content_types = ContentTypeFilter()
class Meta:
model = CustomLink
fields = [
- 'id', 'content_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):
@@ -215,10 +225,12 @@ class ExportTemplateFilterSet(BaseFilterSet):
method='search',
label=_('Search'),
)
- content_type_id = MultiValueNumberFilter(
- field_name='content_types__id'
+ object_type_id = MultiValueNumberFilter(
+ field_name='object_types__id'
+ )
+ object_type = ContentTypeFilter(
+ field_name='object_types'
)
- content_types = ContentTypeFilter()
data_source_id = django_filters.ModelMultipleChoiceFilter(
queryset=DataSource.objects.all(),
label=_('Data source (ID)'),
@@ -230,7 +242,7 @@ class ExportTemplateFilterSet(BaseFilterSet):
class Meta:
model = ExportTemplate
- fields = ['id', 'content_types', 'name', 'description', 'data_synced']
+ fields = ['id', 'name', 'description', 'data_synced']
def search(self, queryset, name, value):
if not value.strip():
@@ -246,10 +258,12 @@ class SavedFilterFilterSet(BaseFilterSet):
method='search',
label=_('Search'),
)
- content_type_id = MultiValueNumberFilter(
- field_name='content_types__id'
+ object_type_id = MultiValueNumberFilter(
+ field_name='object_types__id'
+ )
+ object_type = ContentTypeFilter(
+ field_name='object_types'
)
- content_types = ContentTypeFilter()
user_id = django_filters.ModelMultipleChoiceFilter(
queryset=get_user_model().objects.all(),
label=_('User (ID)'),
@@ -266,7 +280,7 @@ class SavedFilterFilterSet(BaseFilterSet):
class Meta:
model = SavedFilter
- fields = ['id', 'content_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():
@@ -316,11 +330,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():
@@ -660,14 +674,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/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py
index 440600af5..55f71dbd2 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
@@ -30,9 +30,9 @@ __all__ = (
class CustomFieldImportForm(CSVModelForm):
- content_types = CSVMultipleContentTypeField(
- label=_('Content types'),
- queryset=ContentType.objects.with_feature('custom_fields'),
+ object_types = CSVMultipleContentTypeField(
+ label=_('Object types'),
+ queryset=ObjectType.objects.with_feature('custom_fields'),
help_text=_("One or more assigned object types")
)
type = CSVChoiceField(
@@ -40,9 +40,9 @@ 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=ContentType.objects.public(),
+ queryset=ObjectType.objects.public(),
required=False,
help_text=_("Object type (for object or multi-object fields)")
)
@@ -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', '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',
)
@@ -111,31 +111,31 @@ class CustomFieldChoiceSetImportForm(CSVModelForm):
class CustomLinkImportForm(CSVModelForm):
- content_types = CSVMultipleContentTypeField(
- label=_('Content types'),
- queryset=ContentType.objects.with_feature('custom_links'),
+ object_types = CSVMultipleContentTypeField(
+ label=_('Object types'),
+ queryset=ObjectType.objects.with_feature('custom_links'),
help_text=_("One or more assigned object types")
)
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',
)
class ExportTemplateImportForm(CSVModelForm):
- content_types = CSVMultipleContentTypeField(
- label=_('Content types'),
- queryset=ContentType.objects.with_feature('export_templates'),
+ object_types = CSVMultipleContentTypeField(
+ label=_('Object types'),
+ queryset=ObjectType.objects.with_feature('export_templates'),
help_text=_("One or more assigned object types")
)
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',
)
@@ -149,16 +149,16 @@ class ConfigTemplateImportForm(CSVModelForm):
class SavedFilterImportForm(CSVModelForm):
- content_types = CSVMultipleContentTypeField(
- label=_('Content types'),
- queryset=ContentType.objects.all(),
+ object_types = CSVMultipleContentTypeField(
+ label=_('Object types'),
+ queryset=ObjectType.objects.all(),
help_text=_("One or more assigned object types")
)
class Meta:
model = SavedFilter
fields = (
- 'name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared', 'parameters',
+ 'name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared', 'parameters',
)
@@ -173,9 +173,9 @@ class WebhookImportForm(NetBoxModelImportForm):
class EventRuleImportForm(NetBoxModelImportForm):
- content_types = CSVMultipleContentTypeField(
- label=_('Content types'),
- queryset=ContentType.objects.with_feature('event_rules'),
+ object_types = CSVMultipleContentTypeField(
+ label=_('Object types'),
+ queryset=ObjectType.objects.with_feature('event_rules'),
help_text=_("One or more assigned object types")
)
action_object = forms.CharField(
@@ -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'
)
@@ -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..73751872f 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 *
@@ -38,14 +38,14 @@ 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',
- 'is_cloneable',
+ 'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible',
+ 'ui_editable', 'is_cloneable',
)),
)
- content_type_id = ContentTypeMultipleChoiceField(
- queryset=ContentType.objects.with_feature('custom_fields'),
+ 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,
@@ -108,11 +108,11 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
- (_('Attributes'), ('content_types', 'enabled', 'new_window', 'weight')),
+ (_('Attributes'), ('object_type', 'enabled', 'new_window', 'weight')),
)
- content_types = ContentTypeMultipleChoiceField(
- label=_('Content types'),
- queryset=ContentType.objects.with_feature('custom_links'),
+ object_type = ContentTypeMultipleChoiceField(
+ label=_('Object types'),
+ queryset=ObjectType.objects.with_feature('custom_links'),
required=False
)
enabled = forms.NullBooleanField(
@@ -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_type_id', 'mime_type', 'file_extension', 'as_attachment')),
)
data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(),
@@ -154,8 +154,8 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
'source_id': '$data_source_id'
}
)
- content_type_id = ContentTypeMultipleChoiceField(
- queryset=ContentType.objects.with_feature('export_templates'),
+ object_type_id = ContentTypeMultipleChoiceField(
+ queryset=ObjectType.objects.with_feature('export_templates'),
required=False,
label=_('Content types')
)
@@ -179,11 +179,11 @@ 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'),
- queryset=ContentType.objects.with_feature('image_attachments'),
+ object_type_id = ContentTypeChoiceField(
+ label=_('Object type'),
+ queryset=ObjectType.objects.with_feature('image_attachments'),
required=False
)
name = forms.CharField(
@@ -195,11 +195,11 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
- (_('Attributes'), ('content_types', 'enabled', 'shared', 'weight')),
+ (_('Attributes'), ('object_type', 'enabled', 'shared', 'weight')),
)
- content_types = ContentTypeMultipleChoiceField(
- label=_('Content types'),
- queryset=ContentType.objects.public(),
+ object_type = ContentTypeMultipleChoiceField(
+ label=_('Object types'),
+ queryset=ObjectType.objects.public(),
required=False
)
enabled = forms.NullBooleanField(
@@ -250,11 +250,11 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
- (_('Attributes'), ('content_type_id', 'action_type', 'enabled')),
+ (_('Attributes'), ('object_type_id', 'action_type', 'enabled')),
(_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
)
- content_type_id = ContentTypeMultipleChoiceField(
- queryset=ContentType.objects.with_feature('event_rules'),
+ object_type_id = ContentTypeMultipleChoiceField(
+ 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..09d2d9535 100644
--- a/netbox/extras/forms/model_forms.py
+++ b/netbox/extras/forms/model_forms.py
@@ -2,12 +2,11 @@ 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 _
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 *
@@ -39,13 +38,13 @@ __all__ = (
class CustomFieldForm(forms.ModelForm):
- content_types = ContentTypeMultipleChoiceField(
- label=_('Content types'),
- queryset=ContentType.objects.with_feature('custom_fields')
+ object_types = ContentTypeMultipleChoiceField(
+ label=_('Object types'),
+ queryset=ObjectType.objects.with_feature('custom_fields')
)
- object_type = ContentTypeChoiceField(
- label=_('Object type'),
- queryset=ContentType.objects.public(),
+ 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)")
)
@@ -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', 'related_object_type', 'required', 'description',
)),
(_('Behavior'), ('search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable')),
(_('Values'), ('default', 'choice_set')),
@@ -123,13 +122,13 @@ class CustomFieldChoiceSetForm(forms.ModelForm):
class CustomLinkForm(forms.ModelForm):
- content_types = ContentTypeMultipleChoiceField(
- label=_('Content types'),
- queryset=ContentType.objects.with_feature('custom_links')
+ 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')),
)
@@ -152,9 +151,9 @@ class CustomLinkForm(forms.ModelForm):
class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
- content_types = ContentTypeMultipleChoiceField(
- label=_('Content types'),
- queryset=ContentType.objects.with_feature('export_templates')
+ object_types = ContentTypeMultipleChoiceField(
+ label=_('Object types'),
+ queryset=ObjectType.objects.with_feature('export_templates')
)
template_code = forms.CharField(
label=_('Template code'),
@@ -163,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')),
)
@@ -193,14 +192,14 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
class SavedFilterForm(forms.ModelForm):
slug = SlugField()
- content_types = ContentTypeMultipleChoiceField(
- label=_('Content types'),
- queryset=ContentType.objects.all()
+ 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',)),
)
@@ -221,7 +220,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:
@@ -249,9 +248,9 @@ class WebhookForm(NetBoxModelForm):
class EventRuleForm(NetBoxModelForm):
- content_types = ContentTypeMultipleChoiceField(
- label=_('Content types'),
- queryset=ContentType.objects.with_feature('event_rules'),
+ object_types = ContentTypeMultipleChoiceField(
+ label=_('Object types'),
+ queryset=ObjectType.objects.with_feature('event_rules'),
)
action_choice = forms.ChoiceField(
label=_('Action choice'),
@@ -267,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'), (
@@ -278,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'
)
@@ -339,11 +338,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 +355,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/graphql/filters.py b/netbox/extras/graphql/filters.py
index 67b679b51..568ec6ded 100644
--- a/netbox/extras/graphql/filters.py
+++ b/netbox/extras/graphql/filters.py
@@ -39,7 +39,7 @@ class ConfigTemplateFilter(filtersets.ConfigTemplateFilterSet):
@strawberry_django.filter(models.CustomField, lookups=True)
class CustomFieldFilter(filtersets.CustomFieldFilterSet):
id: auto
- content_types: auto
+ object_types: auto
name: auto
group_name: auto
required: auto
@@ -62,7 +62,7 @@ class CustomFieldChoiceSetFilter(filtersets.CustomFieldChoiceSetFilterSet):
@strawberry_django.filter(models.CustomLink, lookups=True)
class CustomLinkFilter(filtersets.CustomLinkFilterSet):
id: auto
- content_types: auto
+ object_types: auto
name: auto
enabled: auto
link_text: auto
@@ -75,7 +75,7 @@ class CustomLinkFilter(filtersets.CustomLinkFilterSet):
@strawberry_django.filter(models.ExportTemplate, lookups=True)
class ExportTemplateFilter(filtersets.ExportTemplateFilterSet):
id: auto
- content_types: auto
+ object_types: auto
name: auto
description: auto
data_synced: auto
@@ -84,7 +84,7 @@ class ExportTemplateFilter(filtersets.ExportTemplateFilterSet):
@strawberry_django.filter(models.ImageAttachment, lookups=True)
class ImageAttachmentFilter(filtersets.ImageAttachmentFilterSet):
id: auto
- content_type_id: auto
+ object_type_id: auto
object_id: auto
name: auto
@@ -113,7 +113,7 @@ class ObjectChangeFilter(filtersets.ObjectChangeFilterSet):
@strawberry_django.filter(models.SavedFilter, lookups=True)
class SavedFilterFilter(filtersets.SavedFilterFilterSet):
id: auto
- content_types: auto
+ object_types: auto
name: auto
slug: auto
description: auto
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/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py
new file mode 100644
index 000000000..7b0fa9459
--- /dev/null
+++ b/netbox/extras/migrations/0111_rename_content_types.py
@@ -0,0 +1,107 @@
+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 = [
+ # Custom fields
+ 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'),
+ ),
+ migrations.RunSQL(
+ "ALTER TABLE extras_customfield_content_types_id_seq RENAME TO extras_customfield_object_types_id_seq"
+ ),
+
+ # 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'),
+ ),
+ migrations.RunSQL(
+ "ALTER TABLE extras_customlink_content_types_id_seq RENAME TO extras_customlink_object_types_id_seq"
+ ),
+
+ # 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'),
+ ),
+ migrations.RunSQL(
+ "ALTER TABLE extras_eventrule_content_types_id_seq RENAME TO extras_eventrule_object_types_id_seq"
+ ),
+
+ # 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'),
+ ),
+ migrations.RunSQL(
+ "ALTER TABLE extras_exporttemplate_content_types_id_seq RENAME TO extras_exporttemplate_object_types_id_seq"
+ ),
+
+ # 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'),
+ ),
+ migrations.RunSQL(
+ "ALTER TABLE extras_savedfilter_content_types_id_seq RENAME TO extras_savedfilter_object_types_id_seq"
+ ),
+
+ # 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/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/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/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..a14c71c63 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,8 +52,8 @@ 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)
- return self.get_queryset().filter(content_types=content_type)
+ content_type = ObjectType.objects.get_for_model(model._meta.concrete_model)
+ 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.')
)
@@ -78,8 +78,8 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
default=CustomFieldTypeChoices.TYPE_TEXT,
help_text=_('The type of data this custom field holds')
)
- object_type = models.ForeignKey(
- to='contenttypes.ContentType',
+ related_object_type = models.ForeignKey(
+ to='core.ObjectType',
on_delete=models.PROTECT,
blank=True,
null=True,
@@ -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', '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',
)
@@ -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)
@@ -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/models/models.py b/netbox/extras/models/models.py
index 60bccd8f2..b55aaa11d 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 *
@@ -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.")
)
@@ -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.')
)
@@ -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:
@@ -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.')
)
@@ -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:
@@ -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.')
)
@@ -561,7 +561,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
)
clone_fields = (
- 'content_types', 'weight', 'enabled', 'parameters',
+ 'object_types', 'weight', 'enabled', 'parameters',
)
class Meta:
@@ -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 ContentType.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):
@@ -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/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 f8dc204e7..833ce0036 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
@@ -205,13 +206,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)
#
@@ -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.")
@@ -256,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)
@@ -266,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/tables/tables.py b/netbox/extras/tables/tables.py
index 8482c5e24..a0f504931 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',
@@ -40,8 +42,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')
@@ -55,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')
@@ -71,11 +76,11 @@ class CustomFieldTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = CustomField
fields = (
- 'pk', 'id', 'name', 'content_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', '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 +120,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 +133,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):
@@ -139,8 +144,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 +166,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',
)
@@ -174,8 +179,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 +198,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):
@@ -204,8 +209,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 +225,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',
)
@@ -281,8 +286,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,12 +314,12 @@ 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',
)
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',
)
@@ -507,3 +512,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/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 5db906b25..5d243ae1a 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
@@ -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',
@@ -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):
@@ -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):
@@ -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(
@@ -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):
@@ -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',
@@ -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)
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..0c8b86f93 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),
+ related_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),
+ related_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()
@@ -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()
@@ -524,10 +536,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 +550,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 +593,19 @@ class CustomFieldAPITest(APITestCase):
CustomField(
type=CustomFieldTypeChoices.TYPE_OBJECT,
name='object_field',
- object_type=ContentType.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=ContentType.objects.get_for_model(VLAN),
+ related_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 +1175,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 +1268,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 +1311,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 +1330,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 +1347,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 +1366,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 +1375,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 +1389,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 +1398,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 +1407,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 +1416,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)
+ related_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)
+ related_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_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 ef8aedcbd..bec62c688 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
@@ -85,13 +86,23 @@ 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].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 +112,16 @@ 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_type(self):
+ params = {'object_type': '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_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):
@@ -174,8 +191,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',
@@ -240,7 +255,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']
)
@@ -333,11 +348,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'}
@@ -351,10 +366,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_type(self):
+ params = {'object_type': '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_type_id': [ObjectType.objects.get_for_model(Region).pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_action_type(self):
@@ -396,7 +411,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(
@@ -426,7 +441,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'}
@@ -436,10 +451,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_type(self):
+ params = {'object_type': '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_type_id': [ObjectType.objects.get_for_model(Site).pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_weight(self):
@@ -465,7 +480,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'),
@@ -508,7 +523,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'}
@@ -526,10 +541,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_type(self):
+ params = {'object_type': '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_type_id': [ObjectType.objects.get_for_model(Site).pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_user(self):
@@ -638,7 +653,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'),
@@ -647,7 +662,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'}
@@ -657,10 +672,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_type(self):
+ params = {'object_type': '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_type_id': [ObjectType.objects.get_for_model(Site).pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_description(self):
@@ -692,7 +707,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',
@@ -700,7 +715,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',
@@ -708,7 +723,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',
@@ -716,7 +731,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',
@@ -734,13 +749,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)
@@ -1113,9 +1128,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 = (
@@ -1124,8 +1139,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')
@@ -1163,12 +1178,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 9c22bf83c..66c4e245e 100644
--- a/netbox/extras/tests/test_forms.py
+++ b/netbox/extras/tests/test_forms.py
@@ -1,6 +1,6 @@
-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 +12,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)
+ related_object_type=ObjectType.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)
+ related_object_type=ObjectType.objects.get_for_model(Site)
)
- cf_multiobject.content_types.set([obj_type])
+ cf_multiobject.object_types.set([object_type])
def test_empty_values(self):
"""
@@ -99,7 +99,7 @@ class SavedFilterFormTest(TestCase):
form = SavedFilterForm({
'name': 'test-sf',
'slug': 'test-sf',
- 'content_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/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)
diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py
index d720560e4..fd478acd4 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,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',
@@ -137,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'),
@@ -145,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,
@@ -158,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",
@@ -183,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'),
@@ -217,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,
@@ -231,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"}',
@@ -302,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 = (
@@ -312,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}",
@@ -396,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]),
@@ -404,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,
@@ -422,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",
)
@@ -651,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/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/extras/views.py b/netbox/extras/views.py
index 56840f7a9..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
#
@@ -46,9 +48,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})
),
@@ -762,8 +764,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 +773,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'),
}
@@ -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/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/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 9d8286968..e07e2c78b 100644
--- a/netbox/netbox/api/viewsets/mixins.py
+++ b/netbox/netbox/api/viewsets/mixins.py
@@ -1,10 +1,10 @@
-from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
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 +26,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
@@ -40,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/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..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
@@ -88,7 +89,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
)
@@ -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 815f1f6fa..6b1f31265 100644
--- a/netbox/netbox/forms/mixins.py
+++ b/netbox/netbox/forms/mixins.py
@@ -1,7 +1,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 +32,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
)
@@ -85,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/models/features.py b/netbox/netbox/models/features.py
index dbd008354..bff9ee59f 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
@@ -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:
@@ -341,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:
@@ -490,17 +494,17 @@ class SyncedDataMixin(models.Model):
ret = super().save(*args, **kwargs)
# Create/delete AutoSyncRecord as needed
- content_type = ContentType.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()
@@ -510,10 +514,10 @@ class SyncedDataMixin(models.Model):
from core.models import AutoSyncRecord
# Delete AutoSyncRecord
- content_type = ContentType.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()
diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py
index 1fb23a37c..a9e867b9f 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
@@ -130,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))
- content_types = ContentType.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.
@@ -151,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 content_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
@@ -169,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
@@ -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..afef74752 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(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/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/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')
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/templates/extras/customfield.html b/netbox/templates/extras/customfield.html
index ca6988152..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 %}
|
@@ -89,7 +91,7 @@
- {% for ct in object.content_types.all %}
+ {% for ct in object.object_types.all %}
{{ ct }} |
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 @@
- {% for ct in object.content_types.all %}
+ {% for ct in object.object_types.all %}
{{ ct }} |
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 @@
- {% for ct in object.content_types.all %}
+ {% for object_type in object.object_types.all %}
- {{ ct }} |
+ {{ object_type }} |
{% endfor %}
diff --git a/netbox/templates/extras/exporttemplate.html b/netbox/templates/extras/exporttemplate.html
index 0648a8191..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 }}
-
{{ object.content_type }}
-{% endblock %}
-
{% block content %}
@@ -70,9 +65,9 @@
- {% for ct in object.content_types.all %}
+ {% for object_type in object.object_types.all %}
- {{ ct }} |
+ {{ object_type }} |
{% endfor %}
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 %}
-
-
- {% if job.data.log %}
-
-
- {% trans "Line" %} |
- {% trans "Time" %} |
- {% trans "Level" %} |
- {% trans "Message" %} |
-
- {% for log in job.data.log %}
+ {% if tests %}
+ {# Summary of test methods #}
+
+
+
+ {% for test, data in tests.items %}
- {{ forloop.counter }} |
- {{ log.time|placeholder }} |
- {% log_level log.status %} |
- {{ log.message|markdown }} |
+ {{ test }} |
+
+ {{ data.success }}
+ {{ data.info }}
+ {{ data.warning }}
+ {{ data.failure }}
+ |
{% endfor %}
- {% else %}
-
{% trans "None" %}
- {% endif %}
-
- {% endif %}
+
+ {% endif %}
- {# Script output. Legacy reports will not have this. #}
- {% if 'output' in job.data %}
-
-
- {% 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 %}
-
-
- {% for test, data in tests.items %}
-
- {{ test }} |
-
- {{ data.success }}
- {{ data.info }}
- {{ data.warning }}
- {{ data.failure }}
- |
-
- {% endfor %}
-
+
+
+ {% include 'htmx/table.html' %}
+
+ {% endif %}
- {# Detailed results for individual tests #}
-
-
-
-
-
- {% trans "Time" %} |
- {% trans "Level" %} |
- {% trans "Object" %} |
- {% trans "Message" %} |
-
-
-
- {% for test, data in tests.items %}
-
-
- {{ test }}
- |
-
- {% for time, level, obj, url, message in data.log %}
-
- {{ time }} |
-
-
- |
-
- {% if obj and url %}
- {{ obj }}
- {% elif obj %}
- {{ obj }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
- {{ message|markdown }} |
-
- {% endfor %}
- {% endfor %}
-
-
-
+ {# Script output. Legacy reports will not have this. #}
+ {% if 'output' in job.data %}
+
+
+ {% 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/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 @@
- {% for ct in object.content_types.all %}
+ {% for object_type in object.object_types.all %}
- {{ ct }} |
+ {{ object_type }} |
{% endfor %}
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 %}
+
+
+
-
-
-
{{ 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 %}
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 %}
diff --git a/netbox/tenancy/api/serializers_/contacts.py b/netbox/tenancy/api/serializers_/contacts.py
index 925e04cfd..38ffd2393 100644
--- a/netbox/tenancy/api/serializers_/contacts.py
+++ b/netbox/tenancy/api/serializers_/contacts.py
@@ -58,7 +58,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)
@@ -69,13 +69,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())
+ serializer = get_serializer_for_model(instance.object_type.model_class())
context = {'request': self.context['request']}
return serializer(instance.object, nested=True, context=context).data
diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py
index 8079b4035..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)'),
)
@@ -86,7 +99,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 +131,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():
@@ -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/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')
diff --git a/netbox/tenancy/forms/filtersets.py b/netbox/tenancy/forms/filtersets.py
index 77e945542..fbd0f2ad0 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 *
@@ -83,10 +83,10 @@ 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(
- queryset=ContentType.objects.with_feature('contacts'),
+ 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 664fff098..e31330657 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 *
@@ -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 ContentType.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..f6890a3d4 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 *
@@ -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()
@@ -295,8 +325,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': 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/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):
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/api/serializers_/permissions.py b/netbox/users/api/serializers_/permissions.py
index 6b0a6f6d9..ff5a1f212 100644
--- a/netbox/users/api/serializers_/permissions.py
+++ b/netbox/users/api/serializers_/permissions.py
@@ -1,7 +1,7 @@
from django.contrib.auth import get_user_model
-from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers
+from core.models import ObjectType
from netbox.api.fields import ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import ValidatedModelSerializer
from users.models import Group, ObjectPermission
@@ -15,7 +15,7 @@ __all__ = (
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/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):
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 19d6013c7..d2ee16e5e 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
@@ -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..5930285a9 100644
--- a/netbox/users/tests/test_filtersets.py
+++ b/netbox/users/tests/test_filtersets.py
@@ -1,10 +1,10 @@
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
+from core.models import ObjectType
from users import filtersets
from users.models import Group, ObjectPermission, Token
from utilities.testing import BaseFilterSetTests
@@ -151,9 +151,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 = (
@@ -198,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/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/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,
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/testing/api.py b/netbox/utilities/testing/api.py
index 6df317b49..50591a95f 100644
--- a/netbox/utilities/testing/api.py
+++ b/netbox/utilities/testing/api.py
@@ -10,6 +10,7 @@ from django.test import override_settings
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
@@ -112,7 +113,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)
@@ -186,7 +187,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)
@@ -227,7 +228,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)
@@ -261,7 +262,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)
@@ -312,7 +313,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)
@@ -347,7 +348,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")
@@ -390,7 +391,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)
@@ -416,7 +417,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)
@@ -507,7 +508,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}, format="json", **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
@@ -532,7 +533,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}, format="json", **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
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/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)
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):
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):
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"),
+
+ ]
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':
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()