9856 merge feature
This commit is contained in:
commit
c2a3275c79
|
@ -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.
|
||||
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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 *
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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']
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
#
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -25,7 +25,4 @@ class Migration(migrations.Migration):
|
|||
migrations.DeleteModel(
|
||||
name='Report',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='ReportModule',
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -12,4 +12,7 @@ class Migration(migrations.Migration):
|
|||
model_name='eventrule',
|
||||
name='action_parameters',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='ReportModule',
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
)
|
||||
|
|
|
@ -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 ''
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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', {})
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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__ = (
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
])
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)'),
|
||||
)
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
)
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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):
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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})
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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'
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue