mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 03:56:53 -06:00
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` |
|
| 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` |
|
| 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.
|
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)
|
* [#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
|
* [#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
|
* [#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`)
|
* [#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
|
* [#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
|
* [#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
|
* [#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
|
* [#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(
|
object_type = ContentTypeChoiceField(
|
||||||
label=_('Object Type'),
|
label=_('Object Type'),
|
||||||
queryset=ContentType.objects.with_feature('jobs'),
|
queryset=ObjectType.objects.with_feature('jobs'),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
status = forms.MultipleChoiceField(
|
status = forms.MultipleChoiceField(
|
||||||
|
@ -8,7 +8,7 @@ from django.conf import settings
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.management.base import BaseCommand
|
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')
|
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ class Command(BaseCommand):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Additional objects to include
|
# Additional objects to include
|
||||||
namespace['ContentType'] = ContentType
|
namespace['ObjectType'] = ObjectType
|
||||||
namespace['User'] = get_user_model()
|
namespace['User'] = get_user_model()
|
||||||
|
|
||||||
# Load convenience commands
|
# Load convenience commands
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# Generated by Django 4.2.6 on 2023-10-31 19:38
|
|
||||||
|
|
||||||
import core.models.contenttypes
|
import core.models.contenttypes
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
@ -13,7 +11,7 @@ class Migration(migrations.Migration):
|
|||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ContentType',
|
name='ObjectType',
|
||||||
fields=[
|
fields=[
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
@ -23,7 +21,7 @@ class Migration(migrations.Migration):
|
|||||||
},
|
},
|
||||||
bases=('contenttypes.contenttype',),
|
bases=('contenttypes.contenttype',),
|
||||||
managers=[
|
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 django.db.models import Q
|
||||||
|
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ContentType',
|
'ObjectType',
|
||||||
'ContentTypeManager',
|
'ObjectTypeManager',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ContentTypeManager(ContentTypeManager_):
|
class ObjectTypeManager(ContentTypeManager):
|
||||||
|
|
||||||
def public(self):
|
def public(self):
|
||||||
"""
|
"""
|
||||||
@ -40,11 +40,11 @@ class ContentTypeManager(ContentTypeManager_):
|
|||||||
return self.get_queryset().filter(q)
|
return self.get_queryset().filter(q)
|
||||||
|
|
||||||
|
|
||||||
class ContentType(ContentType_):
|
class ObjectType(ContentType):
|
||||||
"""
|
"""
|
||||||
Wrap Django's native ContentType model to use our custom manager.
|
Wrap Django's native ContentType model to use our custom manager.
|
||||||
"""
|
"""
|
||||||
objects = ContentTypeManager()
|
objects = ObjectTypeManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
proxy = True
|
proxy = True
|
||||||
|
@ -11,7 +11,7 @@ from django.utils import timezone
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from core.choices import JobStatusChoices
|
from core.choices import JobStatusChoices
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from core.signals import job_end, job_start
|
from core.signals import job_end, job_start
|
||||||
from extras.constants import EVENT_JOB_END, EVENT_JOB_START
|
from extras.constants import EVENT_JOB_END, EVENT_JOB_START
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
@ -130,7 +130,7 @@ class Job(models.Model):
|
|||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Validate the assigned object type
|
# 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(
|
raise ValidationError(
|
||||||
_("Jobs cannot be assigned to this object type ({type}).").format(type=self.object_type)
|
_("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
|
schedule_at: Schedule the job to be executed at the passed date and time
|
||||||
interval: Recurrence interval (in minutes)
|
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)
|
rq_queue_name = get_queue_for_model(object_type.model)
|
||||||
queue = django_rq.get_queue(rq_queue_name)
|
queue = django_rq.get_queue(rq_queue_name)
|
||||||
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING
|
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING
|
||||||
|
@ -89,6 +89,19 @@ class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Parent region (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:
|
class Meta:
|
||||||
model = Region
|
model = Region
|
||||||
@ -106,6 +119,19 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Parent site group (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:
|
class Meta:
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
@ -214,13 +240,23 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Site (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(),
|
queryset=Location.objects.all(),
|
||||||
field_name='parent',
|
field_name='parent',
|
||||||
lookup_expr='in',
|
lookup_expr='in',
|
||||||
label=_('Location (ID)'),
|
label=_('Location (ID)'),
|
||||||
)
|
)
|
||||||
parent = TreeNodeMultipleChoiceFilter(
|
ancestor = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Location.objects.all(),
|
queryset=Location.objects.all(),
|
||||||
field_name='parent',
|
field_name='parent',
|
||||||
lookup_expr='in',
|
lookup_expr='in',
|
||||||
|
@ -9,7 +9,7 @@ from django.dispatch import Signal
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.fields import PathField
|
from dcim.fields import PathField
|
||||||
@ -481,13 +481,13 @@ class CablePath(models.Model):
|
|||||||
def origin_type(self):
|
def origin_type(self):
|
||||||
if self.path:
|
if self.path:
|
||||||
ct_id, _ = decompile_path_node(self.path[0][0])
|
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
|
@property
|
||||||
def destination_type(self):
|
def destination_type(self):
|
||||||
if self.is_complete:
|
if self.is_complete:
|
||||||
ct_id, _ = decompile_path_node(self.path[-1][0])
|
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
|
@property
|
||||||
def path_objects(self):
|
def path_objects(self):
|
||||||
@ -594,7 +594,7 @@ class CablePath(models.Model):
|
|||||||
|
|
||||||
# Step 6: Determine the far-end terminations
|
# Step 6: Determine the far-end terminations
|
||||||
if isinstance(links[0], Cable):
|
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(
|
local_cable_terminations = CableTermination.objects.filter(
|
||||||
termination_type=termination_type,
|
termination_type=termination_type,
|
||||||
termination_id__in=[t.pk for t in terminations]
|
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.
|
# Prefetch path objects using one query per model type. Prefetch related devices where appropriate.
|
||||||
prefetched = {}
|
prefetched = {}
|
||||||
for ct_id, object_ids in to_prefetch.items():
|
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)
|
queryset = model_class.objects.filter(pk__in=object_ids)
|
||||||
if hasattr(model_class, 'device'):
|
if hasattr(model_class, 'device'):
|
||||||
queryset = queryset.prefetch_related('device')
|
queryset = queryset.prefetch_related('device')
|
||||||
@ -774,7 +774,7 @@ class CablePath(models.Model):
|
|||||||
"""
|
"""
|
||||||
Return all Cable IDs within the path.
|
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 = []
|
cable_ids = []
|
||||||
|
|
||||||
for node in self._nodes:
|
for node in self._nodes:
|
||||||
|
@ -64,21 +64,32 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
regions = (
|
parent_regions = (
|
||||||
Region(name='Region 1', slug='region-1', description='foobar1'),
|
Region(name='Region 1', slug='region-1', description='foobar1'),
|
||||||
Region(name='Region 2', slug='region-2', description='foobar2'),
|
Region(name='Region 2', slug='region-2', description='foobar2'),
|
||||||
Region(name='Region 3', slug='region-3', description='foobar3'),
|
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:
|
for region in regions:
|
||||||
region.save()
|
region.save()
|
||||||
|
|
||||||
child_regions = (
|
child_regions = (
|
||||||
Region(name='Region 1A', slug='region-1a', parent=regions[0]),
|
Region(name='Region 1A1', slug='region-1a1', parent=regions[0]),
|
||||||
Region(name='Region 1B', slug='region-1b', parent=regions[0]),
|
Region(name='Region 1B1', slug='region-1b1', parent=regions[1]),
|
||||||
Region(name='Region 2A', slug='region-2a', parent=regions[1]),
|
Region(name='Region 2A1', slug='region-2a1', parent=regions[2]),
|
||||||
Region(name='Region 2B', slug='region-2b', parent=regions[1]),
|
Region(name='Region 2B1', slug='region-2b1', parent=regions[3]),
|
||||||
Region(name='Region 3A', slug='region-3a', parent=regions[2]),
|
Region(name='Region 3A1', slug='region-3a1', parent=regions[4]),
|
||||||
Region(name='Region 3B', slug='region-3b', parent=regions[2]),
|
Region(name='Region 3B1', slug='region-3b1', parent=regions[5]),
|
||||||
)
|
)
|
||||||
for region in child_regions:
|
for region in child_regions:
|
||||||
region.save()
|
region.save()
|
||||||
@ -100,12 +111,19 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_parent(self):
|
def test_parent(self):
|
||||||
parent_regions = Region.objects.filter(parent__isnull=True)[:2]
|
regions = Region.objects.filter(parent__isnull=True)[:2]
|
||||||
params = {'parent_id': [parent_regions[0].pk, parent_regions[1].pk]}
|
params = {'parent_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(), 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)
|
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):
|
class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = SiteGroup.objects.all()
|
queryset = SiteGroup.objects.all()
|
||||||
@ -114,24 +132,35 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
sitegroups = (
|
parent_groups = (
|
||||||
SiteGroup(name='Site Group 1', slug='site-group-1', description='foobar1'),
|
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 2', slug='site-group-2', description='foobar2'),
|
||||||
SiteGroup(name='Site Group 3', slug='site-group-3', description='foobar3'),
|
SiteGroup(name='Site Group 3', slug='site-group-3', description='foobar3'),
|
||||||
)
|
)
|
||||||
for sitegroup in sitegroups:
|
for site_group in parent_groups:
|
||||||
sitegroup.save()
|
site_group.save()
|
||||||
|
|
||||||
child_sitegroups = (
|
groups = (
|
||||||
SiteGroup(name='Site Group 1A', slug='site-group-1a', parent=sitegroups[0]),
|
SiteGroup(name='Site Group 1A', slug='site-group-1a', parent=parent_groups[0]),
|
||||||
SiteGroup(name='Site Group 1B', slug='site-group-1b', parent=sitegroups[0]),
|
SiteGroup(name='Site Group 1B', slug='site-group-1b', parent=parent_groups[0]),
|
||||||
SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=sitegroups[1]),
|
SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=parent_groups[1]),
|
||||||
SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=sitegroups[1]),
|
SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=parent_groups[1]),
|
||||||
SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=sitegroups[2]),
|
SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=parent_groups[2]),
|
||||||
SiteGroup(name='Site Group 3B', slug='site-group-3b', parent=sitegroups[2]),
|
SiteGroup(name='Site Group 3B', slug='site-group-3b', parent=parent_groups[2]),
|
||||||
)
|
)
|
||||||
for sitegroup in child_sitegroups:
|
for site_group in groups:
|
||||||
sitegroup.save()
|
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):
|
def test_q(self):
|
||||||
params = {'q': 'foobar1'}
|
params = {'q': 'foobar1'}
|
||||||
@ -150,12 +179,19 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_parent(self):
|
def test_parent(self):
|
||||||
parent_sitegroups = SiteGroup.objects.filter(parent__isnull=True)[:2]
|
site_groups = SiteGroup.objects.filter(parent__isnull=True)[:2]
|
||||||
params = {'parent_id': [parent_sitegroups[0].pk, parent_sitegroups[1].pk]}
|
params = {'parent_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(), 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)
|
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):
|
class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = Site.objects.all()
|
queryset = Site.objects.all()
|
||||||
@ -314,21 +350,29 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Site.objects.bulk_create(sites)
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
parent_locations = (
|
parent_locations = (
|
||||||
Location(name='Parent Location 1', slug='parent-location-1', site=sites[0]),
|
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||||
Location(name='Parent Location 2', slug='parent-location-2', site=sites[1]),
|
Location(name='Location 2', slug='location-2', site=sites[1]),
|
||||||
Location(name='Parent Location 3', slug='parent-location-3', site=sites[2]),
|
Location(name='Location 3', slug='location-3', site=sites[2]),
|
||||||
)
|
)
|
||||||
for location in parent_locations:
|
for location in parent_locations:
|
||||||
location.save()
|
location.save()
|
||||||
|
|
||||||
locations = (
|
locations = (
|
||||||
Location(name='Location 1', slug='location-1', site=sites[0], parent=parent_locations[0], status=LocationStatusChoices.STATUS_PLANNED, description='foobar1'),
|
Location(name='Location 1A', slug='location-1a', 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 2A', slug='location-2a', 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 3A', slug='location-3a', site=sites[2], parent=parent_locations[2], status=LocationStatusChoices.STATUS_DECOMMISSIONING, description='foobar3'),
|
||||||
)
|
)
|
||||||
for location in locations:
|
for location in locations:
|
||||||
location.save()
|
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):
|
def test_q(self):
|
||||||
params = {'q': 'foobar1'}
|
params = {'q': 'foobar1'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
@ -352,31 +396,38 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
def test_region(self):
|
def test_region(self):
|
||||||
regions = Region.objects.all()[:2]
|
regions = Region.objects.all()[:2]
|
||||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
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]}
|
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):
|
def test_site_group(self):
|
||||||
site_groups = SiteGroup.objects.all()[:2]
|
site_groups = SiteGroup.objects.all()[:2]
|
||||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
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]}
|
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):
|
def test_site(self):
|
||||||
sites = Site.objects.all()[:2]
|
sites = Site.objects.all()[:2]
|
||||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
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]}
|
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):
|
def test_parent(self):
|
||||||
parent_groups = Location.objects.filter(name__startswith='Parent')[:2]
|
locations = Location.objects.filter(parent__isnull=True)[:2]
|
||||||
params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]}
|
params = {'parent_id': [locations[0].pk, locations[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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)
|
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):
|
class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = RackRole.objects.all()
|
queryset = RackRole.objects.all()
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
|
from core.models import ObjectType
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.models import CustomField
|
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
|
# Create a CustomField with a default value & assign it to all component models
|
||||||
cf1 = CustomField.objects.create(name='cf1', default='foo')
|
cf1 = CustomField.objects.create(name='cf1', default='foo')
|
||||||
cf1.content_types.set(
|
cf1.object_types.set(
|
||||||
ContentType.objects.filter(app_label='dcim', model__in=[
|
ObjectType.objects.filter(app_label='dcim', model__in=[
|
||||||
'consoleport',
|
'consoleport',
|
||||||
'consoleserverport',
|
'consoleserverport',
|
||||||
'powerport',
|
'powerport',
|
||||||
|
@ -3,7 +3,6 @@ from zoneinfo import ZoneInfo
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from netaddr import EUI
|
from netaddr import EUI
|
||||||
@ -2982,7 +2981,6 @@ class CableTestCase(
|
|||||||
|
|
||||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
interface_ct = ContentType.objects.get_for_model(Interface)
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
# TODO: Revisit this limitation
|
# TODO: Revisit this limitation
|
||||||
# Changing terminations not supported when editing an existing Cable
|
# 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 django.utils.translation import gettext as _
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework.fields import Field
|
from rest_framework.fields import Field
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from extras.choices import CustomFieldTypeChoices
|
from extras.choices import CustomFieldTypeChoices
|
||||||
from extras.models import CustomField
|
from extras.models import CustomField
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
@ -24,8 +24,8 @@ class CustomFieldDefaultValues:
|
|||||||
self.model = serializer_field.parent.Meta.model
|
self.model = serializer_field.parent.Meta.model
|
||||||
|
|
||||||
# Retrieve the CustomFields for the parent model
|
# Retrieve the CustomFields for the parent model
|
||||||
content_type = ContentType.objects.get_for_model(self.model)
|
object_type = ObjectType.objects.get_for_model(self.model)
|
||||||
fields = CustomField.objects.filter(content_types=content_type)
|
fields = CustomField.objects.filter(object_types=object_type)
|
||||||
|
|
||||||
# Populate the default value for each CustomField
|
# Populate the default value for each CustomField
|
||||||
value = {}
|
value = {}
|
||||||
@ -46,8 +46,8 @@ class CustomFieldsDataField(Field):
|
|||||||
Cache CustomFields assigned to this model to avoid redundant database queries
|
Cache CustomFields assigned to this model to avoid redundant database queries
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, '_custom_fields'):
|
if not hasattr(self, '_custom_fields'):
|
||||||
content_type = ContentType.objects.get_for_model(self.parent.Meta.model)
|
object_type = ObjectType.objects.get_for_model(self.parent.Meta.model)
|
||||||
self._custom_fields = CustomField.objects.filter(content_types=content_type)
|
self._custom_fields = CustomField.objects.filter(object_types=object_type)
|
||||||
return self._custom_fields
|
return self._custom_fields
|
||||||
|
|
||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
@ -57,10 +57,10 @@ class CustomFieldsDataField(Field):
|
|||||||
for cf in self._get_custom_fields():
|
for cf in self._get_custom_fields():
|
||||||
value = cf.deserialize(obj.get(cf.name))
|
value = cf.deserialize(obj.get(cf.name))
|
||||||
if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
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
|
value = serializer(value, nested=True, context=self.parent.context).data
|
||||||
elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
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
|
value = serializer(value, nested=True, many=True, context=self.parent.context).data
|
||||||
data[cf.name] = value
|
data[cf.name] = value
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ class CustomFieldsDataField(Field):
|
|||||||
CustomFieldTypeChoices.TYPE_OBJECT,
|
CustomFieldTypeChoices.TYPE_OBJECT,
|
||||||
CustomFieldTypeChoices.TYPE_MULTIOBJECT
|
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
|
many = cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT
|
||||||
serializer = serializer_class(data=data[cf.name], nested=True, many=many, context=self.parent.context)
|
serializer = serializer_class(data=data[cf.name], nested=True, many=many, context=self.parent.context)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
from .serializers_.objecttypes import *
|
||||||
from .serializers_.attachments import *
|
from .serializers_.attachments import *
|
||||||
from .serializers_.bookmarks import *
|
from .serializers_.bookmarks import *
|
||||||
from .serializers_.change_logging import *
|
from .serializers_.change_logging import *
|
||||||
from .serializers_.contenttypes import *
|
|
||||||
from .serializers_.customfields import *
|
from .serializers_.customfields import *
|
||||||
from .serializers_.customlinks import *
|
from .serializers_.customlinks import *
|
||||||
from .serializers_.dashboard import *
|
from .serializers_.dashboard import *
|
||||||
|
@ -2,7 +2,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.models import ImageAttachment
|
from extras.models import ImageAttachment
|
||||||
from netbox.api.fields import ContentTypeField
|
from netbox.api.fields import ContentTypeField
|
||||||
from netbox.api.serializers import ValidatedModelSerializer
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
@ -15,15 +15,15 @@ __all__ = (
|
|||||||
|
|
||||||
class ImageAttachmentSerializer(ValidatedModelSerializer):
|
class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
|
||||||
content_type = ContentTypeField(
|
object_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.all()
|
queryset=ObjectType.objects.all()
|
||||||
)
|
)
|
||||||
parent = serializers.SerializerMethodField(read_only=True)
|
parent = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ImageAttachment
|
model = ImageAttachment
|
||||||
fields = [
|
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',
|
'image_width', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'image')
|
brief_fields = ('id', 'url', 'display', 'name', 'image')
|
||||||
@ -32,10 +32,10 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
# Validate that the parent object exists
|
# Validate that the parent object exists
|
||||||
try:
|
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:
|
except ObjectDoesNotExist:
|
||||||
raise serializers.ValidationError(
|
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
|
# Enforce model validation
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.models import Bookmark
|
from extras.models import Bookmark
|
||||||
from netbox.api.fields import ContentTypeField
|
from netbox.api.fields import ContentTypeField
|
||||||
from netbox.api.serializers import ValidatedModelSerializer
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
@ -16,7 +16,7 @@ __all__ = (
|
|||||||
class BookmarkSerializer(ValidatedModelSerializer):
|
class BookmarkSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
|
||||||
object_type = ContentTypeField(
|
object_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.with_feature('bookmarks'),
|
queryset=ObjectType.objects.with_feature('bookmarks'),
|
||||||
)
|
)
|
||||||
object = serializers.SerializerMethodField(read_only=True)
|
object = serializers.SerializerMethodField(read_only=True)
|
||||||
user = UserSerializer(nested=True)
|
user = UserSerializer(nested=True)
|
||||||
|
@ -3,7 +3,7 @@ from drf_spectacular.types import OpenApiTypes
|
|||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import CustomField, CustomFieldChoiceSet
|
from extras.models import CustomField, CustomFieldChoiceSet
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
@ -39,13 +39,13 @@ class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
class CustomFieldSerializer(ValidatedModelSerializer):
|
class CustomFieldSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
|
||||||
content_types = ContentTypeField(
|
object_types = ContentTypeField(
|
||||||
queryset=ContentType.objects.with_feature('custom_fields'),
|
queryset=ObjectType.objects.with_feature('custom_fields'),
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
type = ChoiceField(choices=CustomFieldTypeChoices)
|
type = ChoiceField(choices=CustomFieldTypeChoices)
|
||||||
object_type = ContentTypeField(
|
related_object_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ObjectType.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
allow_null=True
|
allow_null=True
|
||||||
)
|
)
|
||||||
@ -62,10 +62,10 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
|
'id', 'url', 'display', 'object_types', 'type', 'related_object_type', 'data_type', 'name', 'label',
|
||||||
'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
|
'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable',
|
||||||
'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set',
|
'is_cloneable', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
|
||||||
'created', 'last_updated',
|
'choice_set', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.models import CustomLink
|
from extras.models import CustomLink
|
||||||
from netbox.api.fields import ContentTypeField
|
from netbox.api.fields import ContentTypeField
|
||||||
from netbox.api.serializers import ValidatedModelSerializer
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
@ -12,15 +12,15 @@ __all__ = (
|
|||||||
|
|
||||||
class CustomLinkSerializer(ValidatedModelSerializer):
|
class CustomLinkSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
||||||
content_types = ContentTypeField(
|
object_types = ContentTypeField(
|
||||||
queryset=ContentType.objects.with_feature('custom_links'),
|
queryset=ObjectType.objects.with_feature('custom_links'),
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
fields = [
|
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',
|
'button_class', 'new_window', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name')
|
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 drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import EventRule, Webhook
|
from extras.models import EventRule, Webhook
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
@ -22,20 +22,20 @@ __all__ = (
|
|||||||
|
|
||||||
class EventRuleSerializer(NetBoxModelSerializer):
|
class EventRuleSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
|
||||||
content_types = ContentTypeField(
|
object_types = ContentTypeField(
|
||||||
queryset=ContentType.objects.with_feature('event_rules'),
|
queryset=ObjectType.objects.with_feature('event_rules'),
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
action_type = ChoiceField(choices=EventRuleActionChoices)
|
action_type = ChoiceField(choices=EventRuleActionChoices)
|
||||||
action_object_type = ContentTypeField(
|
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)
|
action_object = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = EventRule
|
model = EventRule
|
||||||
fields = [
|
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',
|
'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',
|
'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
|
from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.models import ExportTemplate
|
from extras.models import ExportTemplate
|
||||||
from netbox.api.fields import ContentTypeField
|
from netbox.api.fields import ContentTypeField
|
||||||
from netbox.api.serializers import ValidatedModelSerializer
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
@ -13,8 +13,8 @@ __all__ = (
|
|||||||
|
|
||||||
class ExportTemplateSerializer(ValidatedModelSerializer):
|
class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
|
||||||
content_types = ContentTypeField(
|
object_types = ContentTypeField(
|
||||||
queryset=ContentType.objects.with_feature('export_templates'),
|
queryset=ObjectType.objects.with_feature('export_templates'),
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
data_source = DataSourceSerializer(
|
data_source = DataSourceSerializer(
|
||||||
@ -29,7 +29,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
fields = [
|
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',
|
'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created',
|
||||||
'last_updated',
|
'last_updated',
|
||||||
]
|
]
|
||||||
|
@ -3,7 +3,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import JournalEntry
|
from extras.models import JournalEntry
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
@ -18,7 +18,7 @@ __all__ = (
|
|||||||
class JournalEntrySerializer(NetBoxModelSerializer):
|
class JournalEntrySerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
|
||||||
assigned_object_type = ContentTypeField(
|
assigned_object_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.all()
|
queryset=ObjectType.objects.all()
|
||||||
)
|
)
|
||||||
assigned_object = serializers.SerializerMethodField(read_only=True)
|
assigned_object = serializers.SerializerMethodField(read_only=True)
|
||||||
created_by = serializers.PrimaryKeyRelatedField(
|
created_by = serializers.PrimaryKeyRelatedField(
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from netbox.api.serializers import BaseModelSerializer
|
from netbox.api.serializers import BaseModelSerializer
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ContentTypeSerializer',
|
'ObjectTypeSerializer',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ContentTypeSerializer(BaseModelSerializer):
|
class ObjectTypeSerializer(BaseModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objecttype-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContentType
|
model = ObjectType
|
||||||
fields = ['id', 'url', 'display', 'app_label', 'model']
|
fields = ['id', 'url', 'display', 'app_label', 'model']
|
@ -1,6 +1,6 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.models import SavedFilter
|
from extras.models import SavedFilter
|
||||||
from netbox.api.fields import ContentTypeField
|
from netbox.api.fields import ContentTypeField
|
||||||
from netbox.api.serializers import ValidatedModelSerializer
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
@ -12,15 +12,15 @@ __all__ = (
|
|||||||
|
|
||||||
class SavedFilterSerializer(ValidatedModelSerializer):
|
class SavedFilterSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
|
||||||
content_types = ContentTypeField(
|
object_types = ContentTypeField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ObjectType.objects.all(),
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SavedFilter
|
model = SavedFilter
|
||||||
fields = [
|
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',
|
'shared', 'parameters', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.models import Tag
|
from extras.models import Tag
|
||||||
from netbox.api.fields import ContentTypeField, RelatedObjectCountField
|
from netbox.api.fields import ContentTypeField, RelatedObjectCountField
|
||||||
from netbox.api.serializers import ValidatedModelSerializer
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
@ -13,7 +13,7 @@ __all__ = (
|
|||||||
class TagSerializer(ValidatedModelSerializer):
|
class TagSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
|
||||||
object_types = ContentTypeField(
|
object_types = ContentTypeField(
|
||||||
queryset=ContentType.objects.with_feature('tags'),
|
queryset=ObjectType.objects.with_feature('tags'),
|
||||||
many=True,
|
many=True,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
@ -22,7 +22,7 @@ router.register('config-contexts', views.ConfigContextViewSet)
|
|||||||
router.register('config-templates', views.ConfigTemplateViewSet)
|
router.register('config-templates', views.ConfigTemplateViewSet)
|
||||||
router.register('scripts', views.ScriptViewSet, basename='script')
|
router.register('scripts', views.ScriptViewSet, basename='script')
|
||||||
router.register('object-changes', views.ObjectChangeViewSet)
|
router.register('object-changes', views.ObjectChangeViewSet)
|
||||||
router.register('content-types', views.ContentTypeViewSet)
|
router.register('object-types', views.ObjectTypeViewSet)
|
||||||
|
|
||||||
app_name = 'extras-api'
|
app_name = 'extras-api'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django_rq.queues import get_connection
|
from django_rq.queues import get_connection
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -11,7 +10,7 @@ from rest_framework.routers import APIRootView
|
|||||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
from rq import Worker
|
from rq import Worker
|
||||||
|
|
||||||
from core.models import Job
|
from core.models import Job, ObjectType
|
||||||
from extras import filtersets
|
from extras import filtersets
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from extras.scripts import run_script
|
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]
|
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||||
queryset = ContentType.objects.order_by('app_label', 'model')
|
queryset = ObjectType.objects.order_by('app_label', 'model')
|
||||||
serializer_class = serializers.ContentTypeSerializer
|
serializer_class = serializers.ObjectTypeSerializer
|
||||||
filterset_class = filtersets.ContentTypeFilterSet
|
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.urls import NoReverseMatch, resolve, reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.choices import BookmarkOrderingChoices
|
from extras.choices import BookmarkOrderingChoices
|
||||||
from utilities.choices import ButtonColorChoices
|
from utilities.choices import ButtonColorChoices
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
@ -34,14 +34,14 @@ __all__ = (
|
|||||||
def get_object_type_choices():
|
def get_object_type_choices():
|
||||||
return [
|
return [
|
||||||
(content_type_identifier(ct), content_type_name(ct))
|
(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():
|
def get_bookmarks_object_type_choices():
|
||||||
return [
|
return [
|
||||||
(content_type_identifier(ct), content_type_name(ct))
|
(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 = []
|
models = []
|
||||||
for content_type_id in content_types:
|
for content_type_id in content_types:
|
||||||
app_label, model_name = content_type_id.split('.')
|
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())
|
models.append(content_type.model_class())
|
||||||
return models
|
return models
|
||||||
|
|
||||||
@ -238,7 +238,7 @@ class ObjectListWidget(DashboardWidget):
|
|||||||
|
|
||||||
def render(self, request):
|
def render(self, request):
|
||||||
app_label, model_name = self.config['model'].split('.')
|
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')
|
viewname = get_viewname(model, action='list')
|
||||||
|
|
||||||
# Evaluate user's permission. Note that this controls only whether the HTMX element is
|
# 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'])
|
bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by'])
|
||||||
if object_types := self.config.get('object_types'):
|
if object_types := self.config.get('object_types'):
|
||||||
models = get_models_from_content_types(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)
|
bookmarks = bookmarks.filter(object_type__in=conent_types)
|
||||||
if max_items := self.config.get('max_items'):
|
if max_items := self.config.get('max_items'):
|
||||||
bookmarks = bookmarks[:max_items]
|
bookmarks = bookmarks[:max_items]
|
||||||
|
@ -155,7 +155,7 @@ def process_event_queue(events):
|
|||||||
if content_type not in events_cache[action_flag]:
|
if content_type not in events_cache[action_flag]:
|
||||||
events_cache[action_flag][content_type] = EventRule.objects.filter(
|
events_cache[action_flag][content_type] = EventRule.objects.filter(
|
||||||
**{action_flag: True},
|
**{action_flag: True},
|
||||||
content_types=content_type,
|
object_types=content_type,
|
||||||
enabled=True
|
enabled=True
|
||||||
)
|
)
|
||||||
event_rules = events_cache[action_flag][content_type]
|
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.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
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 dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
|
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
@ -18,7 +18,6 @@ __all__ = (
|
|||||||
'BookmarkFilterSet',
|
'BookmarkFilterSet',
|
||||||
'ConfigContextFilterSet',
|
'ConfigContextFilterSet',
|
||||||
'ConfigTemplateFilterSet',
|
'ConfigTemplateFilterSet',
|
||||||
'ContentTypeFilterSet',
|
|
||||||
'CustomFieldChoiceSetFilterSet',
|
'CustomFieldChoiceSetFilterSet',
|
||||||
'CustomFieldFilterSet',
|
'CustomFieldFilterSet',
|
||||||
'CustomLinkFilterSet',
|
'CustomLinkFilterSet',
|
||||||
@ -28,6 +27,7 @@ __all__ = (
|
|||||||
'JournalEntryFilterSet',
|
'JournalEntryFilterSet',
|
||||||
'LocalConfigContextFilterSet',
|
'LocalConfigContextFilterSet',
|
||||||
'ObjectChangeFilterSet',
|
'ObjectChangeFilterSet',
|
||||||
|
'ObjectTypeFilterSet',
|
||||||
'SavedFilterFilterSet',
|
'SavedFilterFilterSet',
|
||||||
'ScriptFilterSet',
|
'ScriptFilterSet',
|
||||||
'TagFilterSet',
|
'TagFilterSet',
|
||||||
@ -89,10 +89,12 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label=_('Search'),
|
label=_('Search'),
|
||||||
)
|
)
|
||||||
content_type_id = MultiValueNumberFilter(
|
object_type_id = MultiValueNumberFilter(
|
||||||
field_name='content_types__id'
|
field_name='object_types__id'
|
||||||
|
)
|
||||||
|
object_type = ContentTypeFilter(
|
||||||
|
field_name='object_types'
|
||||||
)
|
)
|
||||||
content_types = ContentTypeFilter()
|
|
||||||
action_type = django_filters.MultipleChoiceFilter(
|
action_type = django_filters.MultipleChoiceFilter(
|
||||||
choices=EventRuleActionChoices
|
choices=EventRuleActionChoices
|
||||||
)
|
)
|
||||||
@ -124,10 +126,16 @@ class CustomFieldFilterSet(BaseFilterSet):
|
|||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=CustomFieldTypeChoices
|
choices=CustomFieldTypeChoices
|
||||||
)
|
)
|
||||||
content_type_id = MultiValueNumberFilter(
|
object_type_id = MultiValueNumberFilter(
|
||||||
field_name='content_types__id'
|
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(
|
choice_set_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=CustomFieldChoiceSet.objects.all()
|
queryset=CustomFieldChoiceSet.objects.all()
|
||||||
)
|
)
|
||||||
@ -140,8 +148,8 @@ class CustomFieldFilterSet(BaseFilterSet):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'content_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible',
|
'id', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable',
|
||||||
'ui_editable', 'weight', 'is_cloneable', 'description',
|
'weight', 'is_cloneable', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
@ -188,15 +196,17 @@ class CustomLinkFilterSet(BaseFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label=_('Search'),
|
label=_('Search'),
|
||||||
)
|
)
|
||||||
content_type_id = MultiValueNumberFilter(
|
object_type_id = MultiValueNumberFilter(
|
||||||
field_name='content_types__id'
|
field_name='object_types__id'
|
||||||
|
)
|
||||||
|
object_type = ContentTypeFilter(
|
||||||
|
field_name='object_types'
|
||||||
)
|
)
|
||||||
content_types = ContentTypeFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
fields = [
|
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):
|
def search(self, queryset, name, value):
|
||||||
@ -215,10 +225,12 @@ class ExportTemplateFilterSet(BaseFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label=_('Search'),
|
label=_('Search'),
|
||||||
)
|
)
|
||||||
content_type_id = MultiValueNumberFilter(
|
object_type_id = MultiValueNumberFilter(
|
||||||
field_name='content_types__id'
|
field_name='object_types__id'
|
||||||
|
)
|
||||||
|
object_type = ContentTypeFilter(
|
||||||
|
field_name='object_types'
|
||||||
)
|
)
|
||||||
content_types = ContentTypeFilter()
|
|
||||||
data_source_id = django_filters.ModelMultipleChoiceFilter(
|
data_source_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=DataSource.objects.all(),
|
queryset=DataSource.objects.all(),
|
||||||
label=_('Data source (ID)'),
|
label=_('Data source (ID)'),
|
||||||
@ -230,7 +242,7 @@ class ExportTemplateFilterSet(BaseFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
fields = ['id', 'content_types', 'name', 'description', 'data_synced']
|
fields = ['id', 'name', 'description', 'data_synced']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -246,10 +258,12 @@ class SavedFilterFilterSet(BaseFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label=_('Search'),
|
label=_('Search'),
|
||||||
)
|
)
|
||||||
content_type_id = MultiValueNumberFilter(
|
object_type_id = MultiValueNumberFilter(
|
||||||
field_name='content_types__id'
|
field_name='object_types__id'
|
||||||
|
)
|
||||||
|
object_type = ContentTypeFilter(
|
||||||
|
field_name='object_types'
|
||||||
)
|
)
|
||||||
content_types = ContentTypeFilter()
|
|
||||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=get_user_model().objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
label=_('User (ID)'),
|
label=_('User (ID)'),
|
||||||
@ -266,7 +280,7 @@ class SavedFilterFilterSet(BaseFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SavedFilter
|
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):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -316,11 +330,11 @@ class ImageAttachmentFilterSet(BaseFilterSet):
|
|||||||
label=_('Search'),
|
label=_('Search'),
|
||||||
)
|
)
|
||||||
created = django_filters.DateTimeFilter()
|
created = django_filters.DateTimeFilter()
|
||||||
content_type = ContentTypeFilter()
|
object_type = ContentTypeFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ImageAttachment
|
model = ImageAttachment
|
||||||
fields = ['id', 'content_type_id', 'object_id', 'name']
|
fields = ['id', 'object_type_id', 'object_id', 'name']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -660,14 +674,14 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
|||||||
# ContentTypes
|
# ContentTypes
|
||||||
#
|
#
|
||||||
|
|
||||||
class ContentTypeFilterSet(django_filters.FilterSet):
|
class ObjectTypeFilterSet(django_filters.FilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label=_('Search'),
|
label=_('Search'),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContentType
|
model = ObjectType
|
||||||
fields = ['id', 'app_label', 'model']
|
fields = ['id', 'app_label', 'model']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
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.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from netbox.forms import NetBoxModelImportForm
|
from netbox.forms import NetBoxModelImportForm
|
||||||
@ -30,9 +30,9 @@ __all__ = (
|
|||||||
|
|
||||||
|
|
||||||
class CustomFieldImportForm(CSVModelForm):
|
class CustomFieldImportForm(CSVModelForm):
|
||||||
content_types = CSVMultipleContentTypeField(
|
object_types = CSVMultipleContentTypeField(
|
||||||
label=_('Content types'),
|
label=_('Object types'),
|
||||||
queryset=ContentType.objects.with_feature('custom_fields'),
|
queryset=ObjectType.objects.with_feature('custom_fields'),
|
||||||
help_text=_("One or more assigned object types")
|
help_text=_("One or more assigned object types")
|
||||||
)
|
)
|
||||||
type = CSVChoiceField(
|
type = CSVChoiceField(
|
||||||
@ -40,9 +40,9 @@ class CustomFieldImportForm(CSVModelForm):
|
|||||||
choices=CustomFieldTypeChoices,
|
choices=CustomFieldTypeChoices,
|
||||||
help_text=_('Field data type (e.g. text, integer, etc.)')
|
help_text=_('Field data type (e.g. text, integer, etc.)')
|
||||||
)
|
)
|
||||||
object_type = CSVContentTypeField(
|
related_object_type = CSVContentTypeField(
|
||||||
label=_('Object type'),
|
label=_('Object type'),
|
||||||
queryset=ContentType.objects.public(),
|
queryset=ObjectType.objects.public(),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_("Object type (for object or multi-object fields)")
|
help_text=_("Object type (for object or multi-object fields)")
|
||||||
)
|
)
|
||||||
@ -69,7 +69,7 @@ class CustomFieldImportForm(CSVModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = (
|
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',
|
'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
|
||||||
'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable',
|
'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable',
|
||||||
)
|
)
|
||||||
@ -111,31 +111,31 @@ class CustomFieldChoiceSetImportForm(CSVModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class CustomLinkImportForm(CSVModelForm):
|
class CustomLinkImportForm(CSVModelForm):
|
||||||
content_types = CSVMultipleContentTypeField(
|
object_types = CSVMultipleContentTypeField(
|
||||||
label=_('Content types'),
|
label=_('Object types'),
|
||||||
queryset=ContentType.objects.with_feature('custom_links'),
|
queryset=ObjectType.objects.with_feature('custom_links'),
|
||||||
help_text=_("One or more assigned object types")
|
help_text=_("One or more assigned object types")
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
fields = (
|
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',
|
'link_url',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplateImportForm(CSVModelForm):
|
class ExportTemplateImportForm(CSVModelForm):
|
||||||
content_types = CSVMultipleContentTypeField(
|
object_types = CSVMultipleContentTypeField(
|
||||||
label=_('Content types'),
|
label=_('Object types'),
|
||||||
queryset=ContentType.objects.with_feature('export_templates'),
|
queryset=ObjectType.objects.with_feature('export_templates'),
|
||||||
help_text=_("One or more assigned object types")
|
help_text=_("One or more assigned object types")
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
fields = (
|
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):
|
class SavedFilterImportForm(CSVModelForm):
|
||||||
content_types = CSVMultipleContentTypeField(
|
object_types = CSVMultipleContentTypeField(
|
||||||
label=_('Content types'),
|
label=_('Object types'),
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ObjectType.objects.all(),
|
||||||
help_text=_("One or more assigned object types")
|
help_text=_("One or more assigned object types")
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SavedFilter
|
model = SavedFilter
|
||||||
fields = (
|
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):
|
class EventRuleImportForm(NetBoxModelImportForm):
|
||||||
content_types = CSVMultipleContentTypeField(
|
object_types = CSVMultipleContentTypeField(
|
||||||
label=_('Content types'),
|
label=_('Object types'),
|
||||||
queryset=ContentType.objects.with_feature('event_rules'),
|
queryset=ObjectType.objects.with_feature('event_rules'),
|
||||||
help_text=_("One or more assigned object types")
|
help_text=_("One or more assigned object types")
|
||||||
)
|
)
|
||||||
action_object = forms.CharField(
|
action_object = forms.CharField(
|
||||||
@ -187,7 +187,7 @@ class EventRuleImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = EventRule
|
model = EventRule
|
||||||
fields = (
|
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'
|
'type_delete', 'type_job_start', 'type_job_end', 'action_type', 'action_object', 'comments', 'tags'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -213,7 +213,7 @@ class EventRuleImportForm(NetBoxModelImportForm):
|
|||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise forms.ValidationError(_("Script {name} not found").format(name=action_object))
|
raise forms.ValidationError(_("Script {name} not found").format(name=action_object))
|
||||||
self.instance.action_object = script
|
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):
|
class TagImportForm(CSVModelForm):
|
||||||
@ -229,7 +229,7 @@ class TagImportForm(CSVModelForm):
|
|||||||
|
|
||||||
class JournalEntryImportForm(NetBoxModelImportForm):
|
class JournalEntryImportForm(NetBoxModelImportForm):
|
||||||
assigned_object_type = CSVContentTypeField(
|
assigned_object_type = CSVContentTypeField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ObjectType.objects.all(),
|
||||||
label=_('Assigned object type'),
|
label=_('Assigned object type'),
|
||||||
)
|
)
|
||||||
kind = CSVChoiceField(
|
kind = CSVChoiceField(
|
||||||
|
@ -2,7 +2,7 @@ from django import forms
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils.translation import gettext_lazy as _
|
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 dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
@ -38,14 +38,14 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id')),
|
(None, ('q', 'filter_id')),
|
||||||
(_('Attributes'), (
|
(_('Attributes'), (
|
||||||
'type', 'content_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable',
|
'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible',
|
||||||
'is_cloneable',
|
'ui_editable', 'is_cloneable',
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
content_type_id = ContentTypeMultipleChoiceField(
|
related_object_type_id = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.with_feature('custom_fields'),
|
queryset=ObjectType.objects.with_feature('custom_fields'),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Object type')
|
label=_('Related object type')
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
choices=CustomFieldTypeChoices,
|
choices=CustomFieldTypeChoices,
|
||||||
@ -108,11 +108,11 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
|
class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id')),
|
(None, ('q', 'filter_id')),
|
||||||
(_('Attributes'), ('content_types', 'enabled', 'new_window', 'weight')),
|
(_('Attributes'), ('object_type', 'enabled', 'new_window', 'weight')),
|
||||||
)
|
)
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
object_type = ContentTypeMultipleChoiceField(
|
||||||
label=_('Content types'),
|
label=_('Object types'),
|
||||||
queryset=ContentType.objects.with_feature('custom_links'),
|
queryset=ObjectType.objects.with_feature('custom_links'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
enabled = forms.NullBooleanField(
|
enabled = forms.NullBooleanField(
|
||||||
@ -139,7 +139,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id')),
|
(None, ('q', 'filter_id')),
|
||||||
(_('Data'), ('data_source_id', 'data_file_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(
|
data_source_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=DataSource.objects.all(),
|
queryset=DataSource.objects.all(),
|
||||||
@ -154,8 +154,8 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
'source_id': '$data_source_id'
|
'source_id': '$data_source_id'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
content_type_id = ContentTypeMultipleChoiceField(
|
object_type_id = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.with_feature('export_templates'),
|
queryset=ObjectType.objects.with_feature('export_templates'),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Content types')
|
label=_('Content types')
|
||||||
)
|
)
|
||||||
@ -179,11 +179,11 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
|
class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id')),
|
(None, ('q', 'filter_id')),
|
||||||
(_('Attributes'), ('content_type_id', 'name',)),
|
(_('Attributes'), ('object_type_id', 'name',)),
|
||||||
)
|
)
|
||||||
content_type_id = ContentTypeChoiceField(
|
object_type_id = ContentTypeChoiceField(
|
||||||
label=_('Content type'),
|
label=_('Object type'),
|
||||||
queryset=ContentType.objects.with_feature('image_attachments'),
|
queryset=ObjectType.objects.with_feature('image_attachments'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
name = forms.CharField(
|
name = forms.CharField(
|
||||||
@ -195,11 +195,11 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
|
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id')),
|
(None, ('q', 'filter_id')),
|
||||||
(_('Attributes'), ('content_types', 'enabled', 'shared', 'weight')),
|
(_('Attributes'), ('object_type', 'enabled', 'shared', 'weight')),
|
||||||
)
|
)
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
object_type = ContentTypeMultipleChoiceField(
|
||||||
label=_('Content types'),
|
label=_('Object types'),
|
||||||
queryset=ContentType.objects.public(),
|
queryset=ObjectType.objects.public(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
enabled = forms.NullBooleanField(
|
enabled = forms.NullBooleanField(
|
||||||
@ -250,11 +250,11 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
|
|||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id', 'tag')),
|
(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')),
|
(_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
|
||||||
)
|
)
|
||||||
content_type_id = ContentTypeMultipleChoiceField(
|
object_type_id = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.with_feature('event_rules'),
|
queryset=ObjectType.objects.with_feature('event_rules'),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Object type')
|
label=_('Object type')
|
||||||
)
|
)
|
||||||
@ -310,12 +310,12 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class TagFilterForm(SavedFiltersMixin, FilterForm):
|
class TagFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
model = Tag
|
model = Tag
|
||||||
content_type_id = ContentTypeMultipleChoiceField(
|
content_type_id = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.with_feature('tags'),
|
queryset=ObjectType.objects.with_feature('tags'),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Tagged object type')
|
label=_('Tagged object type')
|
||||||
)
|
)
|
||||||
for_object_type_id = ContentTypeChoiceField(
|
for_object_type_id = ContentTypeChoiceField(
|
||||||
queryset=ContentType.objects.with_feature('tags'),
|
queryset=ObjectType.objects.with_feature('tags'),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Allowed object type')
|
label=_('Allowed object type')
|
||||||
)
|
)
|
||||||
@ -464,7 +464,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
|
|||||||
label=_('User')
|
label=_('User')
|
||||||
)
|
)
|
||||||
assigned_object_type_id = DynamicModelMultipleChoiceField(
|
assigned_object_type_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ObjectType.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Object Type'),
|
label=_('Object Type'),
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
@ -507,7 +507,7 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
label=_('User')
|
label=_('User')
|
||||||
)
|
)
|
||||||
changed_object_type_id = DynamicModelMultipleChoiceField(
|
changed_object_type_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ObjectType.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Object Type'),
|
label=_('Object Type'),
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
|
@ -2,12 +2,11 @@ import json
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.forms.mixins import SyncedDataMixin
|
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 dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
@ -39,13 +38,13 @@ __all__ = (
|
|||||||
|
|
||||||
|
|
||||||
class CustomFieldForm(forms.ModelForm):
|
class CustomFieldForm(forms.ModelForm):
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
object_types = ContentTypeMultipleChoiceField(
|
||||||
label=_('Content types'),
|
label=_('Object types'),
|
||||||
queryset=ContentType.objects.with_feature('custom_fields')
|
queryset=ObjectType.objects.with_feature('custom_fields')
|
||||||
)
|
)
|
||||||
object_type = ContentTypeChoiceField(
|
related_object_type = ContentTypeChoiceField(
|
||||||
label=_('Object type'),
|
label=_('Related object type'),
|
||||||
queryset=ContentType.objects.public(),
|
queryset=ObjectType.objects.public(),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_("Type of the related object (for object/multi-object fields only)")
|
help_text=_("Type of the related object (for object/multi-object fields only)")
|
||||||
)
|
)
|
||||||
@ -56,7 +55,7 @@ class CustomFieldForm(forms.ModelForm):
|
|||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(_('Custom Field'), (
|
(_('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')),
|
(_('Behavior'), ('search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable')),
|
||||||
(_('Values'), ('default', 'choice_set')),
|
(_('Values'), ('default', 'choice_set')),
|
||||||
@ -123,13 +122,13 @@ class CustomFieldChoiceSetForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class CustomLinkForm(forms.ModelForm):
|
class CustomLinkForm(forms.ModelForm):
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
object_types = ContentTypeMultipleChoiceField(
|
||||||
label=_('Content types'),
|
label=_('Object types'),
|
||||||
queryset=ContentType.objects.with_feature('custom_links')
|
queryset=ObjectType.objects.with_feature('custom_links')
|
||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
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')),
|
(_('Templates'), ('link_text', 'link_url')),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -152,9 +151,9 @@ class CustomLinkForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
|
class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
object_types = ContentTypeMultipleChoiceField(
|
||||||
label=_('Content types'),
|
label=_('Object types'),
|
||||||
queryset=ContentType.objects.with_feature('export_templates')
|
queryset=ObjectType.objects.with_feature('export_templates')
|
||||||
)
|
)
|
||||||
template_code = forms.CharField(
|
template_code = forms.CharField(
|
||||||
label=_('Template code'),
|
label=_('Template code'),
|
||||||
@ -163,7 +162,7 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
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')),
|
(_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
|
||||||
(_('Rendering'), ('mime_type', 'file_extension', 'as_attachment')),
|
(_('Rendering'), ('mime_type', 'file_extension', 'as_attachment')),
|
||||||
)
|
)
|
||||||
@ -193,14 +192,14 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class SavedFilterForm(forms.ModelForm):
|
class SavedFilterForm(forms.ModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
object_types = ContentTypeMultipleChoiceField(
|
||||||
label=_('Content types'),
|
label=_('Object types'),
|
||||||
queryset=ContentType.objects.all()
|
queryset=ObjectType.objects.all()
|
||||||
)
|
)
|
||||||
parameters = JSONField()
|
parameters = JSONField()
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(_('Saved Filter'), ('name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared')),
|
(_('Saved Filter'), ('name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared')),
|
||||||
(_('Parameters'), ('parameters',)),
|
(_('Parameters'), ('parameters',)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -221,7 +220,7 @@ class SavedFilterForm(forms.ModelForm):
|
|||||||
class BookmarkForm(forms.ModelForm):
|
class BookmarkForm(forms.ModelForm):
|
||||||
object_type = ContentTypeChoiceField(
|
object_type = ContentTypeChoiceField(
|
||||||
label=_('Object type'),
|
label=_('Object type'),
|
||||||
queryset=ContentType.objects.with_feature('bookmarks')
|
queryset=ObjectType.objects.with_feature('bookmarks')
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -249,9 +248,9 @@ class WebhookForm(NetBoxModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class EventRuleForm(NetBoxModelForm):
|
class EventRuleForm(NetBoxModelForm):
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
object_types = ContentTypeMultipleChoiceField(
|
||||||
label=_('Content types'),
|
label=_('Object types'),
|
||||||
queryset=ContentType.objects.with_feature('event_rules'),
|
queryset=ObjectType.objects.with_feature('event_rules'),
|
||||||
)
|
)
|
||||||
action_choice = forms.ChoiceField(
|
action_choice = forms.ChoiceField(
|
||||||
label=_('Action choice'),
|
label=_('Action choice'),
|
||||||
@ -267,7 +266,7 @@ class EventRuleForm(NetBoxModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
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')),
|
(_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
|
||||||
(_('Conditions'), ('conditions',)),
|
(_('Conditions'), ('conditions',)),
|
||||||
(_('Action'), (
|
(_('Action'), (
|
||||||
@ -278,7 +277,7 @@ class EventRuleForm(NetBoxModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = EventRule
|
model = EventRule
|
||||||
fields = (
|
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',
|
'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type', 'action_object_id',
|
||||||
'action_data', 'comments', 'tags'
|
'action_data', 'comments', 'tags'
|
||||||
)
|
)
|
||||||
@ -339,11 +338,11 @@ class EventRuleForm(NetBoxModelForm):
|
|||||||
action_choice = self.cleaned_data.get('action_choice')
|
action_choice = self.cleaned_data.get('action_choice')
|
||||||
# Webhook
|
# Webhook
|
||||||
if self.cleaned_data.get('action_type') == EventRuleActionChoices.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
|
self.cleaned_data['action_object_id'] = action_choice.id
|
||||||
# Script
|
# Script
|
||||||
elif self.cleaned_data.get('action_type') == EventRuleActionChoices.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,
|
Script,
|
||||||
for_concrete_model=False
|
for_concrete_model=False
|
||||||
)
|
)
|
||||||
@ -356,7 +355,7 @@ class TagForm(forms.ModelForm):
|
|||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
object_types = ContentTypeMultipleChoiceField(
|
object_types = ContentTypeMultipleChoiceField(
|
||||||
label=_('Object types'),
|
label=_('Object types'),
|
||||||
queryset=ContentType.objects.with_feature('tags'),
|
queryset=ObjectType.objects.with_feature('tags'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ class ConfigTemplateFilter(filtersets.ConfigTemplateFilterSet):
|
|||||||
@strawberry_django.filter(models.CustomField, lookups=True)
|
@strawberry_django.filter(models.CustomField, lookups=True)
|
||||||
class CustomFieldFilter(filtersets.CustomFieldFilterSet):
|
class CustomFieldFilter(filtersets.CustomFieldFilterSet):
|
||||||
id: auto
|
id: auto
|
||||||
content_types: auto
|
object_types: auto
|
||||||
name: auto
|
name: auto
|
||||||
group_name: auto
|
group_name: auto
|
||||||
required: auto
|
required: auto
|
||||||
@ -62,7 +62,7 @@ class CustomFieldChoiceSetFilter(filtersets.CustomFieldChoiceSetFilterSet):
|
|||||||
@strawberry_django.filter(models.CustomLink, lookups=True)
|
@strawberry_django.filter(models.CustomLink, lookups=True)
|
||||||
class CustomLinkFilter(filtersets.CustomLinkFilterSet):
|
class CustomLinkFilter(filtersets.CustomLinkFilterSet):
|
||||||
id: auto
|
id: auto
|
||||||
content_types: auto
|
object_types: auto
|
||||||
name: auto
|
name: auto
|
||||||
enabled: auto
|
enabled: auto
|
||||||
link_text: auto
|
link_text: auto
|
||||||
@ -75,7 +75,7 @@ class CustomLinkFilter(filtersets.CustomLinkFilterSet):
|
|||||||
@strawberry_django.filter(models.ExportTemplate, lookups=True)
|
@strawberry_django.filter(models.ExportTemplate, lookups=True)
|
||||||
class ExportTemplateFilter(filtersets.ExportTemplateFilterSet):
|
class ExportTemplateFilter(filtersets.ExportTemplateFilterSet):
|
||||||
id: auto
|
id: auto
|
||||||
content_types: auto
|
object_types: auto
|
||||||
name: auto
|
name: auto
|
||||||
description: auto
|
description: auto
|
||||||
data_synced: auto
|
data_synced: auto
|
||||||
@ -84,7 +84,7 @@ class ExportTemplateFilter(filtersets.ExportTemplateFilterSet):
|
|||||||
@strawberry_django.filter(models.ImageAttachment, lookups=True)
|
@strawberry_django.filter(models.ImageAttachment, lookups=True)
|
||||||
class ImageAttachmentFilter(filtersets.ImageAttachmentFilterSet):
|
class ImageAttachmentFilter(filtersets.ImageAttachmentFilterSet):
|
||||||
id: auto
|
id: auto
|
||||||
content_type_id: auto
|
object_type_id: auto
|
||||||
object_id: auto
|
object_id: auto
|
||||||
name: auto
|
name: auto
|
||||||
|
|
||||||
@ -113,7 +113,7 @@ class ObjectChangeFilter(filtersets.ObjectChangeFilterSet):
|
|||||||
@strawberry_django.filter(models.SavedFilter, lookups=True)
|
@strawberry_django.filter(models.SavedFilter, lookups=True)
|
||||||
class SavedFilterFilter(filtersets.SavedFilterFilterSet):
|
class SavedFilterFilter(filtersets.SavedFilterFilterSet):
|
||||||
id: auto
|
id: auto
|
||||||
content_types: auto
|
object_types: auto
|
||||||
name: auto
|
name: auto
|
||||||
slug: auto
|
slug: auto
|
||||||
description: auto
|
description: auto
|
||||||
|
@ -25,7 +25,4 @@ class Migration(migrations.Migration):
|
|||||||
migrations.DeleteModel(
|
migrations.DeleteModel(
|
||||||
name='Report',
|
name='Report',
|
||||||
),
|
),
|
||||||
migrations.DeleteModel(
|
|
||||||
name='ReportModule',
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
@ -82,10 +82,12 @@ def update_scripts(apps, schema_editor):
|
|||||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
Script = apps.get_model('extras', 'Script')
|
Script = apps.get_model('extras', 'Script')
|
||||||
ScriptModule = apps.get_model('extras', 'ScriptModule')
|
ScriptModule = apps.get_model('extras', 'ScriptModule')
|
||||||
|
ReportModule = apps.get_model('extras', 'ReportModule')
|
||||||
Job = apps.get_model('core', 'Job')
|
Job = apps.get_model('core', 'Job')
|
||||||
|
|
||||||
script_ct = ContentType.objects.get_for_model(Script)
|
script_ct = ContentType.objects.get_for_model(Script, for_concrete_model=False)
|
||||||
scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule)
|
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 module in ScriptModule.objects.all():
|
||||||
for script_name in get_module_scripts(module):
|
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
|
# Update all Jobs associated with this ScriptModule & script name to point to the new Script object
|
||||||
Job.objects.filter(
|
Job.objects.filter(
|
||||||
object_type=scriptmodule_ct,
|
object_type_id=scriptmodule_ct.id,
|
||||||
object_id=module.pk,
|
object_id=module.pk,
|
||||||
name=script_name
|
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):
|
def update_event_rules(apps, schema_editor):
|
||||||
|
@ -12,4 +12,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name='eventrule',
|
model_name='eventrule',
|
||||||
name='action_parameters',
|
name='action_parameters',
|
||||||
),
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='ReportModule',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
107
netbox/extras/migrations/0111_rename_content_types.py
Normal file
107
netbox/extras/migrations/0111_rename_content_types.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0010_gfk_indexes'),
|
||||||
|
('extras', '0110_remove_eventrule_action_parameters'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Custom fields
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='customfield',
|
||||||
|
old_name='content_types',
|
||||||
|
new_name='object_types',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='object_types',
|
||||||
|
field=models.ManyToManyField(related_name='custom_fields', to='core.objecttype'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='object_type',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.objecttype'),
|
||||||
|
),
|
||||||
|
migrations.RunSQL(
|
||||||
|
"ALTER TABLE extras_customfield_content_types_id_seq RENAME TO extras_customfield_object_types_id_seq"
|
||||||
|
),
|
||||||
|
|
||||||
|
# Custom links
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='customlink',
|
||||||
|
old_name='content_types',
|
||||||
|
new_name='object_types',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customlink',
|
||||||
|
name='object_types',
|
||||||
|
field=models.ManyToManyField(related_name='custom_links', to='core.objecttype'),
|
||||||
|
),
|
||||||
|
migrations.RunSQL(
|
||||||
|
"ALTER TABLE extras_customlink_content_types_id_seq RENAME TO extras_customlink_object_types_id_seq"
|
||||||
|
),
|
||||||
|
|
||||||
|
# Event rules
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='eventrule',
|
||||||
|
old_name='content_types',
|
||||||
|
new_name='object_types',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventrule',
|
||||||
|
name='object_types',
|
||||||
|
field=models.ManyToManyField(related_name='event_rules', to='core.objecttype'),
|
||||||
|
),
|
||||||
|
migrations.RunSQL(
|
||||||
|
"ALTER TABLE extras_eventrule_content_types_id_seq RENAME TO extras_eventrule_object_types_id_seq"
|
||||||
|
),
|
||||||
|
|
||||||
|
# Export templates
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='exporttemplate',
|
||||||
|
old_name='content_types',
|
||||||
|
new_name='object_types',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='exporttemplate',
|
||||||
|
name='object_types',
|
||||||
|
field=models.ManyToManyField(related_name='export_templates', to='core.objecttype'),
|
||||||
|
),
|
||||||
|
migrations.RunSQL(
|
||||||
|
"ALTER TABLE extras_exporttemplate_content_types_id_seq RENAME TO extras_exporttemplate_object_types_id_seq"
|
||||||
|
),
|
||||||
|
|
||||||
|
# Saved filters
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='savedfilter',
|
||||||
|
old_name='content_types',
|
||||||
|
new_name='object_types',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='savedfilter',
|
||||||
|
name='object_types',
|
||||||
|
field=models.ManyToManyField(related_name='saved_filters', to='core.objecttype'),
|
||||||
|
),
|
||||||
|
migrations.RunSQL(
|
||||||
|
"ALTER TABLE extras_savedfilter_content_types_id_seq RENAME TO extras_savedfilter_object_types_id_seq"
|
||||||
|
),
|
||||||
|
|
||||||
|
# Image attachments
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name='imageattachment',
|
||||||
|
name='extras_imag_content_94728e_idx',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='imageattachment',
|
||||||
|
old_name='content_type',
|
||||||
|
new_name='object_type',
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='imageattachment',
|
||||||
|
index=models.Index(fields=['object_type', 'object_id'], name='extras_imag_object__96bebc_idx'),
|
||||||
|
),
|
||||||
|
]
|
17
netbox/extras/migrations/0112_tag_update_object_types.py
Normal file
17
netbox/extras/migrations/0112_tag_update_object_types.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0010_gfk_indexes'),
|
||||||
|
('extras', '0111_rename_content_types'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tag',
|
||||||
|
name='object_types',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='+', to='core.objecttype'),
|
||||||
|
),
|
||||||
|
]
|
@ -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.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from ..querysets import ObjectChangeQuerySet
|
from ..querysets import ObjectChangeQuerySet
|
||||||
|
|
||||||
@ -113,7 +113,7 @@ class ObjectChange(models.Model):
|
|||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Validate the assigned object type
|
# 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(
|
raise ValidationError(
|
||||||
_("Change logging is not supported for this object type ({type}).").format(
|
_("Change logging is not supported for this object type ({type}).").format(
|
||||||
type=self.changed_object_type
|
type=self.changed_object_type
|
||||||
|
@ -12,7 +12,7 @@ from django.urls import reverse
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.data import CHOICE_SETS
|
from extras.data import CHOICE_SETS
|
||||||
from netbox.models import ChangeLoggedModel
|
from netbox.models import ChangeLoggedModel
|
||||||
@ -52,8 +52,8 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
|||||||
"""
|
"""
|
||||||
Return all CustomFields assigned to the given model.
|
Return all CustomFields assigned to the given model.
|
||||||
"""
|
"""
|
||||||
content_type = ContentType.objects.get_for_model(model._meta.concrete_model)
|
content_type = ObjectType.objects.get_for_model(model._meta.concrete_model)
|
||||||
return self.get_queryset().filter(content_types=content_type)
|
return self.get_queryset().filter(object_types=content_type)
|
||||||
|
|
||||||
def get_defaults_for_model(self, model):
|
def get_defaults_for_model(self, model):
|
||||||
"""
|
"""
|
||||||
@ -66,8 +66,8 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
|||||||
|
|
||||||
|
|
||||||
class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||||
content_types = models.ManyToManyField(
|
object_types = models.ManyToManyField(
|
||||||
to='contenttypes.ContentType',
|
to='core.ObjectType',
|
||||||
related_name='custom_fields',
|
related_name='custom_fields',
|
||||||
help_text=_('The object(s) to which this field applies.')
|
help_text=_('The object(s) to which this field applies.')
|
||||||
)
|
)
|
||||||
@ -78,8 +78,8 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
default=CustomFieldTypeChoices.TYPE_TEXT,
|
default=CustomFieldTypeChoices.TYPE_TEXT,
|
||||||
help_text=_('The type of data this custom field holds')
|
help_text=_('The type of data this custom field holds')
|
||||||
)
|
)
|
||||||
object_type = models.ForeignKey(
|
related_object_type = models.ForeignKey(
|
||||||
to='contenttypes.ContentType',
|
to='core.ObjectType',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
@ -209,7 +209,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
objects = CustomFieldManager()
|
objects = CustomFieldManager()
|
||||||
|
|
||||||
clone_fields = (
|
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',
|
'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
|
||||||
'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable',
|
'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.
|
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()
|
model = ct.model_class()
|
||||||
params = {f'custom_field_data__{old_name}__isnull': False}
|
params = {f'custom_field_data__{old_name}__isnull': False}
|
||||||
instances = model.objects.filter(**params)
|
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
|
# Object fields must define an object_type; other fields must not
|
||||||
if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
|
if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
|
||||||
if not self.object_type:
|
if not self.related_object_type:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'object_type': _("Object fields must define an object type.")
|
'object_type': _("Object fields must define an object type.")
|
||||||
})
|
})
|
||||||
elif self.object_type:
|
elif self.related_object_type:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'object_type': _(
|
'object_type': _(
|
||||||
"{type} fields may not define an object type.")
|
"{type} fields may not define an object type.")
|
||||||
@ -388,10 +388,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return value
|
return value
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
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()
|
return model.objects.filter(pk=value).first()
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
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 model.objects.filter(pk__in=value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@ -488,7 +488,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
|
|
||||||
# Object
|
# Object
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_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_class = CSVModelChoiceField if for_csv_import else DynamicModelChoiceField
|
||||||
field = field_class(
|
field = field_class(
|
||||||
queryset=model.objects.all(),
|
queryset=model.objects.all(),
|
||||||
@ -498,7 +498,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
|
|
||||||
# Multiple objects
|
# Multiple objects
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
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_class = CSVModelMultipleChoiceField if for_csv_import else DynamicModelMultipleChoiceField
|
||||||
field = field_class(
|
field = field_class(
|
||||||
queryset=model.objects.all(),
|
queryset=model.objects.all(),
|
||||||
|
@ -12,7 +12,7 @@ from django.utils.formats import date_format
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.utils.encoders import JSONEncoder
|
from rest_framework.utils.encoders import JSONEncoder
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.conditions import ConditionSet
|
from extras.conditions import ConditionSet
|
||||||
from extras.constants import *
|
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
|
specific type of object is created, modified, or deleted. The action to be taken might entail transmitting a
|
||||||
webhook or executing a custom script.
|
webhook or executing a custom script.
|
||||||
"""
|
"""
|
||||||
content_types = models.ManyToManyField(
|
object_types = models.ManyToManyField(
|
||||||
to='contenttypes.ContentType',
|
to='core.ObjectType',
|
||||||
related_name='eventrules',
|
related_name='event_rules',
|
||||||
verbose_name=_('object types'),
|
verbose_name=_('object types'),
|
||||||
help_text=_("The object(s) to which this rule applies.")
|
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
|
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.
|
code to be rendered with an object as context.
|
||||||
"""
|
"""
|
||||||
content_types = models.ManyToManyField(
|
object_types = models.ManyToManyField(
|
||||||
to='contenttypes.ContentType',
|
to='core.ObjectType',
|
||||||
related_name='custom_links',
|
related_name='custom_links',
|
||||||
help_text=_('The object type(s) to which this link applies.')
|
help_text=_('The object type(s) to which this link applies.')
|
||||||
)
|
)
|
||||||
@ -359,7 +359,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'content_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window',
|
'object_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -409,8 +409,8 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
|
|
||||||
|
|
||||||
class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||||
content_types = models.ManyToManyField(
|
object_types = models.ManyToManyField(
|
||||||
to='contenttypes.ContentType',
|
to='core.ObjectType',
|
||||||
related_name='export_templates',
|
related_name='export_templates',
|
||||||
help_text=_('The object type(s) to which this template applies.')
|
help_text=_('The object type(s) to which this template applies.')
|
||||||
)
|
)
|
||||||
@ -448,7 +448,7 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
|
|||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'content_types', 'template_code', 'mime_type', 'file_extension', 'as_attachment',
|
'object_types', 'template_code', 'mime_type', 'file_extension', 'as_attachment',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
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.
|
A set of predefined keyword parameters that can be reused to filter for specific objects.
|
||||||
"""
|
"""
|
||||||
content_types = models.ManyToManyField(
|
object_types = models.ManyToManyField(
|
||||||
to='contenttypes.ContentType',
|
to='core.ObjectType',
|
||||||
related_name='saved_filters',
|
related_name='saved_filters',
|
||||||
help_text=_('The object type(s) to which this filter applies.')
|
help_text=_('The object type(s) to which this filter applies.')
|
||||||
)
|
)
|
||||||
@ -561,7 +561,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'content_types', 'weight', 'enabled', 'parameters',
|
'object_types', 'weight', 'enabled', 'parameters',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -598,13 +598,13 @@ class ImageAttachment(ChangeLoggedModel):
|
|||||||
"""
|
"""
|
||||||
An uploaded image which is associated with an object.
|
An uploaded image which is associated with an object.
|
||||||
"""
|
"""
|
||||||
content_type = models.ForeignKey(
|
object_type = models.ForeignKey(
|
||||||
to='contenttypes.ContentType',
|
to='contenttypes.ContentType',
|
||||||
on_delete=models.CASCADE
|
on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
object_id = models.PositiveBigIntegerField()
|
object_id = models.PositiveBigIntegerField()
|
||||||
parent = GenericForeignKey(
|
parent = GenericForeignKey(
|
||||||
ct_field='content_type',
|
ct_field='object_type',
|
||||||
fk_field='object_id'
|
fk_field='object_id'
|
||||||
)
|
)
|
||||||
image = models.ImageField(
|
image = models.ImageField(
|
||||||
@ -626,12 +626,12 @@ class ImageAttachment(ChangeLoggedModel):
|
|||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
clone_fields = ('content_type', 'object_id')
|
clone_fields = ('object_type', 'object_id')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('name', 'pk') # name may be non-unique
|
ordering = ('name', 'pk') # name may be non-unique
|
||||||
indexes = (
|
indexes = (
|
||||||
models.Index(fields=('content_type', 'object_id')),
|
models.Index(fields=('object_type', 'object_id')),
|
||||||
)
|
)
|
||||||
verbose_name = _('image attachment')
|
verbose_name = _('image attachment')
|
||||||
verbose_name_plural = _('image attachments')
|
verbose_name_plural = _('image attachments')
|
||||||
@ -646,9 +646,9 @@ class ImageAttachment(ChangeLoggedModel):
|
|||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Validate the assigned object type
|
# 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(
|
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):
|
def delete(self, *args, **kwargs):
|
||||||
@ -739,7 +739,7 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
|
|||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Validate the assigned object type
|
# 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(
|
raise ValidationError(
|
||||||
_("Journaling is not supported for this object type ({type}).").format(type=self.assigned_object_type)
|
_("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()
|
super().clean()
|
||||||
|
|
||||||
# Validate the assigned object type
|
# 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(
|
raise ValidationError(
|
||||||
_("Bookmarks cannot be assigned to this object type ({type}).").format(type=self.object_type)
|
_("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,
|
blank=True,
|
||||||
)
|
)
|
||||||
object_types = models.ManyToManyField(
|
object_types = models.ManyToManyField(
|
||||||
to='contenttypes.ContentType',
|
to='core.ObjectType',
|
||||||
related_name='+',
|
related_name='+',
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_("The object type(s) to which this this tag can be applied.")
|
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.utils.translation import gettext_lazy as _
|
||||||
from django_prometheus.models import model_deletes, model_inserts, model_updates
|
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 core.signals import job_end, job_start
|
||||||
from extras.constants import EVENT_JOB_END, EVENT_JOB_START
|
from extras.constants import EVENT_JOB_END, EVENT_JOB_START
|
||||||
from extras.events import process_event_rules
|
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.
|
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)
|
post_save.connect(handle_cf_renamed, sender=CustomField)
|
||||||
pre_delete.connect(handle_cf_deleted, 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_added_obj_types, sender=CustomField.object_types.through)
|
||||||
m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_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':
|
if action != 'pre_add':
|
||||||
return
|
return
|
||||||
ct = ContentType.objects.get_for_model(instance)
|
ct = ObjectType.objects.get_for_model(instance)
|
||||||
# Retrieve any applied Tags that are restricted to certain object_types
|
# 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'):
|
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():
|
if ct not in tag.object_types.all():
|
||||||
raise AbortRequest(f"Tag {tag} cannot be assigned to {ct.model} objects.")
|
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.
|
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
|
username = sender.user.username if sender.user else None
|
||||||
process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_START, sender.data, username)
|
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.
|
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
|
username = sender.user.username if sender.user else None
|
||||||
process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_END, sender.data, username)
|
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 django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import BaseTable, NetBoxTable, columns
|
||||||
from .template_code import *
|
from .template_code import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -21,6 +21,8 @@ __all__ = (
|
|||||||
'JournalEntryTable',
|
'JournalEntryTable',
|
||||||
'ObjectChangeTable',
|
'ObjectChangeTable',
|
||||||
'SavedFilterTable',
|
'SavedFilterTable',
|
||||||
|
'ReportResultsTable',
|
||||||
|
'ScriptResultsTable',
|
||||||
'TaggedItemTable',
|
'TaggedItemTable',
|
||||||
'TagTable',
|
'TagTable',
|
||||||
'WebhookTable',
|
'WebhookTable',
|
||||||
@ -40,8 +42,8 @@ class CustomFieldTable(NetBoxTable):
|
|||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
content_types = columns.ContentTypesColumn(
|
object_types = columns.ContentTypesColumn(
|
||||||
verbose_name=_('Content Types')
|
verbose_name=_('Object Types')
|
||||||
)
|
)
|
||||||
required = columns.BooleanColumn(
|
required = columns.BooleanColumn(
|
||||||
verbose_name=_('Required')
|
verbose_name=_('Required')
|
||||||
@ -55,6 +57,9 @@ class CustomFieldTable(NetBoxTable):
|
|||||||
description = columns.MarkdownColumn(
|
description = columns.MarkdownColumn(
|
||||||
verbose_name=_('Description')
|
verbose_name=_('Description')
|
||||||
)
|
)
|
||||||
|
related_object_type = columns.ContentTypeColumn(
|
||||||
|
verbose_name=_('Related Object Type')
|
||||||
|
)
|
||||||
choice_set = tables.Column(
|
choice_set = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name=_('Choice Set')
|
verbose_name=_('Choice Set')
|
||||||
@ -71,11 +76,11 @@ class CustomFieldTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description',
|
'pk', 'id', 'name', 'object_types', 'label', 'type', 'related_object_type', 'group_name', 'required',
|
||||||
'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'weight', 'choice_set',
|
'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
|
||||||
'choices', 'created', 'last_updated',
|
'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):
|
class CustomFieldChoiceSetTable(NetBoxTable):
|
||||||
@ -115,8 +120,8 @@ class CustomLinkTable(NetBoxTable):
|
|||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
content_types = columns.ContentTypesColumn(
|
object_types = columns.ContentTypesColumn(
|
||||||
verbose_name=_('Content Types'),
|
verbose_name=_('Object Types'),
|
||||||
)
|
)
|
||||||
enabled = columns.BooleanColumn(
|
enabled = columns.BooleanColumn(
|
||||||
verbose_name=_('Enabled'),
|
verbose_name=_('Enabled'),
|
||||||
@ -128,10 +133,10 @@ class CustomLinkTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
fields = (
|
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',
|
'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):
|
class ExportTemplateTable(NetBoxTable):
|
||||||
@ -139,8 +144,8 @@ class ExportTemplateTable(NetBoxTable):
|
|||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
content_types = columns.ContentTypesColumn(
|
object_types = columns.ContentTypesColumn(
|
||||||
verbose_name=_('Content Types'),
|
verbose_name=_('Object Types'),
|
||||||
)
|
)
|
||||||
as_attachment = columns.BooleanColumn(
|
as_attachment = columns.BooleanColumn(
|
||||||
verbose_name=_('As Attachment'),
|
verbose_name=_('As Attachment'),
|
||||||
@ -161,11 +166,11 @@ class ExportTemplateTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
fields = (
|
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',
|
'data_source', 'data_file', 'data_synced', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
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'),
|
verbose_name=_('ID'),
|
||||||
linkify=False
|
linkify=False
|
||||||
)
|
)
|
||||||
content_type = columns.ContentTypeColumn(
|
object_type = columns.ContentTypeColumn(
|
||||||
verbose_name=_('Content Type'),
|
verbose_name=_('Object Type'),
|
||||||
)
|
)
|
||||||
parent = tables.Column(
|
parent = tables.Column(
|
||||||
verbose_name=_('Parent'),
|
verbose_name=_('Parent'),
|
||||||
@ -193,10 +198,10 @@ class ImageAttachmentTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = ImageAttachment
|
model = ImageAttachment
|
||||||
fields = (
|
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',
|
'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('content_type', 'parent', 'image', 'name', 'size', 'created')
|
default_columns = ('object_type', 'parent', 'image', 'name', 'size', 'created')
|
||||||
|
|
||||||
|
|
||||||
class SavedFilterTable(NetBoxTable):
|
class SavedFilterTable(NetBoxTable):
|
||||||
@ -204,8 +209,8 @@ class SavedFilterTable(NetBoxTable):
|
|||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
content_types = columns.ContentTypesColumn(
|
object_types = columns.ContentTypesColumn(
|
||||||
verbose_name=_('Content Types'),
|
verbose_name=_('Object Types'),
|
||||||
)
|
)
|
||||||
enabled = columns.BooleanColumn(
|
enabled = columns.BooleanColumn(
|
||||||
verbose_name=_('Enabled'),
|
verbose_name=_('Enabled'),
|
||||||
@ -220,11 +225,11 @@ class SavedFilterTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = SavedFilter
|
model = SavedFilter
|
||||||
fields = (
|
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'
|
'created', 'last_updated', 'parameters'
|
||||||
)
|
)
|
||||||
default_columns = (
|
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,
|
linkify=True,
|
||||||
verbose_name=_('Object'),
|
verbose_name=_('Object'),
|
||||||
)
|
)
|
||||||
content_types = columns.ContentTypesColumn(
|
object_types = columns.ContentTypesColumn(
|
||||||
verbose_name=_('Content Types'),
|
verbose_name=_('Object Types'),
|
||||||
)
|
)
|
||||||
enabled = columns.BooleanColumn(
|
enabled = columns.BooleanColumn(
|
||||||
verbose_name=_('Enabled'),
|
verbose_name=_('Enabled'),
|
||||||
@ -309,12 +314,12 @@ class EventRuleTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = EventRule
|
model = EventRule
|
||||||
fields = (
|
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',
|
'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'tags', 'created',
|
||||||
'last_updated',
|
'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
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',
|
'type_delete', 'type_job_start', 'type_job_end',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -507,3 +512,61 @@ class JournalEntryTable(NetBoxTable):
|
|||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments'
|
'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 import template
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from extras.models import CustomLink
|
from extras.models import CustomLink
|
||||||
|
|
||||||
|
|
||||||
@ -32,8 +32,8 @@ def custom_links(context, obj):
|
|||||||
"""
|
"""
|
||||||
Render all applicable links for the given object.
|
Render all applicable links for the given object.
|
||||||
"""
|
"""
|
||||||
content_type = ContentType.objects.get_for_model(obj)
|
object_type = ObjectType.objects.get_for_model(obj)
|
||||||
custom_links = CustomLink.objects.filter(content_types=content_type, enabled=True)
|
custom_links = CustomLink.objects.filter(object_types=object_type, enabled=True)
|
||||||
if not custom_links:
|
if not custom_links:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@ -7,10 +7,10 @@ from django.utils.timezone import make_aware
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from core.choices import ManagedFileRootPathChoices
|
from core.choices import ManagedFileRootPathChoices
|
||||||
|
from core.models import ObjectType
|
||||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from extras.reports import Report
|
|
||||||
from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar
|
from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar
|
||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
|
|||||||
cls.create_data = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
'name': 'EventRule 4',
|
'name': 'EventRule 4',
|
||||||
'content_types': ['dcim.device', 'dcim.devicetype'],
|
'object_types': ['dcim.device', 'dcim.devicetype'],
|
||||||
'type_create': True,
|
'type_create': True,
|
||||||
'action_type': EventRuleActionChoices.WEBHOOK,
|
'action_type': EventRuleActionChoices.WEBHOOK,
|
||||||
'action_object_type': 'extras.webhook',
|
'action_object_type': 'extras.webhook',
|
||||||
@ -130,7 +130,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'EventRule 5',
|
'name': 'EventRule 5',
|
||||||
'content_types': ['dcim.device', 'dcim.devicetype'],
|
'object_types': ['dcim.device', 'dcim.devicetype'],
|
||||||
'type_create': True,
|
'type_create': True,
|
||||||
'action_type': EventRuleActionChoices.WEBHOOK,
|
'action_type': EventRuleActionChoices.WEBHOOK,
|
||||||
'action_object_type': 'extras.webhook',
|
'action_object_type': 'extras.webhook',
|
||||||
@ -138,7 +138,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'EventRule 6',
|
'name': 'EventRule 6',
|
||||||
'content_types': ['dcim.device', 'dcim.devicetype'],
|
'object_types': ['dcim.device', 'dcim.devicetype'],
|
||||||
'type_create': True,
|
'type_create': True,
|
||||||
'action_type': EventRuleActionChoices.WEBHOOK,
|
'action_type': EventRuleActionChoices.WEBHOOK,
|
||||||
'action_object_type': 'extras.webhook',
|
'action_object_type': 'extras.webhook',
|
||||||
@ -152,17 +152,17 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
|
|||||||
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||||
create_data = [
|
create_data = [
|
||||||
{
|
{
|
||||||
'content_types': ['dcim.site'],
|
'object_types': ['dcim.site'],
|
||||||
'name': 'cf4',
|
'name': 'cf4',
|
||||||
'type': 'date',
|
'type': 'date',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_types': ['dcim.site'],
|
'object_types': ['dcim.site'],
|
||||||
'name': 'cf5',
|
'name': 'cf5',
|
||||||
'type': 'url',
|
'type': 'url',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_types': ['dcim.site'],
|
'object_types': ['dcim.site'],
|
||||||
'name': 'cf6',
|
'name': 'cf6',
|
||||||
'type': 'text',
|
'type': 'text',
|
||||||
},
|
},
|
||||||
@ -171,14 +171,14 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
update_data = {
|
update_data = {
|
||||||
'content_types': ['dcim.device'],
|
'object_types': ['dcim.device'],
|
||||||
'name': 'New_Name',
|
'name': 'New_Name',
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
site_ct = ContentType.objects.get_for_model(Site)
|
site_ct = ObjectType.objects.get_for_model(Site)
|
||||||
|
|
||||||
custom_fields = (
|
custom_fields = (
|
||||||
CustomField(
|
CustomField(
|
||||||
@ -196,7 +196,7 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
|
|||||||
)
|
)
|
||||||
CustomField.objects.bulk_create(custom_fields)
|
CustomField.objects.bulk_create(custom_fields)
|
||||||
for cf in custom_fields:
|
for cf in custom_fields:
|
||||||
cf.content_types.add(site_ct)
|
cf.object_types.add(site_ct)
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase):
|
class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase):
|
||||||
@ -273,21 +273,21 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
|||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['display', 'id', 'name', 'url']
|
||||||
create_data = [
|
create_data = [
|
||||||
{
|
{
|
||||||
'content_types': ['dcim.site'],
|
'object_types': ['dcim.site'],
|
||||||
'name': 'Custom Link 4',
|
'name': 'Custom Link 4',
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
'link_text': 'Link 4',
|
'link_text': 'Link 4',
|
||||||
'link_url': 'http://example.com/?4',
|
'link_url': 'http://example.com/?4',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_types': ['dcim.site'],
|
'object_types': ['dcim.site'],
|
||||||
'name': 'Custom Link 5',
|
'name': 'Custom Link 5',
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
'link_text': 'Link 5',
|
'link_text': 'Link 5',
|
||||||
'link_url': 'http://example.com/?5',
|
'link_url': 'http://example.com/?5',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_types': ['dcim.site'],
|
'object_types': ['dcim.site'],
|
||||||
'name': 'Custom Link 6',
|
'name': 'Custom Link 6',
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
'link_text': 'Link 6',
|
'link_text': 'Link 6',
|
||||||
@ -301,7 +301,7 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
site_ct = ContentType.objects.get_for_model(Site)
|
site_type = ObjectType.objects.get_for_model(Site)
|
||||||
|
|
||||||
custom_links = (
|
custom_links = (
|
||||||
CustomLink(
|
CustomLink(
|
||||||
@ -325,7 +325,7 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
|||||||
)
|
)
|
||||||
CustomLink.objects.bulk_create(custom_links)
|
CustomLink.objects.bulk_create(custom_links)
|
||||||
for i, custom_link in enumerate(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):
|
class SavedFilterTest(APIViewTestCases.APIViewTestCase):
|
||||||
@ -333,7 +333,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase):
|
|||||||
brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url']
|
||||||
create_data = [
|
create_data = [
|
||||||
{
|
{
|
||||||
'content_types': ['dcim.site'],
|
'object_types': ['dcim.site'],
|
||||||
'name': 'Saved Filter 4',
|
'name': 'Saved Filter 4',
|
||||||
'slug': 'saved-filter-4',
|
'slug': 'saved-filter-4',
|
||||||
'weight': 100,
|
'weight': 100,
|
||||||
@ -342,7 +342,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'parameters': {'status': ['active']},
|
'parameters': {'status': ['active']},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_types': ['dcim.site'],
|
'object_types': ['dcim.site'],
|
||||||
'name': 'Saved Filter 5',
|
'name': 'Saved Filter 5',
|
||||||
'slug': 'saved-filter-5',
|
'slug': 'saved-filter-5',
|
||||||
'weight': 200,
|
'weight': 200,
|
||||||
@ -351,7 +351,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'parameters': {'status': ['planned']},
|
'parameters': {'status': ['planned']},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_types': ['dcim.site'],
|
'object_types': ['dcim.site'],
|
||||||
'name': 'Saved Filter 6',
|
'name': 'Saved Filter 6',
|
||||||
'slug': 'saved-filter-6',
|
'slug': 'saved-filter-6',
|
||||||
'weight': 300,
|
'weight': 300,
|
||||||
@ -368,7 +368,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
site_ct = ContentType.objects.get_for_model(Site)
|
site_type = ObjectType.objects.get_for_model(Site)
|
||||||
|
|
||||||
saved_filters = (
|
saved_filters = (
|
||||||
SavedFilter(
|
SavedFilter(
|
||||||
@ -398,7 +398,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase):
|
|||||||
)
|
)
|
||||||
SavedFilter.objects.bulk_create(saved_filters)
|
SavedFilter.objects.bulk_create(saved_filters)
|
||||||
for i, savedfilter in enumerate(saved_filters):
|
for i, savedfilter in enumerate(saved_filters):
|
||||||
savedfilter.content_types.set([site_ct])
|
savedfilter.object_types.set([site_type])
|
||||||
|
|
||||||
|
|
||||||
class BookmarkTest(
|
class BookmarkTest(
|
||||||
@ -458,17 +458,17 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||||
create_data = [
|
create_data = [
|
||||||
{
|
{
|
||||||
'content_types': ['dcim.device'],
|
'object_types': ['dcim.device'],
|
||||||
'name': 'Test Export Template 4',
|
'name': 'Test Export Template 4',
|
||||||
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_types': ['dcim.device'],
|
'object_types': ['dcim.device'],
|
||||||
'name': 'Test Export Template 5',
|
'name': 'Test Export Template 5',
|
||||||
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_types': ['dcim.device'],
|
'object_types': ['dcim.device'],
|
||||||
'name': 'Test Export Template 6',
|
'name': 'Test Export Template 6',
|
||||||
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
||||||
},
|
},
|
||||||
@ -495,7 +495,7 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
)
|
)
|
||||||
ExportTemplate.objects.bulk_create(export_templates)
|
ExportTemplate.objects.bulk_create(export_templates)
|
||||||
for et in 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):
|
class TagTest(APIViewTestCases.APIViewTestCase):
|
||||||
@ -548,7 +548,7 @@ class ImageAttachmentTest(
|
|||||||
|
|
||||||
image_attachments = (
|
image_attachments = (
|
||||||
ImageAttachment(
|
ImageAttachment(
|
||||||
content_type=ct,
|
object_type=ct,
|
||||||
object_id=site.pk,
|
object_id=site.pk,
|
||||||
name='Image Attachment 1',
|
name='Image Attachment 1',
|
||||||
image='http://example.com/image1.png',
|
image='http://example.com/image1.png',
|
||||||
@ -556,7 +556,7 @@ class ImageAttachmentTest(
|
|||||||
image_width=100
|
image_width=100
|
||||||
),
|
),
|
||||||
ImageAttachment(
|
ImageAttachment(
|
||||||
content_type=ct,
|
object_type=ct,
|
||||||
object_id=site.pk,
|
object_id=site.pk,
|
||||||
name='Image Attachment 2',
|
name='Image Attachment 2',
|
||||||
image='http://example.com/image2.png',
|
image='http://example.com/image2.png',
|
||||||
@ -564,7 +564,7 @@ class ImageAttachmentTest(
|
|||||||
image_width=100
|
image_width=100
|
||||||
),
|
),
|
||||||
ImageAttachment(
|
ImageAttachment(
|
||||||
content_type=ct,
|
object_type=ct,
|
||||||
object_id=site.pk,
|
object_id=site.pk,
|
||||||
name='Image Attachment 3',
|
name='Image Attachment 3',
|
||||||
image='http://example.com/image3.png',
|
image='http://example.com/image3.png',
|
||||||
@ -876,17 +876,17 @@ class CreatedUpdatedFilterTest(APITestCase):
|
|||||||
self.assertEqual(response.data['results'][0]['id'], rack2.pk)
|
self.assertEqual(response.data['results'][0]['id'], rack2.pk)
|
||||||
|
|
||||||
|
|
||||||
class ContentTypeTest(APITestCase):
|
class ObjectTypeTest(APITestCase):
|
||||||
|
|
||||||
def test_list_objects(self):
|
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.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):
|
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)
|
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 django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from dcim.choices import SiteStatusChoices
|
from dcim.choices import SiteStatusChoices
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
@ -23,14 +24,14 @@ class ChangeLogViewTest(ModelViewTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Create a custom field on the Site model
|
# 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(
|
cf = CustomField(
|
||||||
type=CustomFieldTypeChoices.TYPE_TEXT,
|
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||||
name='cf1',
|
name='cf1',
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([ct])
|
cf.object_types.set([site_type])
|
||||||
|
|
||||||
# Create a select custom field on the Site model
|
# Create a select custom field on the Site model
|
||||||
cf_select = CustomField(
|
cf_select = CustomField(
|
||||||
@ -40,7 +41,7 @@ class ChangeLogViewTest(ModelViewTestCase):
|
|||||||
choice_set=choice_set
|
choice_set=choice_set
|
||||||
)
|
)
|
||||||
cf_select.save()
|
cf_select.save()
|
||||||
cf_select.content_types.set([ct])
|
cf_select.object_types.set([site_type])
|
||||||
|
|
||||||
def test_create_object(self):
|
def test_create_object(self):
|
||||||
tags = create_tags('Tag 1', 'Tag 2')
|
tags = create_tags('Tag 1', 'Tag 2')
|
||||||
@ -275,14 +276,14 @@ class ChangeLogAPITest(APITestCase):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
# Create a custom field on the Site model
|
# 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(
|
cf = CustomField(
|
||||||
type=CustomFieldTypeChoices.TYPE_TEXT,
|
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||||
name='cf1',
|
name='cf1',
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([ct])
|
cf.object_types.set([site_type])
|
||||||
|
|
||||||
# Create a select custom field on the Site model
|
# Create a select custom field on the Site model
|
||||||
choice_set = CustomFieldChoiceSet.objects.create(
|
choice_set = CustomFieldChoiceSet.objects.create(
|
||||||
@ -296,7 +297,7 @@ class ChangeLogAPITest(APITestCase):
|
|||||||
choice_set=choice_set
|
choice_set=choice_set
|
||||||
)
|
)
|
||||||
cf_select.save()
|
cf_select.save()
|
||||||
cf_select.content_types.set([ct])
|
cf_select.object_types.set([site_type])
|
||||||
|
|
||||||
# Create some tags
|
# Create some tags
|
||||||
tags = (
|
tags = (
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from dcim.filtersets import SiteFilterSet
|
from dcim.filtersets import SiteFilterSet
|
||||||
from dcim.forms import SiteImportForm
|
from dcim.forms import SiteImportForm
|
||||||
from dcim.models import Manufacturer, Rack, Site
|
from dcim.models import Manufacturer, Rack, Site
|
||||||
@ -28,7 +28,7 @@ class CustomFieldTest(TestCase):
|
|||||||
Site(name='Site C', slug='site-c'),
|
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):
|
def test_invalid_name(self):
|
||||||
"""
|
"""
|
||||||
@ -50,7 +50,7 @@ class CustomFieldTest(TestCase):
|
|||||||
type=CustomFieldTypeChoices.TYPE_TEXT,
|
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cf.content_types.set([self.object_type])
|
cf.object_types.set([self.object_type])
|
||||||
instance = Site.objects.first()
|
instance = Site.objects.first()
|
||||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ class CustomFieldTest(TestCase):
|
|||||||
type=CustomFieldTypeChoices.TYPE_LONGTEXT,
|
type=CustomFieldTypeChoices.TYPE_LONGTEXT,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cf.content_types.set([self.object_type])
|
cf.object_types.set([self.object_type])
|
||||||
instance = Site.objects.first()
|
instance = Site.objects.first()
|
||||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ class CustomFieldTest(TestCase):
|
|||||||
type=CustomFieldTypeChoices.TYPE_INTEGER,
|
type=CustomFieldTypeChoices.TYPE_INTEGER,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cf.content_types.set([self.object_type])
|
cf.object_types.set([self.object_type])
|
||||||
instance = Site.objects.first()
|
instance = Site.objects.first()
|
||||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
@ -125,7 +125,7 @@ class CustomFieldTest(TestCase):
|
|||||||
type=CustomFieldTypeChoices.TYPE_DECIMAL,
|
type=CustomFieldTypeChoices.TYPE_DECIMAL,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cf.content_types.set([self.object_type])
|
cf.object_types.set([self.object_type])
|
||||||
instance = Site.objects.first()
|
instance = Site.objects.first()
|
||||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
@ -151,7 +151,7 @@ class CustomFieldTest(TestCase):
|
|||||||
type=CustomFieldTypeChoices.TYPE_INTEGER,
|
type=CustomFieldTypeChoices.TYPE_INTEGER,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cf.content_types.set([self.object_type])
|
cf.object_types.set([self.object_type])
|
||||||
instance = Site.objects.first()
|
instance = Site.objects.first()
|
||||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
@ -178,7 +178,7 @@ class CustomFieldTest(TestCase):
|
|||||||
type=CustomFieldTypeChoices.TYPE_DATE,
|
type=CustomFieldTypeChoices.TYPE_DATE,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cf.content_types.set([self.object_type])
|
cf.object_types.set([self.object_type])
|
||||||
instance = Site.objects.first()
|
instance = Site.objects.first()
|
||||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
@ -203,7 +203,7 @@ class CustomFieldTest(TestCase):
|
|||||||
type=CustomFieldTypeChoices.TYPE_DATETIME,
|
type=CustomFieldTypeChoices.TYPE_DATETIME,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cf.content_types.set([self.object_type])
|
cf.object_types.set([self.object_type])
|
||||||
instance = Site.objects.first()
|
instance = Site.objects.first()
|
||||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
@ -228,7 +228,7 @@ class CustomFieldTest(TestCase):
|
|||||||
type=CustomFieldTypeChoices.TYPE_URL,
|
type=CustomFieldTypeChoices.TYPE_URL,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cf.content_types.set([self.object_type])
|
cf.object_types.set([self.object_type])
|
||||||
instance = Site.objects.first()
|
instance = Site.objects.first()
|
||||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
@ -253,7 +253,7 @@ class CustomFieldTest(TestCase):
|
|||||||
type=CustomFieldTypeChoices.TYPE_JSON,
|
type=CustomFieldTypeChoices.TYPE_JSON,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cf.content_types.set([self.object_type])
|
cf.object_types.set([self.object_type])
|
||||||
instance = Site.objects.first()
|
instance = Site.objects.first()
|
||||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
@ -290,7 +290,7 @@ class CustomFieldTest(TestCase):
|
|||||||
required=False,
|
required=False,
|
||||||
choice_set=choice_set
|
choice_set=choice_set
|
||||||
)
|
)
|
||||||
cf.content_types.set([self.object_type])
|
cf.object_types.set([self.object_type])
|
||||||
instance = Site.objects.first()
|
instance = Site.objects.first()
|
||||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
@ -327,7 +327,7 @@ class CustomFieldTest(TestCase):
|
|||||||
required=False,
|
required=False,
|
||||||
choice_set=choice_set
|
choice_set=choice_set
|
||||||
)
|
)
|
||||||
cf.content_types.set([self.object_type])
|
cf.object_types.set([self.object_type])
|
||||||
instance = Site.objects.first()
|
instance = Site.objects.first()
|
||||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
@ -350,10 +350,10 @@ class CustomFieldTest(TestCase):
|
|||||||
cf = CustomField.objects.create(
|
cf = CustomField.objects.create(
|
||||||
name='object_field',
|
name='object_field',
|
||||||
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
||||||
object_type=ContentType.objects.get_for_model(VLAN),
|
related_object_type=ObjectType.objects.get_for_model(VLAN),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cf.content_types.set([self.object_type])
|
cf.object_types.set([self.object_type])
|
||||||
instance = Site.objects.first()
|
instance = Site.objects.first()
|
||||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
@ -382,10 +382,10 @@ class CustomFieldTest(TestCase):
|
|||||||
cf = CustomField.objects.create(
|
cf = CustomField.objects.create(
|
||||||
name='object_field',
|
name='object_field',
|
||||||
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
||||||
object_type=ContentType.objects.get_for_model(VLAN),
|
related_object_type=ObjectType.objects.get_for_model(VLAN),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cf.content_types.set([self.object_type])
|
cf.object_types.set([self.object_type])
|
||||||
instance = Site.objects.first()
|
instance = Site.objects.first()
|
||||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||||
|
|
||||||
@ -402,13 +402,13 @@ class CustomFieldTest(TestCase):
|
|||||||
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
def test_rename_customfield(self):
|
def test_rename_customfield(self):
|
||||||
obj_type = ContentType.objects.get_for_model(Site)
|
obj_type = ObjectType.objects.get_for_model(Site)
|
||||||
FIELD_DATA = 'abc'
|
FIELD_DATA = 'abc'
|
||||||
|
|
||||||
# Create a custom field
|
# Create a custom field
|
||||||
cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1')
|
cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1')
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.object_types.set([obj_type])
|
||||||
|
|
||||||
# Assign custom field data to an object
|
# Assign custom field data to an object
|
||||||
site = Site.objects.create(
|
site = Site.objects.create(
|
||||||
@ -437,7 +437,7 @@ class CustomFieldTest(TestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
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
|
# Text
|
||||||
CustomField(name='test', type='text', required=True, default="Default text").full_clean()
|
CustomField(name='test', type='text', required=True, default="Default text").full_clean()
|
||||||
@ -498,16 +498,28 @@ class CustomFieldTest(TestCase):
|
|||||||
).full_clean()
|
).full_clean()
|
||||||
|
|
||||||
# Object
|
# Object
|
||||||
CustomField(name='test', type='object', required=True, object_type=object_type, default=site.pk).full_clean()
|
CustomField(
|
||||||
with self.assertRaises(ValidationError):
|
name='test',
|
||||||
CustomField(name='test', type='object', required=True, object_type=object_type, default="xxx").full_clean()
|
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
|
# Multi-object
|
||||||
CustomField(
|
CustomField(
|
||||||
name='test',
|
name='test',
|
||||||
type='multiobject',
|
type='multiobject',
|
||||||
required=True,
|
required=True,
|
||||||
object_type=object_type,
|
related_object_type=object_type,
|
||||||
default=[site.pk]
|
default=[site.pk]
|
||||||
).full_clean()
|
).full_clean()
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
@ -515,7 +527,7 @@ class CustomFieldTest(TestCase):
|
|||||||
name='test',
|
name='test',
|
||||||
type='multiobject',
|
type='multiobject',
|
||||||
required=True,
|
required=True,
|
||||||
object_type=object_type,
|
related_object_type=object_type,
|
||||||
default=["xxx"]
|
default=["xxx"]
|
||||||
).full_clean()
|
).full_clean()
|
||||||
|
|
||||||
@ -524,10 +536,10 @@ class CustomFieldManagerTest(TestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
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 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo')
|
||||||
custom_field.save()
|
custom_field.save()
|
||||||
custom_field.content_types.set([content_type])
|
custom_field.object_types.set([object_type])
|
||||||
|
|
||||||
def test_get_for_model(self):
|
def test_get_for_model(self):
|
||||||
self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1)
|
self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1)
|
||||||
@ -538,7 +550,7 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
content_type = ContentType.objects.get_for_model(Site)
|
object_type = ObjectType.objects.get_for_model(Site)
|
||||||
|
|
||||||
# Create some VLANs
|
# Create some VLANs
|
||||||
vlans = (
|
vlans = (
|
||||||
@ -581,19 +593,19 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
CustomField(
|
CustomField(
|
||||||
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
||||||
name='object_field',
|
name='object_field',
|
||||||
object_type=ContentType.objects.get_for_model(VLAN),
|
related_object_type=ObjectType.objects.get_for_model(VLAN),
|
||||||
default=vlans[0].pk,
|
default=vlans[0].pk,
|
||||||
),
|
),
|
||||||
CustomField(
|
CustomField(
|
||||||
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
||||||
name='multiobject_field',
|
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],
|
default=[vlans[0].pk, vlans[1].pk],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for cf in custom_fields:
|
for cf in custom_fields:
|
||||||
cf.save()
|
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
|
# Create some sites *after* creating the custom fields. This ensures that
|
||||||
# default values are not set for the assigned objects.
|
# default values are not set for the assigned objects.
|
||||||
@ -1163,7 +1175,7 @@ class CustomFieldImportTest(TestCase):
|
|||||||
)
|
)
|
||||||
for cf in custom_fields:
|
for cf in custom_fields:
|
||||||
cf.save()
|
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):
|
def test_import(self):
|
||||||
"""
|
"""
|
||||||
@ -1256,11 +1268,11 @@ class CustomFieldModelTest(TestCase):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='foo')
|
cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='foo')
|
||||||
cf1.save()
|
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 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='bar')
|
||||||
cf2.save()
|
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):
|
def test_cf_data(self):
|
||||||
"""
|
"""
|
||||||
@ -1299,7 +1311,7 @@ class CustomFieldModelTest(TestCase):
|
|||||||
"""
|
"""
|
||||||
cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='baz', required=True)
|
cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='baz', required=True)
|
||||||
cf3.save()
|
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')
|
site = Site(name='Test Site', slug='test-site')
|
||||||
|
|
||||||
@ -1318,7 +1330,7 @@ class CustomFieldModelFilterTest(TestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
obj_type = ContentType.objects.get_for_model(Site)
|
object_type = ObjectType.objects.get_for_model(Site)
|
||||||
|
|
||||||
manufacturers = Manufacturer.objects.bulk_create((
|
manufacturers = Manufacturer.objects.bulk_create((
|
||||||
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
|
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
|
||||||
@ -1335,17 +1347,17 @@ class CustomFieldModelFilterTest(TestCase):
|
|||||||
# Integer filtering
|
# Integer filtering
|
||||||
cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER)
|
cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.object_types.set([object_type])
|
||||||
|
|
||||||
# Decimal filtering
|
# Decimal filtering
|
||||||
cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_DECIMAL)
|
cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_DECIMAL)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.object_types.set([object_type])
|
||||||
|
|
||||||
# Boolean filtering
|
# Boolean filtering
|
||||||
cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
|
cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.object_types.set([object_type])
|
||||||
|
|
||||||
# Exact text filtering
|
# Exact text filtering
|
||||||
cf = CustomField(
|
cf = CustomField(
|
||||||
@ -1354,7 +1366,7 @@ class CustomFieldModelFilterTest(TestCase):
|
|||||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
|
||||||
)
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.object_types.set([object_type])
|
||||||
|
|
||||||
# Loose text filtering
|
# Loose text filtering
|
||||||
cf = CustomField(
|
cf = CustomField(
|
||||||
@ -1363,12 +1375,12 @@ class CustomFieldModelFilterTest(TestCase):
|
|||||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
|
||||||
)
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.object_types.set([object_type])
|
||||||
|
|
||||||
# Date filtering
|
# Date filtering
|
||||||
cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_DATE)
|
cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_DATE)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.object_types.set([object_type])
|
||||||
|
|
||||||
# Exact URL filtering
|
# Exact URL filtering
|
||||||
cf = CustomField(
|
cf = CustomField(
|
||||||
@ -1377,7 +1389,7 @@ class CustomFieldModelFilterTest(TestCase):
|
|||||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
|
||||||
)
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.object_types.set([object_type])
|
||||||
|
|
||||||
# Loose URL filtering
|
# Loose URL filtering
|
||||||
cf = CustomField(
|
cf = CustomField(
|
||||||
@ -1386,7 +1398,7 @@ class CustomFieldModelFilterTest(TestCase):
|
|||||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
|
||||||
)
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.object_types.set([object_type])
|
||||||
|
|
||||||
# Selection filtering
|
# Selection filtering
|
||||||
cf = CustomField(
|
cf = CustomField(
|
||||||
@ -1395,7 +1407,7 @@ class CustomFieldModelFilterTest(TestCase):
|
|||||||
choice_set=choice_set
|
choice_set=choice_set
|
||||||
)
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.object_types.set([object_type])
|
||||||
|
|
||||||
# Multiselect filtering
|
# Multiselect filtering
|
||||||
cf = CustomField(
|
cf = CustomField(
|
||||||
@ -1404,25 +1416,25 @@ class CustomFieldModelFilterTest(TestCase):
|
|||||||
choice_set=choice_set
|
choice_set=choice_set
|
||||||
)
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.object_types.set([object_type])
|
||||||
|
|
||||||
# Object filtering
|
# Object filtering
|
||||||
cf = CustomField(
|
cf = CustomField(
|
||||||
name='cf11',
|
name='cf11',
|
||||||
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
||||||
object_type=ContentType.objects.get_for_model(Manufacturer)
|
related_object_type=ObjectType.objects.get_for_model(Manufacturer)
|
||||||
)
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.object_types.set([object_type])
|
||||||
|
|
||||||
# Multi-object filtering
|
# Multi-object filtering
|
||||||
cf = CustomField(
|
cf = CustomField(
|
||||||
name='cf12',
|
name='cf12',
|
||||||
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
||||||
object_type=ContentType.objects.get_for_model(Manufacturer)
|
related_object_type=ObjectType.objects.get_for_model(Manufacturer)
|
||||||
)
|
)
|
||||||
cf.save()
|
cf.save()
|
||||||
cf.content_types.set([obj_type])
|
cf.object_types.set([object_type])
|
||||||
|
|
||||||
Site.objects.bulk_create([
|
Site.objects.bulk_create([
|
||||||
Site(name='Site 1', slug='site-1', custom_field_data={
|
Site(name='Site 1', slug='site-1', custom_field_data={
|
||||||
|
@ -3,17 +3,18 @@ import uuid
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import django_rq
|
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.http import HttpResponse
|
||||||
from django.urls import reverse
|
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.choices import EventRuleActionChoices, ObjectChangeActionChoices
|
||||||
from extras.events import enqueue_object, flush_events, serialize_for_event
|
from extras.events import enqueue_object, flush_events, serialize_for_event
|
||||||
from extras.models import EventRule, Tag, Webhook
|
from extras.models import EventRule, Tag, Webhook
|
||||||
from extras.webhooks import generate_signature, send_webhook
|
from extras.webhooks import generate_signature, send_webhook
|
||||||
from requests import Session
|
|
||||||
from rest_framework import status
|
|
||||||
from utilities.testing import APITestCase
|
from utilities.testing import APITestCase
|
||||||
|
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ class EventRuleTest(APITestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
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_URL = 'http://localhost:9000/'
|
||||||
DUMMY_SECRET = 'LOOKATMEIMASECRETSTRING'
|
DUMMY_SECRET = 'LOOKATMEIMASECRETSTRING'
|
||||||
|
|
||||||
@ -39,32 +40,32 @@ class EventRuleTest(APITestCase):
|
|||||||
Webhook(name='Webhook 3', payload_url=DUMMY_URL, secret=DUMMY_SECRET),
|
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((
|
event_rules = EventRule.objects.bulk_create((
|
||||||
EventRule(
|
EventRule(
|
||||||
name='Webhook Event 1',
|
name='Webhook Event 1',
|
||||||
type_create=True,
|
type_create=True,
|
||||||
action_type=EventRuleActionChoices.WEBHOOK,
|
action_type=EventRuleActionChoices.WEBHOOK,
|
||||||
action_object_type=ct,
|
action_object_type=webhook_type,
|
||||||
action_object_id=webhooks[0].id
|
action_object_id=webhooks[0].id
|
||||||
),
|
),
|
||||||
EventRule(
|
EventRule(
|
||||||
name='Webhook Event 2',
|
name='Webhook Event 2',
|
||||||
type_update=True,
|
type_update=True,
|
||||||
action_type=EventRuleActionChoices.WEBHOOK,
|
action_type=EventRuleActionChoices.WEBHOOK,
|
||||||
action_object_type=ct,
|
action_object_type=webhook_type,
|
||||||
action_object_id=webhooks[0].id
|
action_object_id=webhooks[0].id
|
||||||
),
|
),
|
||||||
EventRule(
|
EventRule(
|
||||||
name='Webhook Event 3',
|
name='Webhook Event 3',
|
||||||
type_delete=True,
|
type_delete=True,
|
||||||
action_type=EventRuleActionChoices.WEBHOOK,
|
action_type=EventRuleActionChoices.WEBHOOK,
|
||||||
action_object_type=ct,
|
action_object_type=webhook_type,
|
||||||
action_object_id=webhooks[0].id
|
action_object_id=webhooks[0].id
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
for event_rule in event_rules:
|
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.objects.bulk_create((
|
||||||
Tag(name='Foo', slug='foo'),
|
Tag(name='Foo', slug='foo'),
|
||||||
|
@ -7,6 +7,7 @@ from django.test import TestCase
|
|||||||
|
|
||||||
from circuits.models import Provider
|
from circuits.models import Provider
|
||||||
from core.choices import ManagedFileRootPathChoices
|
from core.choices import ManagedFileRootPathChoices
|
||||||
|
from core.models import ObjectType
|
||||||
from dcim.filtersets import SiteFilterSet
|
from dcim.filtersets import SiteFilterSet
|
||||||
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
|
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
|
||||||
from dcim.models import Location
|
from dcim.models import Location
|
||||||
@ -85,13 +86,23 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
|
|||||||
ui_editable=CustomFieldUIEditableChoices.HIDDEN,
|
ui_editable=CustomFieldUIEditableChoices.HIDDEN,
|
||||||
choice_set=choice_sets[1]
|
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)
|
CustomField.objects.bulk_create(custom_fields)
|
||||||
custom_fields[0].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'site'))
|
custom_fields[0].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'site'))
|
||||||
custom_fields[1].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'rack'))
|
custom_fields[1].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'rack'))
|
||||||
custom_fields[2].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device'))
|
custom_fields[2].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device'))
|
||||||
custom_fields[3].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device'))
|
custom_fields[3].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device'))
|
||||||
custom_fields[4].content_types.add(ContentType.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):
|
def test_q(self):
|
||||||
params = {'q': 'foobar1'}
|
params = {'q': 'foobar1'}
|
||||||
@ -101,10 +112,16 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
|
|||||||
params = {'name': ['Custom Field 1', 'Custom Field 2']}
|
params = {'name': ['Custom Field 1', 'Custom Field 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_content_types(self):
|
def test_object_type(self):
|
||||||
params = {'content_types': 'dcim.site'}
|
params = {'object_type': 'dcim.site'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
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)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
def test_required(self):
|
def test_required(self):
|
||||||
@ -174,8 +191,6 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
content_types = ContentType.objects.filter(model__in=['region', 'site', 'rack', 'location', 'device'])
|
|
||||||
|
|
||||||
webhooks = (
|
webhooks = (
|
||||||
Webhook(
|
Webhook(
|
||||||
name='Webhook 1',
|
name='Webhook 1',
|
||||||
@ -240,7 +255,7 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
content_types = ContentType.objects.filter(
|
object_types = ObjectType.objects.filter(
|
||||||
model__in=['region', 'site', 'rack', 'location', 'device']
|
model__in=['region', 'site', 'rack', 'location', 'device']
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -333,11 +348,11 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
EventRule.objects.bulk_create(event_rules)
|
EventRule.objects.bulk_create(event_rules)
|
||||||
event_rules[0].content_types.add(content_types[0])
|
event_rules[0].object_types.add(object_types[0])
|
||||||
event_rules[1].content_types.add(content_types[1])
|
event_rules[1].object_types.add(object_types[1])
|
||||||
event_rules[2].content_types.add(content_types[2])
|
event_rules[2].object_types.add(object_types[2])
|
||||||
event_rules[3].content_types.add(content_types[3])
|
event_rules[3].object_types.add(object_types[3])
|
||||||
event_rules[4].content_types.add(content_types[4])
|
event_rules[4].object_types.add(object_types[4])
|
||||||
|
|
||||||
def test_q(self):
|
def test_q(self):
|
||||||
params = {'q': 'foobar1'}
|
params = {'q': 'foobar1'}
|
||||||
@ -351,10 +366,10 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
|
|||||||
params = {'description': ['foobar1', 'foobar2']}
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_content_types(self):
|
def test_object_type(self):
|
||||||
params = {'content_types': 'dcim.region'}
|
params = {'object_type': 'dcim.region'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
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)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
def test_action_type(self):
|
def test_action_type(self):
|
||||||
@ -396,7 +411,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
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 = (
|
custom_links = (
|
||||||
CustomLink(
|
CustomLink(
|
||||||
@ -426,7 +441,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
)
|
)
|
||||||
CustomLink.objects.bulk_create(custom_links)
|
CustomLink.objects.bulk_create(custom_links)
|
||||||
for i, custom_link in enumerate(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):
|
def test_q(self):
|
||||||
params = {'q': 'Custom Link 1'}
|
params = {'q': 'Custom Link 1'}
|
||||||
@ -436,10 +451,10 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
params = {'name': ['Custom Link 1', 'Custom Link 2']}
|
params = {'name': ['Custom Link 1', 'Custom Link 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_content_types(self):
|
def test_object_type(self):
|
||||||
params = {'content_types': 'dcim.site'}
|
params = {'object_type': 'dcim.site'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
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)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
def test_weight(self):
|
def test_weight(self):
|
||||||
@ -465,7 +480,7 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
|
object_types = ObjectType.objects.filter(model__in=['site', 'rack', 'device'])
|
||||||
|
|
||||||
users = (
|
users = (
|
||||||
User(username='User 1'),
|
User(username='User 1'),
|
||||||
@ -508,7 +523,7 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests):
|
|||||||
)
|
)
|
||||||
SavedFilter.objects.bulk_create(saved_filters)
|
SavedFilter.objects.bulk_create(saved_filters)
|
||||||
for i, savedfilter in enumerate(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):
|
def test_q(self):
|
||||||
params = {'q': 'foobar1'}
|
params = {'q': 'foobar1'}
|
||||||
@ -526,10 +541,10 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests):
|
|||||||
params = {'description': ['foobar1', 'foobar2']}
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_content_types(self):
|
def test_object_type(self):
|
||||||
params = {'content_types': 'dcim.site'}
|
params = {'object_type': 'dcim.site'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
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)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
def test_user(self):
|
def test_user(self):
|
||||||
@ -638,7 +653,7 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
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 = (
|
export_templates = (
|
||||||
ExportTemplate(name='Export Template 1', template_code='TESTING', description='foobar1'),
|
ExportTemplate(name='Export Template 1', template_code='TESTING', description='foobar1'),
|
||||||
@ -647,7 +662,7 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
|
|||||||
)
|
)
|
||||||
ExportTemplate.objects.bulk_create(export_templates)
|
ExportTemplate.objects.bulk_create(export_templates)
|
||||||
for i, et in enumerate(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):
|
def test_q(self):
|
||||||
params = {'q': 'foobar1'}
|
params = {'q': 'foobar1'}
|
||||||
@ -657,10 +672,10 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
|
|||||||
params = {'name': ['Export Template 1', 'Export Template 2']}
|
params = {'name': ['Export Template 1', 'Export Template 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_content_types(self):
|
def test_object_type(self):
|
||||||
params = {'content_types': 'dcim.site'}
|
params = {'object_type': 'dcim.site'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
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)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
def test_description(self):
|
def test_description(self):
|
||||||
@ -692,7 +707,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
|
|||||||
|
|
||||||
image_attachments = (
|
image_attachments = (
|
||||||
ImageAttachment(
|
ImageAttachment(
|
||||||
content_type=site_ct,
|
object_type=site_ct,
|
||||||
object_id=sites[0].pk,
|
object_id=sites[0].pk,
|
||||||
name='Image Attachment 1',
|
name='Image Attachment 1',
|
||||||
image='http://example.com/image1.png',
|
image='http://example.com/image1.png',
|
||||||
@ -700,7 +715,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
|
|||||||
image_width=100
|
image_width=100
|
||||||
),
|
),
|
||||||
ImageAttachment(
|
ImageAttachment(
|
||||||
content_type=site_ct,
|
object_type=site_ct,
|
||||||
object_id=sites[1].pk,
|
object_id=sites[1].pk,
|
||||||
name='Image Attachment 2',
|
name='Image Attachment 2',
|
||||||
image='http://example.com/image2.png',
|
image='http://example.com/image2.png',
|
||||||
@ -708,7 +723,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
|
|||||||
image_width=100
|
image_width=100
|
||||||
),
|
),
|
||||||
ImageAttachment(
|
ImageAttachment(
|
||||||
content_type=rack_ct,
|
object_type=rack_ct,
|
||||||
object_id=racks[0].pk,
|
object_id=racks[0].pk,
|
||||||
name='Image Attachment 3',
|
name='Image Attachment 3',
|
||||||
image='http://example.com/image3.png',
|
image='http://example.com/image3.png',
|
||||||
@ -716,7 +731,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
|
|||||||
image_width=100
|
image_width=100
|
||||||
),
|
),
|
||||||
ImageAttachment(
|
ImageAttachment(
|
||||||
content_type=rack_ct,
|
object_type=rack_ct,
|
||||||
object_id=racks[1].pk,
|
object_id=racks[1].pk,
|
||||||
name='Image Attachment 4',
|
name='Image Attachment 4',
|
||||||
image='http://example.com/image4.png',
|
image='http://example.com/image4.png',
|
||||||
@ -734,13 +749,13 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
|
|||||||
params = {'name': ['Image Attachment 1', 'Image Attachment 2']}
|
params = {'name': ['Image Attachment 1', 'Image Attachment 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_content_type(self):
|
def test_object_type(self):
|
||||||
params = {'content_type': 'dcim.site'}
|
params = {'object_type': 'dcim.site'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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 = {
|
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],
|
'object_id': [Site.objects.first().pk],
|
||||||
}
|
}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
@ -1113,9 +1128,9 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
content_types = {
|
object_types = {
|
||||||
'site': ContentType.objects.get_by_natural_key('dcim', 'site'),
|
'site': ObjectType.objects.get_by_natural_key('dcim', 'site'),
|
||||||
'provider': ContentType.objects.get_by_natural_key('circuits', 'provider'),
|
'provider': ObjectType.objects.get_by_natural_key('circuits', 'provider'),
|
||||||
}
|
}
|
||||||
|
|
||||||
tags = (
|
tags = (
|
||||||
@ -1124,8 +1139,8 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tag(name='Tag 3', slug='tag-3', color='0000ff'),
|
Tag(name='Tag 3', slug='tag-3', color='0000ff'),
|
||||||
)
|
)
|
||||||
Tag.objects.bulk_create(tags)
|
Tag.objects.bulk_create(tags)
|
||||||
tags[0].object_types.add(content_types['site'])
|
tags[0].object_types.add(object_types['site'])
|
||||||
tags[1].object_types.add(content_types['provider'])
|
tags[1].object_types.add(object_types['provider'])
|
||||||
|
|
||||||
# Apply some tags so we can filter by content type
|
# Apply some tags so we can filter by content type
|
||||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
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)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_object_types(self):
|
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(
|
self.assertEqual(
|
||||||
list(self.filterset(params, self.queryset).qs.values_list('name', flat=True)),
|
list(self.filterset(params, self.queryset).qs.values_list('name', flat=True)),
|
||||||
['Tag 1', 'Tag 3']
|
['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(
|
self.assertEqual(
|
||||||
list(self.filterset(params, self.queryset).qs.values_list('name', flat=True)),
|
list(self.filterset(params, self.queryset).qs.values_list('name', flat=True)),
|
||||||
['Tag 2', 'Tag 3']
|
['Tag 2', 'Tag 3']
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from dcim.forms import SiteForm
|
from dcim.forms import SiteForm
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.choices import CustomFieldTypeChoices
|
from extras.choices import CustomFieldTypeChoices
|
||||||
@ -12,66 +12,66 @@ class CustomFieldModelFormTest(TestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
obj_type = ContentType.objects.get_for_model(Site)
|
object_type = ObjectType.objects.get_for_model(Site)
|
||||||
choice_set = CustomFieldChoiceSet.objects.create(
|
choice_set = CustomFieldChoiceSet.objects.create(
|
||||||
name='Choice Set 1',
|
name='Choice Set 1',
|
||||||
extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'))
|
extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'))
|
||||||
)
|
)
|
||||||
|
|
||||||
cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT)
|
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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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(
|
cf_select = CustomField.objects.create(
|
||||||
name='select',
|
name='select',
|
||||||
type=CustomFieldTypeChoices.TYPE_SELECT,
|
type=CustomFieldTypeChoices.TYPE_SELECT,
|
||||||
choice_set=choice_set
|
choice_set=choice_set
|
||||||
)
|
)
|
||||||
cf_select.content_types.set([obj_type])
|
cf_select.object_types.set([object_type])
|
||||||
|
|
||||||
cf_multiselect = CustomField.objects.create(
|
cf_multiselect = CustomField.objects.create(
|
||||||
name='multiselect',
|
name='multiselect',
|
||||||
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
|
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
|
||||||
choice_set=choice_set
|
choice_set=choice_set
|
||||||
)
|
)
|
||||||
cf_multiselect.content_types.set([obj_type])
|
cf_multiselect.object_types.set([object_type])
|
||||||
|
|
||||||
cf_object = CustomField.objects.create(
|
cf_object = CustomField.objects.create(
|
||||||
name='object',
|
name='object',
|
||||||
type=CustomFieldTypeChoices.TYPE_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(
|
cf_multiobject = CustomField.objects.create(
|
||||||
name='multiobject',
|
name='multiobject',
|
||||||
type=CustomFieldTypeChoices.TYPE_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):
|
def test_empty_values(self):
|
||||||
"""
|
"""
|
||||||
@ -99,7 +99,7 @@ class SavedFilterFormTest(TestCase):
|
|||||||
form = SavedFilterForm({
|
form = SavedFilterForm({
|
||||||
'name': 'test-sf',
|
'name': 'test-sf',
|
||||||
'slug': '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,
|
'weight': 100,
|
||||||
'parameters': {
|
'parameters': {
|
||||||
"status": [
|
"status": [
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
|
||||||
from extras.models import ConfigContext, Tag
|
from extras.models import ConfigContext, Tag
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
@ -22,7 +22,7 @@ class TagTest(TestCase):
|
|||||||
|
|
||||||
# Create a Tag that can only be applied to Regions
|
# Create a Tag that can only be applied to Regions
|
||||||
tag = Tag.objects.create(name='Tag 1', slug='tag-1')
|
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
|
# Apply the Tag to a Region
|
||||||
region.tags.add(tag)
|
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.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from dcim.models import DeviceType, Manufacturer, Site
|
from dcim.models import DeviceType, Manufacturer, Site
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
@ -19,7 +20,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
site_ct = ContentType.objects.get_for_model(Site)
|
site_type = ObjectType.objects.get_for_model(Site)
|
||||||
CustomFieldChoiceSet.objects.create(
|
CustomFieldChoiceSet.objects.create(
|
||||||
name='Choice Set 1',
|
name='Choice Set 1',
|
||||||
extra_choices=(
|
extra_choices=(
|
||||||
@ -36,13 +37,13 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
)
|
)
|
||||||
for customfield in custom_fields:
|
for customfield in custom_fields:
|
||||||
customfield.save()
|
customfield.save()
|
||||||
customfield.content_types.add(site_ct)
|
customfield.object_types.add(site_type)
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'field_x',
|
'name': 'field_x',
|
||||||
'label': 'Field X',
|
'label': 'Field X',
|
||||||
'type': 'text',
|
'type': 'text',
|
||||||
'content_types': [site_ct.pk],
|
'object_types': [site_type.pk],
|
||||||
'search_weight': 2000,
|
'search_weight': 2000,
|
||||||
'filter_logic': CustomFieldFilterLogicChoices.FILTER_EXACT,
|
'filter_logic': CustomFieldFilterLogicChoices.FILTER_EXACT,
|
||||||
'default': None,
|
'default': None,
|
||||||
@ -53,7 +54,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
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',
|
'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',
|
'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',
|
'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,always,yes',
|
||||||
@ -137,7 +138,7 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
site_ct = ContentType.objects.get_for_model(Site)
|
site_type = ObjectType.objects.get_for_model(Site)
|
||||||
custom_links = (
|
custom_links = (
|
||||||
CustomLink(name='Custom Link 1', enabled=True, link_text='Link 1', link_url='http://example.com/?1'),
|
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'),
|
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)
|
CustomLink.objects.bulk_create(custom_links)
|
||||||
for i, custom_link in enumerate(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 = {
|
cls.form_data = {
|
||||||
'name': 'Custom Link X',
|
'name': 'Custom Link X',
|
||||||
'content_types': [site_ct.pk],
|
'object_types': [site_type.pk],
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
'weight': 100,
|
'weight': 100,
|
||||||
'button_class': CustomLinkButtonClassChoices.DEFAULT,
|
'button_class': CustomLinkButtonClassChoices.DEFAULT,
|
||||||
@ -158,7 +159,7 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
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 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 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",
|
"Custom Link 6,dcim.site,False,100,blue,Link 6,http://exmaple.com/?6",
|
||||||
@ -183,7 +184,7 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
site_ct = ContentType.objects.get_for_model(Site)
|
site_type = ObjectType.objects.get_for_model(Site)
|
||||||
|
|
||||||
users = (
|
users = (
|
||||||
User(username='User 1'),
|
User(username='User 1'),
|
||||||
@ -217,12 +218,12 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
)
|
)
|
||||||
SavedFilter.objects.bulk_create(saved_filters)
|
SavedFilter.objects.bulk_create(saved_filters)
|
||||||
for i, savedfilter in enumerate(saved_filters):
|
for i, savedfilter in enumerate(saved_filters):
|
||||||
savedfilter.content_types.set([site_ct])
|
savedfilter.object_types.set([site_type])
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Saved Filter X',
|
'name': 'Saved Filter X',
|
||||||
'slug': 'saved-filter-x',
|
'slug': 'saved-filter-x',
|
||||||
'content_types': [site_ct.pk],
|
'object_types': [site_type.pk],
|
||||||
'description': 'Foo',
|
'description': 'Foo',
|
||||||
'weight': 1000,
|
'weight': 1000,
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
@ -231,7 +232,7 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
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 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 5,saved-filter-5,dcim.device,500,True,True,{"foo": "b"}',
|
||||||
'Saved Filter 6,saved-filter-6,dcim.device,600,True,True,{"foo": "c"}',
|
'Saved Filter 6,saved-filter-6,dcim.device,600,True,True,{"foo": "c"}',
|
||||||
@ -302,7 +303,7 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
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 %}"""
|
TEMPLATE_CODE = """{% for object in queryset %}{{ object }}{% endfor %}"""
|
||||||
|
|
||||||
export_templates = (
|
export_templates = (
|
||||||
@ -312,16 +313,16 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
)
|
)
|
||||||
ExportTemplate.objects.bulk_create(export_templates)
|
ExportTemplate.objects.bulk_create(export_templates)
|
||||||
for et in export_templates:
|
for et in export_templates:
|
||||||
et.content_types.set([site_ct])
|
et.object_types.set([site_type])
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Export Template X',
|
'name': 'Export Template X',
|
||||||
'content_types': [site_ct.pk],
|
'object_types': [site_type.pk],
|
||||||
'template_code': TEMPLATE_CODE,
|
'template_code': TEMPLATE_CODE,
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"name,content_types,template_code",
|
"name,object_types,template_code",
|
||||||
f"Export Template 4,dcim.site,{TEMPLATE_CODE}",
|
f"Export Template 4,dcim.site,{TEMPLATE_CODE}",
|
||||||
f"Export Template 5,dcim.site,{TEMPLATE_CODE}",
|
f"Export Template 5,dcim.site,{TEMPLATE_CODE}",
|
||||||
f"Export Template 6,dcim.site,{TEMPLATE_CODE}",
|
f"Export Template 6,dcim.site,{TEMPLATE_CODE}",
|
||||||
@ -396,7 +397,7 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
for webhook in webhooks:
|
for webhook in webhooks:
|
||||||
webhook.save()
|
webhook.save()
|
||||||
|
|
||||||
site_ct = ContentType.objects.get_for_model(Site)
|
site_type = ObjectType.objects.get_for_model(Site)
|
||||||
event_rules = (
|
event_rules = (
|
||||||
EventRule(name='EventRule 1', type_create=True, action_object=webhooks[0]),
|
EventRule(name='EventRule 1', type_create=True, action_object=webhooks[0]),
|
||||||
EventRule(name='EventRule 2', type_create=True, action_object=webhooks[1]),
|
EventRule(name='EventRule 2', type_create=True, action_object=webhooks[1]),
|
||||||
@ -404,12 +405,12 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
)
|
)
|
||||||
for event in event_rules:
|
for event in event_rules:
|
||||||
event.save()
|
event.save()
|
||||||
event.content_types.add(site_ct)
|
event.object_types.add(site_type)
|
||||||
|
|
||||||
webhook_ct = ContentType.objects.get_for_model(Webhook)
|
webhook_ct = ContentType.objects.get_for_model(Webhook)
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Event X',
|
'name': 'Event X',
|
||||||
'content_types': [site_ct.pk],
|
'object_types': [site_type.pk],
|
||||||
'type_create': False,
|
'type_create': False,
|
||||||
'type_update': True,
|
'type_update': True,
|
||||||
'type_delete': True,
|
'type_delete': True,
|
||||||
@ -422,7 +423,7 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
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",
|
"Webhook 4,dcim.site,True,webhook,Webhook 1",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -651,7 +652,7 @@ class CustomLinkTest(TestCase):
|
|||||||
new_window=False
|
new_window=False
|
||||||
)
|
)
|
||||||
customlink.save()
|
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 = Site(name='Test Site', slug='test-site')
|
||||||
site.save()
|
site.save()
|
||||||
|
@ -24,7 +24,7 @@ def image_upload(instance, filename):
|
|||||||
elif instance.name:
|
elif instance.name:
|
||||||
filename = 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):
|
def is_script(obj):
|
||||||
|
@ -17,6 +17,7 @@ from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
|
|||||||
from extras.dashboard.utils import get_widget_class
|
from extras.dashboard.utils import get_widget_class
|
||||||
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
|
from netbox.views.generic.mixins import TableMixin
|
||||||
from utilities.forms import ConfirmationForm, get_field_value
|
from utilities.forms import ConfirmationForm, get_field_value
|
||||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||||
from utilities.rqworker import get_workers_for_queue
|
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 . import filtersets, forms, tables
|
||||||
from .models import *
|
from .models import *
|
||||||
from .scripts import run_script
|
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):
|
def get_extra_context(self, request, instance):
|
||||||
related_models = ()
|
related_models = ()
|
||||||
|
|
||||||
for content_type in instance.content_types.all():
|
for object_type in instance.object_types.all():
|
||||||
related_models += (
|
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}': ''}) |
|
||||||
Q(**{f'custom_field_data__{instance.name}': None})
|
Q(**{f'custom_field_data__{instance.name}': None})
|
||||||
),
|
),
|
||||||
@ -762,8 +764,8 @@ class ImageAttachmentEditView(generic.ObjectEditView):
|
|||||||
def alter_object(self, instance, request, args, kwargs):
|
def alter_object(self, instance, request, args, kwargs):
|
||||||
if not instance.pk:
|
if not instance.pk:
|
||||||
# Assign the parent object based on URL kwargs
|
# Assign the parent object based on URL kwargs
|
||||||
content_type = get_object_or_404(ContentType, pk=request.GET.get('content_type'))
|
object_type = get_object_or_404(ContentType, pk=request.GET.get('object_type'))
|
||||||
instance.parent = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id'))
|
instance.parent = get_object_or_404(object_type.model_class(), pk=request.GET.get('object_id'))
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def get_return_url(self, request, obj=None):
|
def get_return_url(self, request, obj=None):
|
||||||
@ -771,7 +773,7 @@ class ImageAttachmentEditView(generic.ObjectEditView):
|
|||||||
|
|
||||||
def get_extra_addanother_params(self, request):
|
def get_extra_addanother_params(self, request):
|
||||||
return {
|
return {
|
||||||
'content_type': request.GET.get('content_type'),
|
'object_type': request.GET.get('object_type'),
|
||||||
'object_id': request.GET.get('object_id'),
|
'object_id': request.GET.get('object_id'),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1143,19 +1145,72 @@ class LegacyScriptRedirectView(ContentTypePermissionRequiredMixin, View):
|
|||||||
return redirect(f'{url}{path}')
|
return redirect(f'{url}{path}')
|
||||||
|
|
||||||
|
|
||||||
class ScriptResultView(generic.ObjectView):
|
class ScriptResultView(TableMixin, generic.ObjectView):
|
||||||
queryset = Job.objects.all()
|
queryset = Job.objects.all()
|
||||||
|
|
||||||
def get_required_permission(self):
|
def get_required_permission(self):
|
||||||
return 'extras.view_script'
|
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):
|
def get(self, request, **kwargs):
|
||||||
|
table = None
|
||||||
job = get_object_or_404(Job.objects.all(), pk=kwargs.get('job_pk'))
|
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 = {
|
context = {
|
||||||
'script': job.object,
|
'script': job.object,
|
||||||
'job': job,
|
'job': job,
|
||||||
|
'table': table,
|
||||||
}
|
}
|
||||||
|
|
||||||
if job.data and 'log' in job.data:
|
if job.data and 'log' in job.data:
|
||||||
# Script
|
# Script
|
||||||
context['tests'] = job.data.get('tests', {})
|
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.functional import cached_property
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import *
|
from ipam.constants import *
|
||||||
from ipam.fields import IPNetworkField, IPAddressField
|
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:
|
if self._original_assigned_object_id and self._original_assigned_object_type_id:
|
||||||
parent = getattr(self.assigned_object, 'parent_object', None)
|
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_assigned_object = ct.get_object_for_this_type(pk=self._original_assigned_object_id)
|
||||||
original_parent = getattr(original_assigned_object, 'parent_object', None)
|
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 import serializers
|
||||||
from rest_framework.fields import CreateOnlyDefault
|
from rest_framework.fields import CreateOnlyDefault
|
||||||
|
|
||||||
from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues
|
from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues
|
||||||
from extras.models import CustomField
|
|
||||||
from .nested import NestedTagSerializer
|
from .nested import NestedTagSerializer
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from extras.models import ExportTemplate
|
from extras.models import ExportTemplate
|
||||||
from netbox.api.serializers import BulkOperationSerializer
|
from netbox.api.serializers import BulkOperationSerializer
|
||||||
|
|
||||||
@ -26,9 +26,9 @@ class CustomFieldsMixin:
|
|||||||
context = super().get_serializer_context()
|
context = super().get_serializer_context()
|
||||||
|
|
||||||
if hasattr(self.queryset.model, 'custom_fields'):
|
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({
|
context.update({
|
||||||
'custom_fields': content_type.custom_fields.all(),
|
'custom_fields': object_type.custom_fields.all(),
|
||||||
})
|
})
|
||||||
|
|
||||||
return context
|
return context
|
||||||
@ -40,8 +40,8 @@ class ExportTemplatesMixin:
|
|||||||
"""
|
"""
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
if 'export' in request.GET:
|
if 'export' in request.GET:
|
||||||
content_type = ContentType.objects.get_for_model(self.get_serializer_class().Meta.model)
|
object_type = ObjectType.objects.get_for_model(self.get_serializer_class().Meta.model)
|
||||||
et = ExportTemplate.objects.filter(content_types=content_type, name=request.GET['export']).first()
|
et = ExportTemplate.objects.filter(object_types=object_type, name=request.GET['export']).first()
|
||||||
if et is None:
|
if et is None:
|
||||||
raise Http404
|
raise Http404
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
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
|
# Dynamically add a Filter for each CustomField applicable to the parent model
|
||||||
custom_fields = CustomField.objects.filter(
|
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(
|
).exclude(
|
||||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
|
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
|
||||||
)
|
)
|
||||||
|
@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import CustomField, Tag
|
from extras.models import CustomField, Tag
|
||||||
from utilities.forms import CSVModelForm
|
from utilities.forms import CSVModelForm
|
||||||
@ -88,7 +89,7 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
|
|||||||
|
|
||||||
def _get_custom_fields(self, content_type):
|
def _get_custom_fields(self, content_type):
|
||||||
return CustomField.objects.filter(
|
return CustomField.objects.filter(
|
||||||
content_types=content_type,
|
object_types=content_type,
|
||||||
ui_editable=CustomFieldUIEditableChoices.YES
|
ui_editable=CustomFieldUIEditableChoices.YES
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -129,9 +130,9 @@ class NetBoxModelBulkEditForm(CustomFieldsMixin, forms.Form):
|
|||||||
self.fields['pk'].queryset = self.model.objects.all()
|
self.fields['pk'].queryset = self.model.objects.all()
|
||||||
|
|
||||||
# Restrict tag fields by model
|
# Restrict tag fields by model
|
||||||
ct = ContentType.objects.get_for_model(self.model)
|
object_type = ObjectType.objects.get_for_model(self.model)
|
||||||
self.fields['add_tags'].widget.add_query_param('for_object_type_id', ct.pk)
|
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', ct.pk)
|
self.fields['remove_tags'].widget.add_query_param('for_object_type_id', object_type.pk)
|
||||||
|
|
||||||
self._extend_nullable_fields()
|
self._extend_nullable_fields()
|
||||||
|
|
||||||
@ -169,9 +170,9 @@ class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form)
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit saved filters to those applicable to the form's model
|
# 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({
|
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):
|
def _get_custom_fields(self, content_type):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
||||||
@ -32,16 +32,16 @@ class CustomFieldsMixin:
|
|||||||
|
|
||||||
def _get_content_type(self):
|
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):
|
if not getattr(self, 'model', None):
|
||||||
raise NotImplementedError(_("{class_name} must specify a model class.").format(
|
raise NotImplementedError(_("{class_name} must specify a model class.").format(
|
||||||
class_name=self.__class__.__name__
|
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):
|
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
|
ui_editable=CustomFieldUIEditableChoices.HIDDEN
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -85,6 +85,6 @@ class TagsMixin(forms.Form):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit tags to those applicable to the object type
|
# Limit tags to those applicable to the object type
|
||||||
content_type = ContentType.objects.get_for_model(self._meta.model)
|
object_type = ObjectType.objects.get_for_model(self._meta.model)
|
||||||
if content_type and hasattr(self.fields['tags'].widget, 'add_query_param'):
|
if object_type and hasattr(self.fields['tags'].widget, 'add_query_param'):
|
||||||
self.fields['tags'].widget.add_query_param('for_object_type_id', content_type.pk)
|
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 taggit.managers import TaggableManager
|
||||||
|
|
||||||
from core.choices import JobStatusChoices
|
from core.choices import JobStatusChoices
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.utils import is_taggable
|
from extras.utils import is_taggable
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
@ -329,7 +329,9 @@ class ImageAttachmentsMixin(models.Model):
|
|||||||
Enables the assignments of ImageAttachments.
|
Enables the assignments of ImageAttachments.
|
||||||
"""
|
"""
|
||||||
images = GenericRelation(
|
images = GenericRelation(
|
||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment',
|
||||||
|
content_type_field='object_type',
|
||||||
|
object_id_field='object_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -341,7 +343,9 @@ class ContactsMixin(models.Model):
|
|||||||
Enables the assignments of Contacts (via ContactAssignment).
|
Enables the assignments of Contacts (via ContactAssignment).
|
||||||
"""
|
"""
|
||||||
contacts = GenericRelation(
|
contacts = GenericRelation(
|
||||||
to='tenancy.ContactAssignment'
|
to='tenancy.ContactAssignment',
|
||||||
|
content_type_field='object_type',
|
||||||
|
object_id_field='object_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -490,17 +494,17 @@ class SyncedDataMixin(models.Model):
|
|||||||
ret = super().save(*args, **kwargs)
|
ret = super().save(*args, **kwargs)
|
||||||
|
|
||||||
# Create/delete AutoSyncRecord as needed
|
# 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:
|
if self.auto_sync_enabled:
|
||||||
AutoSyncRecord.objects.update_or_create(
|
AutoSyncRecord.objects.update_or_create(
|
||||||
object_type=content_type,
|
object_type=object_type,
|
||||||
object_id=self.pk,
|
object_id=self.pk,
|
||||||
defaults={'datafile': self.data_file}
|
defaults={'datafile': self.data_file}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
AutoSyncRecord.objects.filter(
|
AutoSyncRecord.objects.filter(
|
||||||
datafile=self.data_file,
|
datafile=self.data_file,
|
||||||
object_type=content_type,
|
object_type=object_type,
|
||||||
object_id=self.pk
|
object_id=self.pk
|
||||||
).delete()
|
).delete()
|
||||||
|
|
||||||
@ -510,10 +514,10 @@ class SyncedDataMixin(models.Model):
|
|||||||
from core.models import AutoSyncRecord
|
from core.models import AutoSyncRecord
|
||||||
|
|
||||||
# Delete AutoSyncRecord
|
# Delete AutoSyncRecord
|
||||||
content_type = ContentType.objects.get_for_model(self)
|
object_type = ObjectType.objects.get_for_model(self)
|
||||||
AutoSyncRecord.objects.filter(
|
AutoSyncRecord.objects.filter(
|
||||||
datafile=self.data_file,
|
datafile=self.data_file,
|
||||||
object_type=content_type,
|
object_type=object_type,
|
||||||
object_id=self.pk
|
object_id=self.pk
|
||||||
).delete()
|
).delete()
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ from django.utils.module_loading import import_string
|
|||||||
import netaddr
|
import netaddr
|
||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from extras.models import CachedValue, CustomField
|
from extras.models import CachedValue, CustomField
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from utilities.querysets import RestrictedPrefetch
|
from utilities.querysets import RestrictedPrefetch
|
||||||
@ -130,11 +131,11 @@ class CachedValueSearchBackend(SearchBackend):
|
|||||||
)
|
)
|
||||||
)[:MAX_RESULTS]
|
)[: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
|
# objects). This must be done before generating the final results list, which returns
|
||||||
# a RawQuerySet.
|
# a RawQuerySet.
|
||||||
content_type_ids = set(queryset.values_list('object_type', flat=True))
|
object_type_ids = set(queryset.values_list('object_type', flat=True))
|
||||||
content_types = ContentType.objects.filter(pk__in=content_type_ids)
|
object_types = ObjectType.objects.filter(pk__in=object_type_ids)
|
||||||
|
|
||||||
# Construct a Prefetch to pre-fetch only those related objects for which the
|
# Construct a Prefetch to pre-fetch only those related objects for which the
|
||||||
# user has permission to view.
|
# user has permission to view.
|
||||||
@ -151,11 +152,11 @@ class CachedValueSearchBackend(SearchBackend):
|
|||||||
params
|
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).
|
# related objects necessary to render the prescribed display attributes (display_attrs).
|
||||||
for ct in content_types:
|
for object_type in object_types:
|
||||||
model = ct.model_class()
|
model = object_type.model_class()
|
||||||
indexer = registry['search'].get(content_type_identifier(ct))
|
indexer = registry['search'].get(content_type_identifier(object_type))
|
||||||
if not (display_attrs := getattr(indexer, 'display_attrs', None)):
|
if not (display_attrs := getattr(indexer, 'display_attrs', None)):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -169,7 +170,7 @@ class CachedValueSearchBackend(SearchBackend):
|
|||||||
# Compile a list of all CachedValues referencing this object type, and prefetch
|
# Compile a list of all CachedValues referencing this object type, and prefetch
|
||||||
# any related objects
|
# any related objects
|
||||||
if prefetch_fields:
|
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)
|
prefetch_related_objects(objects, *prefetch_fields)
|
||||||
|
|
||||||
# Omit any results pertaining to an object the user does not have permission to view
|
# Omit any results pertaining to an object the user does not have permission to view
|
||||||
@ -182,7 +183,7 @@ class CachedValueSearchBackend(SearchBackend):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def cache(self, instances, indexer=None, remove_existing=True):
|
def cache(self, instances, indexer=None, remove_existing=True):
|
||||||
content_type = None
|
object_type = None
|
||||||
custom_fields = None
|
custom_fields = None
|
||||||
|
|
||||||
# Convert a single instance to an iterable
|
# Convert a single instance to an iterable
|
||||||
@ -204,8 +205,8 @@ class CachedValueSearchBackend(SearchBackend):
|
|||||||
break
|
break
|
||||||
|
|
||||||
# Prefetch any associated custom fields
|
# Prefetch any associated custom fields
|
||||||
content_type = ContentType.objects.get_for_model(indexer.model)
|
object_type = ObjectType.objects.get_for_model(indexer.model)
|
||||||
custom_fields = CustomField.objects.filter(content_types=content_type).exclude(search_weight=0)
|
custom_fields = CustomField.objects.filter(object_types=object_type).exclude(search_weight=0)
|
||||||
|
|
||||||
# Wipe out any previously cached values for the object
|
# Wipe out any previously cached values for the object
|
||||||
if remove_existing:
|
if remove_existing:
|
||||||
@ -215,7 +216,7 @@ class CachedValueSearchBackend(SearchBackend):
|
|||||||
for field in indexer.to_cache(instance, custom_fields=custom_fields):
|
for field in indexer.to_cache(instance, custom_fields=custom_fields):
|
||||||
buffer.append(
|
buffer.append(
|
||||||
CachedValue(
|
CachedValue(
|
||||||
object_type=content_type,
|
object_type=object_type,
|
||||||
object_id=instance.pk,
|
object_id=instance.pk,
|
||||||
field=field.name,
|
field=field.name,
|
||||||
type=field.type,
|
type=field.type,
|
||||||
|
@ -3,7 +3,6 @@ from copy import deepcopy
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.exceptions import FieldDoesNotExist
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
from django.db.models.fields.related import RelatedField
|
from django.db.models.fields.related import RelatedField
|
||||||
from django.urls import reverse
|
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.utils.translation import gettext_lazy as _
|
||||||
from django_tables2.data import TableQuerysetData
|
from django_tables2.data import TableQuerysetData
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import CustomField, CustomLink
|
from extras.models import CustomField, CustomLink
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
@ -201,14 +201,14 @@ class NetBoxTable(BaseTable):
|
|||||||
])
|
])
|
||||||
|
|
||||||
# Add custom field & custom link columns
|
# 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(
|
custom_fields = CustomField.objects.filter(
|
||||||
content_types=content_type
|
object_types=object_type
|
||||||
).exclude(ui_visible=CustomFieldUIVisibleChoices.HIDDEN)
|
).exclude(ui_visible=CustomFieldUIVisibleChoices.HIDDEN)
|
||||||
extra_columns.extend([
|
extra_columns.extend([
|
||||||
(f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields
|
(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([
|
extra_columns.extend([
|
||||||
(f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in custom_links
|
(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.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from netaddr import IPNetwork
|
from netaddr import IPNetwork
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from ipam.models import Prefix
|
from ipam.models import Prefix
|
||||||
from users.models import Group, ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token
|
||||||
@ -452,7 +452,7 @@ class ObjectPermissionAPIViewTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
obj_perm.save()
|
obj_perm.save()
|
||||||
obj_perm.users.add(self.user)
|
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
|
# Retrieve permitted object
|
||||||
url = reverse('ipam-api:prefix-detail',
|
url = reverse('ipam-api:prefix-detail',
|
||||||
@ -482,7 +482,7 @@ class ObjectPermissionAPIViewTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
obj_perm.save()
|
obj_perm.save()
|
||||||
obj_perm.users.add(self.user)
|
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.
|
# Retrieve all objects. Only permitted objects should be returned.
|
||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(url, **self.header)
|
||||||
@ -510,7 +510,7 @@ class ObjectPermissionAPIViewTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
obj_perm.save()
|
obj_perm.save()
|
||||||
obj_perm.users.add(self.user)
|
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
|
# Attempt to create a non-permitted object
|
||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
@ -541,7 +541,7 @@ class ObjectPermissionAPIViewTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
obj_perm.save()
|
obj_perm.save()
|
||||||
obj_perm.users.add(self.user)
|
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
|
# Attempt to edit a non-permitted object
|
||||||
data = {'site': self.sites[0].pk}
|
data = {'site': self.sites[0].pk}
|
||||||
@ -581,7 +581,7 @@ class ObjectPermissionAPIViewTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
obj_perm.save()
|
obj_perm.save()
|
||||||
obj_perm.users.add(self.user)
|
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
|
# Attempt to delete a non-permitted object
|
||||||
url = reverse('ipam-api:prefix-detail',
|
url = reverse('ipam-api:prefix-detail',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from users.models import ObjectPermission
|
from users.models import ObjectPermission
|
||||||
from utilities.choices import CSVDelimiterChoices, ImportFormatChoices
|
from utilities.choices import CSVDelimiterChoices, ImportFormatChoices
|
||||||
@ -67,7 +67,7 @@ class CSVImportTestCase(ModelViewTestCase):
|
|||||||
obj_perm = ObjectPermission(name='Test permission', actions=['add'])
|
obj_perm = ObjectPermission(name='Test permission', actions=['add'])
|
||||||
obj_perm.save()
|
obj_perm.save()
|
||||||
obj_perm.users.add(self.user)
|
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
|
# Try GET with model-level permission
|
||||||
self.assertHttpStatus(self.client.get(self._get_url('import')), 200)
|
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 = ObjectPermission(name='Test permission', actions=['add'])
|
||||||
obj_perm.save()
|
obj_perm.save()
|
||||||
obj_perm.users.add(self.user)
|
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
|
# Try GET with model-level permission
|
||||||
self.assertHttpStatus(self.client.get(self._get_url('import')), 200)
|
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 django.test import TransactionTestCase
|
||||||
|
|
||||||
from circuits.models import Provider, Circuit, CircuitType
|
from circuits.models import Provider, Circuit, CircuitType
|
||||||
from extras.choices import ChangeActionChoices
|
from extras.choices import ChangeActionChoices
|
||||||
from extras.models import Branch, StagedChange, Tag
|
from extras.models import Branch, StagedChange, Tag
|
||||||
from ipam.models import ASN, RIR
|
from ipam.models import ASN, RIR
|
||||||
|
from netbox.search.backends import search_backend
|
||||||
from netbox.staging import checkout
|
from netbox.staging import checkout
|
||||||
from utilities.testing import create_tags
|
from utilities.testing import create_tags
|
||||||
|
|
||||||
@ -11,6 +13,10 @@ from utilities.testing import create_tags
|
|||||||
class StagingTestCase(TransactionTestCase):
|
class StagingTestCase(TransactionTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
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')
|
create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
rir = RIR.objects.create(name='RIR 1', slug='rir-1')
|
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 import messages
|
||||||
from django.contrib.contenttypes.fields import GenericRel
|
from django.contrib.contenttypes.fields import GenericRel
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError
|
||||||
from django.db import transaction, IntegrityError
|
from django.db import transaction, IntegrityError
|
||||||
from django.db.models import ManyToManyField, ProtectedError, RestrictedError
|
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.utils.translation import gettext as _
|
||||||
from django_tables2.export import TableExport
|
from django_tables2.export import TableExport
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from extras.models import ExportTemplate
|
from extras.models import ExportTemplate
|
||||||
from extras.signals import clear_events
|
from extras.signals import clear_events
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
@ -124,7 +124,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
|||||||
request: The current request
|
request: The current request
|
||||||
"""
|
"""
|
||||||
model = self.queryset.model
|
model = self.queryset.model
|
||||||
content_type = ContentType.objects.get_for_model(model)
|
object_type = ObjectType.objects.get_for_model(model)
|
||||||
|
|
||||||
if self.filterset:
|
if self.filterset:
|
||||||
self.queryset = self.filterset(request.GET, self.queryset, request=request).qs
|
self.queryset = self.filterset(request.GET, self.queryset, request=request).qs
|
||||||
@ -143,7 +143,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
|||||||
|
|
||||||
# Render an ExportTemplate
|
# Render an ExportTemplate
|
||||||
elif request.GET['export']:
|
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)
|
return self.export_template(template, request)
|
||||||
|
|
||||||
# Check for YAML export support on the model
|
# Check for YAML export support on the model
|
||||||
|
@ -17,7 +17,9 @@
|
|||||||
<th scope="row">Type</th>
|
<th scope="row">Type</th>
|
||||||
<td>
|
<td>
|
||||||
{{ object.get_type_display }}
|
{{ 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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@ -89,7 +91,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{% trans "Object Types" %}</h5>
|
<h5 class="card-header">{% trans "Object Types" %}</h5>
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
{% for ct in object.content_types.all %}
|
{% for ct in object.object_types.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ ct }}</td>
|
<td>{{ ct }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{% trans "Assigned Models" %}</h5>
|
<h5 class="card-header">{% trans "Assigned Models" %}</h5>
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
{% for ct in object.content_types.all %}
|
{% for ct in object.object_types.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ ct }}</td>
|
<td>{{ ct }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -26,9 +26,9 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{% trans "Object Types" %}</h5>
|
<h5 class="card-header">{% trans "Object Types" %}</h5>
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
{% for ct in object.content_types.all %}
|
{% for object_type in object.object_types.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ ct }}</td>
|
<td>{{ object_type }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
@ -5,11 +5,6 @@
|
|||||||
|
|
||||||
{% block title %}{{ object.name }}{% endblock %}
|
{% 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 %}
|
{% block content %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-md-5">
|
<div class="col col-md-5">
|
||||||
@ -70,9 +65,9 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{% trans "Assigned Models" %}</h5>
|
<h5 class="card-header">{% trans "Assigned Models" %}</h5>
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
{% for ct in object.content_types.all %}
|
{% for object_type in object.object_types.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ ct }}</td>
|
<td>{{ object_type }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
{% load log_levels %}
|
{% load log_levels %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="htmx-container">
|
||||||
<p>
|
<p>
|
||||||
{% if job.started %}
|
{% if job.started %}
|
||||||
{% trans "Started" %}: <strong>{{ job.started|annotated_date }}</strong>
|
{% trans "Started" %}: <strong>{{ job.started|annotated_date }}</strong>
|
||||||
@ -17,49 +18,7 @@
|
|||||||
<span id="pending-result-label">{% badge job.get_status_display job.get_status_color %}</span>
|
<span id="pending-result-label">{% badge job.get_status_display job.get_status_color %}</span>
|
||||||
</p>
|
</p>
|
||||||
{% if job.completed %}
|
{% 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 %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ forloop.counter }}</td>
|
|
||||||
<td>{{ log.time|placeholder }}</td>
|
|
||||||
<td>{% log_level log.status %}</td>
|
|
||||||
<td>{{ log.message|markdown }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
{% else %}
|
|
||||||
<div class="card-body text-muted">{% trans "None" %}</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 %}
|
{% if tests %}
|
||||||
|
|
||||||
{# Summary of test methods #}
|
{# Summary of test methods #}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{% trans "Test Summary" %}</h5>
|
<h5 class="card-header">{% trans "Test Summary" %}</h5>
|
||||||
@ -77,50 +36,30 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{# Detailed results for individual tests #}
|
{% if table %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{% trans "Test Details" %}</h5>
|
<div class="table-responsive" id="object_list">
|
||||||
<table class="table table-hover report">
|
<h5 class="card-header">{% trans "Log" %}</h5>
|
||||||
<thead>
|
{% include 'htmx/table.html' %}
|
||||||
<tr class="table-headings">
|
</div>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% 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 %}
|
||||||
|
|
||||||
{% elif job.started %}
|
{% elif job.started %}
|
||||||
{% include 'extras/inc/result_pending.html' %}
|
{% include 'extras/inc/result_pending.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
@ -38,9 +38,9 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{% trans "Assigned Models" %}</h5>
|
<h5 class="card-header">{% trans "Assigned Models" %}</h5>
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
{% for ct in object.content_types.all %}
|
{% for object_type in object.object_types.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ ct }}</td>
|
<td>{{ object_type }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
@ -32,28 +32,74 @@
|
|||||||
{% block tabs %}
|
{% block tabs %}
|
||||||
<ul class="nav nav-tabs" role="tablist">
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a href="#log" role="tab" data-bs-toggle="tab" class="nav-link active">{% trans "Log" %}</a>
|
<a href="#results" role="tab" data-bs-toggle="tab" class="nav-link active">{% trans "Results" %}</a>
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">{% trans "Source" %}</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div role="tabpanel" class="tab-pane active" id="log">
|
{# Object list tab #}
|
||||||
<div class="row">
|
<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 %}>
|
<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' %}
|
{% include 'extras/htmx/script_result.html' %}
|
||||||
</div>
|
</div>
|
||||||
|
{# /Objects table #}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div role="tabpanel" class="tab-pane" id="source">
|
{# /Object list tab #}
|
||||||
<p><code>{{ script.filename }}</code></p>
|
|
||||||
<pre class="block">{{ script.source }}</pre>
|
{# 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>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{# /Filters tab #}
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block modals %}
|
{% block modals %}
|
||||||
{% include 'inc/htmx_modal.html' %}
|
{% table_config_form table table_name="ObjectTable" %}
|
||||||
{% endblock modals %}
|
{% endblock modals %}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
{% trans "Images" %}
|
{% trans "Images" %}
|
||||||
{% if perms.extras.add_imageattachment %}
|
{% if perms.extras.add_imageattachment %}
|
||||||
<div class="card-actions">
|
<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" %}
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Attach an image" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{% block extra_controls %}
|
{% block extra_controls %}
|
||||||
{% if perms.tenancy.add_contactassignment %}
|
{% if perms.tenancy.add_contactassignment %}
|
||||||
{% with viewname=object|viewname:"contacts" %}
|
{% 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" %}
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a contact" %}
|
||||||
</a>
|
</a>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -58,7 +58,7 @@ class ContactSerializer(NetBoxModelSerializer):
|
|||||||
|
|
||||||
class ContactAssignmentSerializer(NetBoxModelSerializer):
|
class ContactAssignmentSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail')
|
||||||
content_type = ContentTypeField(
|
object_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.all()
|
queryset=ContentType.objects.all()
|
||||||
)
|
)
|
||||||
object = serializers.SerializerMethodField(read_only=True)
|
object = serializers.SerializerMethodField(read_only=True)
|
||||||
@ -69,13 +69,13 @@ class ContactAssignmentSerializer(NetBoxModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ContactAssignment
|
model = ContactAssignment
|
||||||
fields = [
|
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',
|
'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'contact', 'role', 'priority')
|
brief_fields = ('id', 'url', 'display', 'contact', 'role', 'priority')
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.OBJECT)
|
@extend_schema_field(OpenApiTypes.OBJECT)
|
||||||
def get_object(self, instance):
|
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']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(instance.object, nested=True, context=context).data
|
return serializer(instance.object, nested=True, context=context).data
|
||||||
|
@ -26,12 +26,25 @@ __all__ = (
|
|||||||
class ContactGroupFilterSet(OrganizationalModelFilterSet):
|
class ContactGroupFilterSet(OrganizationalModelFilterSet):
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
label=_('Contact group (ID)'),
|
label=_('Parent contact group (ID)'),
|
||||||
)
|
)
|
||||||
parent = django_filters.ModelMultipleChoiceFilter(
|
parent = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='parent__slug',
|
field_name='parent__slug',
|
||||||
queryset=ContactGroup.objects.all(),
|
queryset=ContactGroup.objects.all(),
|
||||||
to_field_name='slug',
|
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)'),
|
label=_('Contact group (slug)'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,7 +99,7 @@ class ContactAssignmentFilterSet(NetBoxModelFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label=_('Search'),
|
label=_('Search'),
|
||||||
)
|
)
|
||||||
content_type = ContentTypeFilter()
|
object_type = ContentTypeFilter()
|
||||||
contact_id = django_filters.ModelMultipleChoiceFilter(
|
contact_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Contact.objects.all(),
|
queryset=Contact.objects.all(),
|
||||||
label=_('Contact (ID)'),
|
label=_('Contact (ID)'),
|
||||||
@ -118,7 +131,7 @@ class ContactAssignmentFilterSet(NetBoxModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContactAssignment
|
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):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -155,12 +168,25 @@ class ContactModelFilterSet(django_filters.FilterSet):
|
|||||||
class TenantGroupFilterSet(OrganizationalModelFilterSet):
|
class TenantGroupFilterSet(OrganizationalModelFilterSet):
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
label=_('Tenant group (ID)'),
|
label=_('Parent tenant group (ID)'),
|
||||||
)
|
)
|
||||||
parent = django_filters.ModelMultipleChoiceFilter(
|
parent = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='parent__slug',
|
field_name='parent__slug',
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
to_field_name='slug',
|
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)'),
|
label=_('Tenant group (slug)'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ class ContactImportForm(NetBoxModelImportForm):
|
|||||||
|
|
||||||
|
|
||||||
class ContactAssignmentImportForm(NetBoxModelImportForm):
|
class ContactAssignmentImportForm(NetBoxModelImportForm):
|
||||||
content_type = CSVContentTypeField(
|
object_type = CSVContentTypeField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
help_text=_("One or more assigned object types")
|
help_text=_("One or more assigned object types")
|
||||||
)
|
)
|
||||||
@ -108,4 +108,4 @@ class ContactAssignmentImportForm(NetBoxModelImportForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContactAssignment
|
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 import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from netbox.forms import NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
from tenancy.choices import *
|
from tenancy.choices import *
|
||||||
from tenancy.models import *
|
from tenancy.models import *
|
||||||
@ -83,10 +83,10 @@ class ContactAssignmentFilterForm(NetBoxModelFilterSetForm):
|
|||||||
model = ContactAssignment
|
model = ContactAssignment
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id', 'tag')),
|
(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(
|
object_type_id = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.with_feature('contacts'),
|
queryset=ObjectType.objects.with_feature('contacts'),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Object type')
|
label=_('Object type')
|
||||||
)
|
)
|
||||||
|
@ -143,9 +143,9 @@ class ContactAssignmentForm(NetBoxModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ContactAssignment
|
model = ContactAssignment
|
||||||
fields = (
|
fields = (
|
||||||
'content_type', 'object_id', 'group', 'contact', 'role', 'priority', 'tags'
|
'object_type', 'object_id', 'group', 'contact', 'role', 'priority', 'tags'
|
||||||
)
|
)
|
||||||
widgets = {
|
widgets = {
|
||||||
'content_type': forms.HiddenInput(),
|
'object_type': forms.HiddenInput(),
|
||||||
'object_id': 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.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
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 import ChangeLoggedModel, NestedGroupModel, OrganizationalModel, PrimaryModel
|
||||||
from netbox.models.features import CustomFieldsMixin, ExportTemplatesMixin, TagsMixin
|
from netbox.models.features import CustomFieldsMixin, ExportTemplatesMixin, TagsMixin
|
||||||
from tenancy.choices import *
|
from tenancy.choices import *
|
||||||
@ -111,13 +111,13 @@ class Contact(PrimaryModel):
|
|||||||
|
|
||||||
|
|
||||||
class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
||||||
content_type = models.ForeignKey(
|
object_type = models.ForeignKey(
|
||||||
to='contenttypes.ContentType',
|
to='contenttypes.ContentType',
|
||||||
on_delete=models.CASCADE
|
on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
object_id = models.PositiveBigIntegerField()
|
object_id = models.PositiveBigIntegerField()
|
||||||
object = GenericForeignKey(
|
object = GenericForeignKey(
|
||||||
ct_field='content_type',
|
ct_field='object_type',
|
||||||
fk_field='object_id'
|
fk_field='object_id'
|
||||||
)
|
)
|
||||||
contact = models.ForeignKey(
|
contact = models.ForeignKey(
|
||||||
@ -137,16 +137,16 @@ class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, Chan
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = ('content_type', 'object_id', 'role', 'priority')
|
clone_fields = ('object_type', 'object_id', 'role', 'priority')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('contact', 'priority', 'role', 'pk')
|
ordering = ('contact', 'priority', 'role', 'pk')
|
||||||
indexes = (
|
indexes = (
|
||||||
models.Index(fields=('content_type', 'object_id')),
|
models.Index(fields=('object_type', 'object_id')),
|
||||||
)
|
)
|
||||||
constraints = (
|
constraints = (
|
||||||
models.UniqueConstraint(
|
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'
|
name='%(app_label)s_%(class)s_unique_object_contact_role'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -165,9 +165,9 @@ class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, Chan
|
|||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Validate the assigned object type
|
# 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(
|
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):
|
def to_objectchange(self, action):
|
||||||
|
@ -86,7 +86,7 @@ class ContactTable(NetBoxTable):
|
|||||||
|
|
||||||
|
|
||||||
class ContactAssignmentTable(NetBoxTable):
|
class ContactAssignmentTable(NetBoxTable):
|
||||||
content_type = columns.ContentTypeColumn(
|
object_type = columns.ContentTypeColumn(
|
||||||
verbose_name=_('Object Type')
|
verbose_name=_('Object Type')
|
||||||
)
|
)
|
||||||
object = tables.Column(
|
object = tables.Column(
|
||||||
@ -141,10 +141,10 @@ class ContactAssignmentTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = ContactAssignment
|
model = ContactAssignment
|
||||||
fields = (
|
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',
|
'contact_email', 'contact_address', 'contact_link', 'contact_description', 'contact_group', 'tags',
|
||||||
'actions'
|
'actions'
|
||||||
)
|
)
|
||||||
default_columns = (
|
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 = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
'content_type': 'dcim.site',
|
'object_type': 'dcim.site',
|
||||||
'object_id': sites[1].pk,
|
'object_id': sites[1].pk,
|
||||||
'contact': contacts[3].pk,
|
'contact': contacts[3].pk,
|
||||||
'role': contact_roles[0].pk,
|
'role': contact_roles[0].pk,
|
||||||
'priority': ContactPriorityChoices.PRIORITY_PRIMARY,
|
'priority': ContactPriorityChoices.PRIORITY_PRIMARY,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_type': 'dcim.site',
|
'object_type': 'dcim.site',
|
||||||
'object_id': sites[1].pk,
|
'object_id': sites[1].pk,
|
||||||
'contact': contacts[4].pk,
|
'contact': contacts[4].pk,
|
||||||
'role': contact_roles[1].pk,
|
'role': contact_roles[1].pk,
|
||||||
'priority': ContactPriorityChoices.PRIORITY_SECONDARY,
|
'priority': ContactPriorityChoices.PRIORITY_SECONDARY,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_type': 'dcim.site',
|
'object_type': 'dcim.site',
|
||||||
'object_id': sites[1].pk,
|
'object_id': sites[1].pk,
|
||||||
'contact': contacts[5].pk,
|
'contact': contacts[5].pk,
|
||||||
'role': contact_roles[2].pk,
|
'role': contact_roles[2].pk,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from dcim.models import Manufacturer, Site
|
from dcim.models import Manufacturer, Site
|
||||||
from tenancy.filtersets import *
|
from tenancy.filtersets import *
|
||||||
from tenancy.models import *
|
from tenancy.models import *
|
||||||
@ -15,35 +15,43 @@ class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
parent_tenant_groups = (
|
parent_tenant_groups = (
|
||||||
TenantGroup(name='Parent Tenant Group 1', slug='parent-tenant-group-1'),
|
TenantGroup(name='Tenant Group 1', slug='tenant-group-1'),
|
||||||
TenantGroup(name='Parent Tenant Group 2', slug='parent-tenant-group-2'),
|
TenantGroup(name='Tenant Group 2', slug='tenant-group-2'),
|
||||||
TenantGroup(name='Parent Tenant Group 3', slug='parent-tenant-group-3'),
|
TenantGroup(name='Tenant Group 3', slug='tenant-group-3'),
|
||||||
)
|
)
|
||||||
for tenantgroup in parent_tenant_groups:
|
for tenant_group in parent_tenant_groups:
|
||||||
tenantgroup.save()
|
tenant_group.save()
|
||||||
|
|
||||||
tenant_groups = (
|
tenant_groups = (
|
||||||
TenantGroup(
|
TenantGroup(
|
||||||
name='Tenant Group 1',
|
name='Tenant Group 1A',
|
||||||
slug='tenant-group-1',
|
slug='tenant-group-1a',
|
||||||
parent=parent_tenant_groups[0],
|
parent=parent_tenant_groups[0],
|
||||||
description='foobar1'
|
description='foobar1'
|
||||||
),
|
),
|
||||||
TenantGroup(
|
TenantGroup(
|
||||||
name='Tenant Group 2',
|
name='Tenant Group 2A',
|
||||||
slug='tenant-group-2',
|
slug='tenant-group-2a',
|
||||||
parent=parent_tenant_groups[1],
|
parent=parent_tenant_groups[1],
|
||||||
description='foobar2'
|
description='foobar2'
|
||||||
),
|
),
|
||||||
TenantGroup(
|
TenantGroup(
|
||||||
name='Tenant Group 3',
|
name='Tenant Group 3A',
|
||||||
slug='tenant-group-3',
|
slug='tenant-group-3a',
|
||||||
parent=parent_tenant_groups[2],
|
parent=parent_tenant_groups[2],
|
||||||
description='foobar3'
|
description='foobar3'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for tenantgroup in tenant_groups:
|
for tenant_group in tenant_groups:
|
||||||
tenantgroup.save()
|
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):
|
def test_q(self):
|
||||||
params = {'q': 'foobar1'}
|
params = {'q': 'foobar1'}
|
||||||
@ -62,12 +70,19 @@ class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_parent(self):
|
def test_parent(self):
|
||||||
parent_groups = TenantGroup.objects.filter(name__startswith='Parent')[:2]
|
tenant_groups = TenantGroup.objects.filter(parent__isnull=True)[:2]
|
||||||
params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]}
|
params = {'parent_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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)
|
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):
|
class TenantTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = Tenant.objects.all()
|
queryset = Tenant.objects.all()
|
||||||
@ -123,35 +138,43 @@ class ContactGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
parent_contact_groups = (
|
parent_contact_groups = (
|
||||||
ContactGroup(name='Parent Contact Group 1', slug='parent-contact-group-1'),
|
ContactGroup(name='Contact Group 1', slug='contact-group-1'),
|
||||||
ContactGroup(name='Parent Contact Group 2', slug='parent-contact-group-2'),
|
ContactGroup(name='Contact Group 2', slug='contact-group-2'),
|
||||||
ContactGroup(name='Parent Contact Group 3', slug='parent-contact-group-3'),
|
ContactGroup(name='Contact Group 3', slug='contact-group-3'),
|
||||||
)
|
)
|
||||||
for contactgroup in parent_contact_groups:
|
for contact_group in parent_contact_groups:
|
||||||
contactgroup.save()
|
contact_group.save()
|
||||||
|
|
||||||
contact_groups = (
|
contact_groups = (
|
||||||
ContactGroup(
|
ContactGroup(
|
||||||
name='Contact Group 1',
|
name='Contact Group 1A',
|
||||||
slug='contact-group-1',
|
slug='contact-group-1a',
|
||||||
parent=parent_contact_groups[0],
|
parent=parent_contact_groups[0],
|
||||||
description='foobar1'
|
description='foobar1'
|
||||||
),
|
),
|
||||||
ContactGroup(
|
ContactGroup(
|
||||||
name='Contact Group 2',
|
name='Contact Group 2A',
|
||||||
slug='contact-group-2',
|
slug='contact-group-2a',
|
||||||
parent=parent_contact_groups[1],
|
parent=parent_contact_groups[1],
|
||||||
description='foobar2'
|
description='foobar2'
|
||||||
),
|
),
|
||||||
ContactGroup(
|
ContactGroup(
|
||||||
name='Contact Group 3',
|
name='Contact Group 3A',
|
||||||
slug='contact-group-3',
|
slug='contact-group-3a',
|
||||||
parent=parent_contact_groups[2],
|
parent=parent_contact_groups[2],
|
||||||
description='foobar3'
|
description='foobar3'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for contactgroup in contact_groups:
|
for contact_group in contact_groups:
|
||||||
contactgroup.save()
|
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):
|
def test_q(self):
|
||||||
params = {'q': 'foobar1'}
|
params = {'q': 'foobar1'}
|
||||||
@ -170,12 +193,19 @@ class ContactGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_parent(self):
|
def test_parent(self):
|
||||||
parent_groups = ContactGroup.objects.filter(parent__isnull=True)[:2]
|
contact_groups = ContactGroup.objects.filter(parent__isnull=True)[:2]
|
||||||
params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]}
|
params = {'parent_id': [contact_groups[0].pk, contact_groups[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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)
|
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):
|
class ContactRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = ContactRole.objects.all()
|
queryset = ContactRole.objects.all()
|
||||||
@ -295,8 +325,8 @@ class ContactAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
ContactAssignment.objects.bulk_create(assignments)
|
ContactAssignment.objects.bulk_create(assignments)
|
||||||
|
|
||||||
def test_content_type(self):
|
def test_object_type(self):
|
||||||
params = {'content_type_id': ContentType.objects.get_by_natural_key('dcim', 'site')}
|
params = {'object_type_id': ObjectType.objects.get_by_natural_key('dcim', 'site')}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
|
||||||
def test_contact(self):
|
def test_contact(self):
|
||||||
|
@ -292,7 +292,7 @@ class ContactAssignmentTestCase(
|
|||||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
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,
|
'object_id': sites[3].pk,
|
||||||
'contact': contacts[3].pk,
|
'contact': contacts[3].pk,
|
||||||
'role': contact_roles[3].pk,
|
'role': contact_roles[3].pk,
|
||||||
@ -306,11 +306,11 @@ class ContactAssignmentTestCase(
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _get_url(self, action, instance=None):
|
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':
|
if action == 'add':
|
||||||
url = reverse('tenancy:contactassignment_add')
|
url = reverse('tenancy:contactassignment_add')
|
||||||
content_type = ContentType.objects.get_for_model(Site).pk
|
content_type = ContentType.objects.get_for_model(Site).pk
|
||||||
object_id = Site.objects.first().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)
|
return super()._get_url(action, instance=instance)
|
||||||
|
@ -23,7 +23,7 @@ class ObjectContactsView(generic.ObjectChildrenView):
|
|||||||
|
|
||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
return ContactAssignment.objects.restrict(request.user, 'view').filter(
|
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
|
object_id=parent.pk
|
||||||
).order_by('priority', 'contact', 'role')
|
).order_by('priority', 'contact', 'role')
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ class ObjectContactsView(generic.ObjectChildrenView):
|
|||||||
table = super().get_table(*args, **kwargs)
|
table = super().get_table(*args, **kwargs)
|
||||||
|
|
||||||
# Hide object columns
|
# Hide object columns
|
||||||
table.columns.hide('content_type')
|
table.columns.hide('object_type')
|
||||||
table.columns.hide('object')
|
table.columns.hide('object')
|
||||||
|
|
||||||
return table
|
return table
|
||||||
@ -374,8 +374,8 @@ class ContactAssignmentEditView(generic.ObjectEditView):
|
|||||||
def alter_object(self, instance, request, args, kwargs):
|
def alter_object(self, instance, request, args, kwargs):
|
||||||
if not instance.pk:
|
if not instance.pk:
|
||||||
# Assign the object based on URL kwargs
|
# Assign the object based on URL kwargs
|
||||||
content_type = get_object_or_404(ContentType, pk=request.GET.get('content_type'))
|
object_type = get_object_or_404(ContentType, pk=request.GET.get('object_type'))
|
||||||
instance.object = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id'))
|
instance.object = get_object_or_404(object_type.model_class(), pk=request.GET.get('object_id'))
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def get_extra_addanother_params(self, request):
|
def get_extra_addanother_params(self, request):
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
from django.contrib.auth import get_user_model
|
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.utils import extend_schema_field
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from netbox.api.fields import ContentTypeField
|
from netbox.api.fields import ContentTypeField
|
||||||
from netbox.api.serializers import WritableNestedSerializer
|
from netbox.api.serializers import WritableNestedSerializer
|
||||||
from users.models import Group, ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token
|
||||||
@ -49,7 +49,7 @@ class NestedTokenSerializer(WritableNestedSerializer):
|
|||||||
class NestedObjectPermissionSerializer(WritableNestedSerializer):
|
class NestedObjectPermissionSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail')
|
||||||
object_types = ContentTypeField(
|
object_types = ContentTypeField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ObjectType.objects.all(),
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
groups = serializers.SerializerMethodField(read_only=True)
|
groups = serializers.SerializerMethodField(read_only=True)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from netbox.api.fields import ContentTypeField, SerializedPKRelatedField
|
from netbox.api.fields import ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import ValidatedModelSerializer
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
from users.models import Group, ObjectPermission
|
from users.models import Group, ObjectPermission
|
||||||
@ -15,7 +15,7 @@ __all__ = (
|
|||||||
class ObjectPermissionSerializer(ValidatedModelSerializer):
|
class ObjectPermissionSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail')
|
||||||
object_types = ContentTypeField(
|
object_types = ContentTypeField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ObjectType.objects.all(),
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
groups = SerializedPKRelatedField(
|
groups = SerializedPKRelatedField(
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.contrib.postgres.forms import SimpleArrayField
|
from django.contrib.postgres.forms import SimpleArrayField
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import FieldError
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from ipam.formfields import IPNetworkFormField
|
from ipam.formfields import IPNetworkFormField
|
||||||
from ipam.validators import prefix_validator
|
from ipam.validators import prefix_validator
|
||||||
from netbox.preferences import PREFERENCES
|
from netbox.preferences import PREFERENCES
|
||||||
@ -278,7 +278,7 @@ class GroupForm(forms.ModelForm):
|
|||||||
class ObjectPermissionForm(forms.ModelForm):
|
class ObjectPermissionForm(forms.ModelForm):
|
||||||
object_types = ContentTypeMultipleChoiceField(
|
object_types = ContentTypeMultipleChoiceField(
|
||||||
label=_('Object types'),
|
label=_('Object types'),
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ObjectType.objects.all(),
|
||||||
limit_choices_to=OBJECTPERMISSION_OBJECT_TYPES,
|
limit_choices_to=OBJECTPERMISSION_OBJECT_TYPES,
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6})
|
widget=forms.SelectMultiple(attrs={'size': 6})
|
||||||
)
|
)
|
||||||
|
@ -14,7 +14,7 @@ def update_content_types(apps, schema_editor):
|
|||||||
if netboxuser_ct:
|
if netboxuser_ct:
|
||||||
user_ct = ContentType.objects.filter(app_label='users', model='user').first()
|
user_ct = ContentType.objects.filter(app_label='users', model='user').first()
|
||||||
CustomField = apps.get_model('extras', 'CustomField')
|
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()
|
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():
|
if old_ct := ContentType.objects.filter(app_label='users', model='netboxgroup').first():
|
||||||
new_ct = ContentType.objects.get_for_model(Group)
|
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):
|
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 django.utils.translation import gettext_lazy as _
|
||||||
from netaddr import IPNetwork
|
from netaddr import IPNetwork
|
||||||
|
|
||||||
from core.models import ContentType
|
from core.models import ObjectType
|
||||||
from ipam.fields import IPNetworkField
|
from ipam.fields import IPNetworkField
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
@ -383,7 +383,7 @@ class ObjectPermission(models.Model):
|
|||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
object_types = models.ManyToManyField(
|
object_types = models.ManyToManyField(
|
||||||
to='contenttypes.ContentType',
|
to='core.ObjectType',
|
||||||
limit_choices_to=OBJECTPERMISSION_OBJECT_TYPES,
|
limit_choices_to=OBJECTPERMISSION_OBJECT_TYPES,
|
||||||
related_name='object_permissions'
|
related_name='object_permissions'
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from users.models import Group, ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token
|
||||||
from utilities.testing import APIViewTestCases, APITestCase, create_test_user
|
from utilities.testing import APIViewTestCases, APITestCase, create_test_user
|
||||||
from utilities.utils import deepmerge
|
from utilities.utils import deepmerge
|
||||||
@ -64,7 +64,7 @@ class UserTest(APIViewTestCases.APIViewTestCase):
|
|||||||
)
|
)
|
||||||
obj_perm.save()
|
obj_perm.save()
|
||||||
obj_perm.users.add(self.user)
|
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 = {
|
user_credentials = {
|
||||||
'username': 'user1',
|
'username': 'user1',
|
||||||
@ -261,7 +261,7 @@ class ObjectPermissionTest(
|
|||||||
)
|
)
|
||||||
User.objects.bulk_create(users)
|
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):
|
for i in range(3):
|
||||||
objectpermission = ObjectPermission(
|
objectpermission = ObjectPermission(
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
|
|
||||||
|
from core.models import ObjectType
|
||||||
from users import filtersets
|
from users import filtersets
|
||||||
from users.models import Group, ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token
|
||||||
from utilities.testing import BaseFilterSetTests
|
from utilities.testing import BaseFilterSetTests
|
||||||
@ -151,9 +151,9 @@ class ObjectPermissionTestCase(TestCase, BaseFilterSetTests):
|
|||||||
User.objects.bulk_create(users)
|
User.objects.bulk_create(users)
|
||||||
|
|
||||||
object_types = (
|
object_types = (
|
||||||
ContentType.objects.get(app_label='dcim', model='site'),
|
ObjectType.objects.get(app_label='dcim', model='site'),
|
||||||
ContentType.objects.get(app_label='dcim', model='rack'),
|
ObjectType.objects.get(app_label='dcim', model='rack'),
|
||||||
ContentType.objects.get(app_label='dcim', model='device'),
|
ObjectType.objects.get(app_label='dcim', model='device'),
|
||||||
)
|
)
|
||||||
|
|
||||||
permissions = (
|
permissions = (
|
||||||
@ -198,7 +198,7 @@ class ObjectPermissionTestCase(TestCase, BaseFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_object_types(self):
|
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]}
|
params = {'object_types': [object_types[0].pk, object_types[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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 users.models import *
|
||||||
from utilities.testing import ViewTestCases, create_test_user
|
from utilities.testing import ViewTestCases, create_test_user
|
||||||
|
|
||||||
@ -115,7 +114,7 @@ class ObjectPermissionTestCase(
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
ct = ContentType.objects.get_by_natural_key('dcim', 'site')
|
object_type = ObjectType.objects.get_by_natural_key('dcim', 'site')
|
||||||
|
|
||||||
permissions = (
|
permissions = (
|
||||||
ObjectPermission(name='Permission 1', actions=['view', 'add', 'delete']),
|
ObjectPermission(name='Permission 1', actions=['view', 'add', 'delete']),
|
||||||
@ -127,7 +126,7 @@ class ObjectPermissionTestCase(
|
|||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Permission X',
|
'name': 'Permission X',
|
||||||
'description': 'A new permission',
|
'description': 'A new permission',
|
||||||
'object_types': [ct.pk],
|
'object_types': [object_type.pk],
|
||||||
'actions': 'view,edit,delete',
|
'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
Block a user