9856 merge feature

This commit is contained in:
Arthur 2024-03-12 10:12:09 -07:00
commit c2a3275c79
113 changed files with 1503 additions and 898 deletions

View File

@ -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.

View File

@ -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`

View File

@ -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(

View File

@ -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

View File

@ -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()),
],
),
]

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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:

View File

@ -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()

View File

@ -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',

View File

@ -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

View File

@ -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():

View File

@ -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 *

View File

@ -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

View File

@ -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)

View File

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

View File

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

View File

@ -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',
]

View File

@ -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',
]

View File

@ -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(

View File

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

View File

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

View File

@ -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
)

View File

@ -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 = [

View File

@ -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
#

View File

@ -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]

View File

@ -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]

View File

@ -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):

View File

@ -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(

View File

@ -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(

View File

@ -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
)

View File

@ -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

View File

@ -25,7 +25,4 @@ class Migration(migrations.Migration):
migrations.DeleteModel(
name='Report',
),
migrations.DeleteModel(
name='ReportModule',
),
]

View File

@ -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):

View File

@ -12,4 +12,7 @@ class Migration(migrations.Migration):
model_name='eventrule',
name='action_parameters',
),
migrations.DeleteModel(
name='ReportModule',
),
]

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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(),

View File

@ -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)
)

View File

@ -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.")

View File

@ -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)

View File

@ -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',
)

View File

@ -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 ''

View File

@ -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)

View File

@ -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 = (

View File

@ -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={

View File

@ -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'),

View File

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

View File

@ -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": [

View File

@ -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)

View File

@ -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()

View File

@ -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):

View File

@ -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', {})

View File

@ -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)

View File

@ -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__ = (

View File

@ -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())

View File

@ -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
)

View File

@ -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):

View File

@ -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)

View File

@ -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()

View File

@ -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,

View File

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

View File

@ -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',

View File

@ -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)

View File

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

View File

@ -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

View File

@ -17,7 +17,9 @@
<th scope="row">Type</th>
<td>
{{ 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 %}
</td>
</tr>
<tr>
@ -89,7 +91,7 @@
<div class="card">
<h5 class="card-header">{% trans "Object Types" %}</h5>
<table class="table table-hover attr-table">
{% for ct in object.content_types.all %}
{% for ct in object.object_types.all %}
<tr>
<td>{{ ct }}</td>
</tr>

View File

@ -38,7 +38,7 @@
<div class="card">
<h5 class="card-header">{% trans "Assigned Models" %}</h5>
<table class="table table-hover attr-table">
{% for ct in object.content_types.all %}
{% for ct in object.object_types.all %}
<tr>
<td>{{ ct }}</td>
</tr>

View File

@ -26,9 +26,9 @@
<div class="card">
<h5 class="card-header">{% trans "Object Types" %}</h5>
<table class="table table-hover attr-table">
{% for ct in object.content_types.all %}
{% for object_type in object.object_types.all %}
<tr>
<td>{{ ct }}</td>
<td>{{ object_type }}</td>
</tr>
{% endfor %}
</table>

View File

@ -5,11 +5,6 @@
{% block title %}{{ object.name }}{% endblock %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'extras:exporttemplate_list' %}?content_type={{ object.content_type.pk }}">{{ object.content_type }}</a></li>
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-5">
@ -70,9 +65,9 @@
<div class="card">
<h5 class="card-header">{% trans "Assigned Models" %}</h5>
<table class="table table-hover attr-table">
{% for ct in object.content_types.all %}
{% for object_type in object.object_types.all %}
<tr>
<td>{{ ct }}</td>
<td>{{ object_type }}</td>
</tr>
{% endfor %}
</table>

View File

@ -3,124 +3,63 @@
{% load log_levels %}
{% load i18n %}
<p>
{% if job.started %}
{% trans "Started" %}: <strong>{{ job.started|annotated_date }}</strong>
{% elif job.scheduled %}
{% trans "Scheduled for" %}: <strong>{{ job.scheduled|annotated_date }}</strong> ({{ job.scheduled|naturaltime }})
{% else %}
{% trans "Created" %}: <strong>{{ job.created|annotated_date }}</strong>
{% endif %}
<div class="htmx-container">
<p>
{% if job.started %}
{% trans "Started" %}: <strong>{{ job.started|annotated_date }}</strong>
{% elif job.scheduled %}
{% trans "Scheduled for" %}: <strong>{{ job.scheduled|annotated_date }}</strong> ({{ job.scheduled|naturaltime }})
{% else %}
{% trans "Created" %}: <strong>{{ job.created|annotated_date }}</strong>
{% endif %}
{% if job.completed %}
{% trans "Duration" %}: <strong>{{ job.duration }}</strong>
{% endif %}
<span id="pending-result-label">{% badge job.get_status_display job.get_status_color %}</span>
</p>
{% if job.completed %}
{% trans "Duration" %}: <strong>{{ job.duration }}</strong>
{% endif %}
<span id="pending-result-label">{% badge job.get_status_display job.get_status_color %}</span>
</p>
{% if job.completed %}
{# Script log. Legacy reports will not have this. #}
{% if 'log' in job.data %}
<div class="card mb-3">
<h5 class="card-header">{% trans "Log" %}</h5>
{% if job.data.log %}
<table class="table table-hover panel-body">
<tr>
<th>{% trans "Line" %}</th>
<th>{% trans "Time" %}</th>
<th>{% trans "Level" %}</th>
<th>{% trans "Message" %}</th>
</tr>
{% for log in job.data.log %}
{% if tests %}
{# Summary of test methods #}
<div class="card">
<h5 class="card-header">{% trans "Test Summary" %}</h5>
<table class="table table-hover">
{% for test, data in tests.items %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ log.time|placeholder }}</td>
<td>{% log_level log.status %}</td>
<td>{{ log.message|markdown }}</td>
<td class="font-monospace"><a href="#{{ test }}">{{ test }}</a></td>
<td class="text-end report-stats">
<span class="badge text-bg-success">{{ data.success }}</span>
<span class="badge text-bg-info">{{ data.info }}</span>
<span class="badge text-bg-warning">{{ data.warning }}</span>
<span class="badge text-bg-danger">{{ data.failure }}</span>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="card-body text-muted">{% trans "None" %}</div>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
{# Script output. Legacy reports will not have this. #}
{% if 'output' in job.data %}
<div class="card mb-3">
<h5 class="card-header">{% trans "Output" %}</h5>
{% if job.data.output %}
<pre class="card-body font-monospace">{{ job.data.output }}</pre>
{% else %}
<div class="card-body text-muted">{% trans "None" %}</div>
{% endif %}
</div>
{% endif %}
{# Test method logs (for legacy Reports) #}
{% if tests %}
{# Summary of test methods #}
{% if table %}
<div class="card">
<h5 class="card-header">{% trans "Test Summary" %}</h5>
<table class="table table-hover">
{% for test, data in tests.items %}
<tr>
<td class="font-monospace"><a href="#{{ test }}">{{ test }}</a></td>
<td class="text-end report-stats">
<span class="badge text-bg-success">{{ data.success }}</span>
<span class="badge text-bg-info">{{ data.info }}</span>
<span class="badge text-bg-warning">{{ data.warning }}</span>
<span class="badge text-bg-danger">{{ data.failure }}</span>
</td>
</tr>
{% endfor %}
</table>
<div class="table-responsive" id="object_list">
<h5 class="card-header">{% trans "Log" %}</h5>
{% include 'htmx/table.html' %}
</div>
</div>
{% endif %}
{# Detailed results for individual tests #}
<div class="card">
<h5 class="card-header">{% trans "Test Details" %}</h5>
<table class="table table-hover report">
<thead>
<tr class="table-headings">
<th>{% trans "Time" %}</th>
<th>{% trans "Level" %}</th>
<th>{% trans "Object" %}</th>
<th>{% trans "Message" %}</th>
</tr>
</thead>
<tbody>
{% for test, data in tests.items %}
<tr>
<th colspan="4" style="font-family: monospace">
<a name="{{ test }}"></a>{{ test }}
</th>
</tr>
{% for time, level, obj, url, message in data.log %}
<tr class="{% if level == 'failure' %}danger{% elif level %}{{ level }}{% endif %}">
<td>{{ time }}</td>
<td>
<label class="badge text-bg-{% if level == 'failure' %}danger{% else %}{{ level }}{% endif %}">{{ level|title }}</label>
</td>
<td>
{% if obj and url %}
<a href="{{ url }}">{{ obj }}</a>
{% elif obj %}
{{ obj }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<td class="rendered-markdown">{{ message|markdown }}</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
{# Script output. Legacy reports will not have this. #}
{% if 'output' in job.data %}
<div class="card mb-3">
<h5 class="card-header">{% trans "Output" %}</h5>
{% if job.data.output %}
<pre class="card-body font-monospace">{{ job.data.output }}</pre>
{% else %}
<div class="card-body text-muted">{% trans "None" %}</div>
{% endif %}
</div>
{% endif %}
{% elif job.started %}
{% include 'extras/inc/result_pending.html' %}
{% endif %}
{% elif job.started %}
{% include 'extras/inc/result_pending.html' %}
{% endif %}
</div>

View File

@ -38,9 +38,9 @@
<div class="card">
<h5 class="card-header">{% trans "Assigned Models" %}</h5>
<table class="table table-hover attr-table">
{% for ct in object.content_types.all %}
{% for object_type in object.object_types.all %}
<tr>
<td>{{ ct }}</td>
<td>{{ object_type }}</td>
</tr>
{% endfor %}
</table>

View File

@ -32,28 +32,74 @@
{% block tabs %}
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<a href="#log" role="tab" data-bs-toggle="tab" class="nav-link active">{% trans "Log" %}</a>
</li>
<li class="nav-item" role="presentation">
<a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">{% trans "Source" %}</a>
<a href="#results" role="tab" data-bs-toggle="tab" class="nav-link active">{% trans "Results" %}</a>
</li>
</ul>
{% endblock %}
{% block content %}
<div role="tabpanel" class="tab-pane active" id="log">
<div class="row">
<div class="col col-md-12"{% if not job.completed %} hx-get="{% url 'extras:script_result' job_pk=job.pk %}" hx-trigger="load delay:0.5s, every 5s"{% endif %}>
{% include 'extras/htmx/script_result.html' %}
{# Object list tab #}
<div class="tab-pane show active" id="results" role="tabpanel" aria-labelledby="results-tab">
{# Object table controls #}
<div class="row mb-3">
<div class="col-auto ms-auto d-print-none">
{% if request.user.is_authenticated %}
<div class="table-configure input-group">
<button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#ObjectTable_config"
class="btn">
<i class="mdi mdi-cog"></i> {% trans "Configure Table" %}
</button>
</div>
{% endif %}
</div>
</div>
<form method="post" class="form form-horizontal">
{% csrf_token %}
{# "Select all" form #}
{% if table.paginator.num_pages > 1 %}
<div id="select-all-box" class="d-none card d-print-none">
<div class="form col-md-12">
<div class="card-body">
<div class="form-check">
<input type="checkbox" id="select-all" name="_all" class="form-check-input" />
<label for="select-all" class="form-check-label">
{% blocktrans trimmed with count=table.rows|length object_type_plural=table.data.verbose_name_plural %}
Select <strong>all {{ count }} {{ object_type_plural }}</strong> matching query
{% endblocktrans %}
</label>
</div>
</div>
</div>
</div>
{% endif %}
<div class="form form-horizontal">
{% csrf_token %}
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
{# Objects table #}
<div class="col col-md-12"{% if not job.completed %} hx-get="{% url 'extras:script_result' job_pk=job.pk %}" hx-trigger="load delay:0.5s, every 5s"{% endif %}>
{% include 'extras/htmx/script_result.html' %}
</div>
{# /Objects table #}
</div>
</form>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="source">
<p><code>{{ script.filename }}</code></p>
<pre class="block">{{ script.source }}</pre>
</div>
{# /Object list tab #}
{# Filters tab #}
{% if filter_form %}
<div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
{% include 'inc/filter_list.html' %}
</div>
{% endif %}
{# /Filters tab #}
{% endblock content %}
{% block modals %}
{% include 'inc/htmx_modal.html' %}
{% table_config_form table table_name="ObjectTable" %}
{% endblock modals %}

View File

@ -6,7 +6,7 @@
{% trans "Images" %}
{% if perms.extras.add_imageattachment %}
<div class="card-actions">
<a href="{% url 'extras:imageattachment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
<a href="{% url 'extras:imageattachment_add' %}?object_type={{ object|content_type_id }}&object_id={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Attach an image" %}
</a>
</div>

View File

@ -5,7 +5,7 @@
{% block extra_controls %}
{% if perms.tenancy.add_contactassignment %}
{% with viewname=object|viewname:"contacts" %}
<a href="{% url 'tenancy:contactassignment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={% url viewname pk=object.pk %}" class="btn btn-primary">
<a href="{% url 'tenancy:contactassignment_add' %}?object_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={% url viewname pk=object.pk %}" class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a contact" %}
</a>
{% endwith %}

View File

@ -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

View File

@ -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)'),
)

View File

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

View File

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

View File

@ -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(),
}

View File

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

View File

@ -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):

View File

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

View File

@ -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,

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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(

View File

@ -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})
)

View File

@ -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()

View File

@ -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):

View File

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

View File

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

View File

@ -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(

View File

@ -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)

View File

@ -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',
}

Some files were not shown because too many files have changed in this diff Show More