mirror of
https://github.com/netbox-community/netbox.git
synced 2026-03-21 17:10:10 +01:00
Merge branch 'feature' into 8366-job-scheduling
Sync with upstream
This commit is contained in:
@@ -99,6 +99,8 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
||||
types = CustomFieldTypeChoices
|
||||
if obj.type == types.TYPE_INTEGER:
|
||||
return 'integer'
|
||||
if obj.type == types.TYPE_DECIMAL:
|
||||
return 'decimal'
|
||||
if obj.type == types.TYPE_BOOLEAN:
|
||||
return 'boolean'
|
||||
if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT):
|
||||
|
||||
@@ -10,6 +10,7 @@ class CustomFieldTypeChoices(ChoiceSet):
|
||||
TYPE_TEXT = 'text'
|
||||
TYPE_LONGTEXT = 'longtext'
|
||||
TYPE_INTEGER = 'integer'
|
||||
TYPE_DECIMAL = 'decimal'
|
||||
TYPE_BOOLEAN = 'boolean'
|
||||
TYPE_DATE = 'date'
|
||||
TYPE_URL = 'url'
|
||||
@@ -23,6 +24,7 @@ class CustomFieldTypeChoices(ChoiceSet):
|
||||
(TYPE_TEXT, 'Text'),
|
||||
(TYPE_LONGTEXT, 'Text (long)'),
|
||||
(TYPE_INTEGER, 'Integer'),
|
||||
(TYPE_DECIMAL, 'Decimal'),
|
||||
(TYPE_BOOLEAN, 'Boolean (true/false)'),
|
||||
(TYPE_DATE, 'Date'),
|
||||
(TYPE_URL, 'URL'),
|
||||
|
||||
@@ -34,7 +34,9 @@ class CustomFieldsMixin:
|
||||
return ContentType.objects.get_for_model(self.model)
|
||||
|
||||
def _get_custom_fields(self, content_type):
|
||||
return CustomField.objects.filter(content_types=content_type)
|
||||
return CustomField.objects.filter(content_types=content_type).exclude(
|
||||
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
|
||||
)
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
return customfield.to_form_field()
|
||||
@@ -50,13 +52,6 @@ class CustomFieldsMixin:
|
||||
field_name = f'cf_{customfield.name}'
|
||||
self.fields[field_name] = self._get_form_field(customfield)
|
||||
|
||||
if customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
|
||||
self.fields[field_name].disabled = True
|
||||
if self.fields[field_name].help_text:
|
||||
self.fields[field_name].help_text += '<br />'
|
||||
self.fields[field_name].help_text += '<i class="mdi mdi-alert-circle-outline"></i> ' \
|
||||
'Field is set to read-only.'
|
||||
|
||||
# Annotate the field in the list of CustomField form fields
|
||||
self.custom_fields[field_name] = customfield
|
||||
if customfield.group_name not in self.custom_field_groups:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import django.core.serializers.json
|
||||
from utilities.json import CustomFieldJSONEncoder
|
||||
from django.db import migrations, models
|
||||
import taggit.managers
|
||||
|
||||
@@ -13,7 +13,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='journalentry',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
|
||||
field=models.JSONField(blank=True, default=dict, encoder=CustomFieldJSONEncoder),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='journalentry',
|
||||
|
||||
27
netbox/extras/migrations/0078_unique_constraints.py
Normal file
27
netbox/extras/migrations/0078_unique_constraints.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0077_customlink_extend_text_and_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='exporttemplate',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='webhook',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='exporttemplate',
|
||||
constraint=models.UniqueConstraint(fields=('content_type', 'name'), name='extras_exporttemplate_unique_content_type_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='webhook',
|
||||
constraint=models.UniqueConstraint(fields=('payload_url', 'type_create', 'type_update', 'type_delete'), name='extras_webhook_unique_payload_url_types'),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,6 @@
|
||||
import re
|
||||
from datetime import datetime, date
|
||||
import decimal
|
||||
|
||||
import django_filters
|
||||
from django import forms
|
||||
@@ -219,14 +220,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
||||
})
|
||||
|
||||
# Minimum/maximum values can be set only for numeric fields
|
||||
if self.validation_minimum is not None and self.type != CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
raise ValidationError({
|
||||
'validation_minimum': "A minimum value may be set only for numeric fields"
|
||||
})
|
||||
if self.validation_maximum is not None and self.type != CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
raise ValidationError({
|
||||
'validation_maximum': "A maximum value may be set only for numeric fields"
|
||||
})
|
||||
if self.type not in (CustomFieldTypeChoices.TYPE_INTEGER, CustomFieldTypeChoices.TYPE_DECIMAL):
|
||||
if self.validation_minimum:
|
||||
raise ValidationError({'validation_minimum': "A minimum value may be set only for numeric fields"})
|
||||
if self.validation_maximum:
|
||||
raise ValidationError({'validation_maximum': "A maximum value may be set only for numeric fields"})
|
||||
|
||||
# Regex validation can be set only for text fields
|
||||
regex_types = (
|
||||
@@ -297,12 +295,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
||||
return model.objects.filter(pk__in=value)
|
||||
return value
|
||||
|
||||
def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False):
|
||||
def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibility=True, for_csv_import=False):
|
||||
"""
|
||||
Return a form field suitable for setting a CustomField's value for an object.
|
||||
|
||||
set_initial: Set initial data for the field. This should be False when generating a field for bulk editing.
|
||||
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
|
||||
enforce_visibility: Honor the value of CustomField.ui_visibility. Set to False for filtering.
|
||||
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
|
||||
"""
|
||||
initial = self.default if set_initial else None
|
||||
@@ -317,6 +316,17 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
||||
max_value=self.validation_maximum
|
||||
)
|
||||
|
||||
# Decimal
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL:
|
||||
field = forms.DecimalField(
|
||||
required=required,
|
||||
initial=initial,
|
||||
max_digits=12,
|
||||
decimal_places=4,
|
||||
min_value=self.validation_minimum,
|
||||
max_value=self.validation_maximum
|
||||
)
|
||||
|
||||
# Boolean
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
choices = (
|
||||
@@ -398,6 +408,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
||||
if self.description:
|
||||
field.help_text = escape(self.description)
|
||||
|
||||
# Annotate read-only fields
|
||||
if enforce_visibility and self.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
|
||||
field.disabled = True
|
||||
prepend = '<br />' if field.help_text else ''
|
||||
field.help_text += f'{prepend}<i class="mdi mdi-alert-circle-outline"></i> Field is set to read-only.'
|
||||
|
||||
return field
|
||||
|
||||
def to_filter(self, lookup_expr=None):
|
||||
@@ -426,6 +442,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
filter_class = filters.MultiValueNumberFilter
|
||||
|
||||
# Decimal
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL:
|
||||
filter_class = filters.MultiValueDecimalFilter
|
||||
|
||||
# Boolean
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
filter_class = django_filters.BooleanFilter
|
||||
@@ -475,7 +495,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
||||
raise ValidationError(f"Value must match regex '{self.validation_regex}'")
|
||||
|
||||
# Validate integer
|
||||
if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
if type(value) is not int:
|
||||
raise ValidationError("Value must be an integer.")
|
||||
if self.validation_minimum is not None and value < self.validation_minimum:
|
||||
@@ -483,12 +503,23 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
||||
if self.validation_maximum is not None and value > self.validation_maximum:
|
||||
raise ValidationError(f"Value must not exceed {self.validation_maximum}")
|
||||
|
||||
# Validate decimal
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL:
|
||||
try:
|
||||
decimal.Decimal(value)
|
||||
except decimal.InvalidOperation:
|
||||
raise ValidationError("Value must be a decimal.")
|
||||
if self.validation_minimum is not None and value < self.validation_minimum:
|
||||
raise ValidationError(f"Value must be at least {self.validation_minimum}")
|
||||
if self.validation_maximum is not None and value > self.validation_maximum:
|
||||
raise ValidationError(f"Value must not exceed {self.validation_maximum}")
|
||||
|
||||
# Validate boolean
|
||||
if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||
raise ValidationError("Value must be true or false.")
|
||||
|
||||
# Validate date
|
||||
if self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||
if type(value) is not date:
|
||||
try:
|
||||
datetime.strptime(value, '%Y-%m-%d')
|
||||
@@ -496,14 +527,14 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
||||
raise ValidationError("Date values must be in the format YYYY-MM-DD.")
|
||||
|
||||
# Validate selected choice
|
||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
if value not in self.choices:
|
||||
raise ValidationError(
|
||||
f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}"
|
||||
)
|
||||
|
||||
# Validate all selected choices
|
||||
if self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
||||
if not set(value).issubset(self.choices):
|
||||
raise ValidationError(
|
||||
f"Invalid choice(s) ({', '.join(value)}). Available choices are: {', '.join(self.choices)}"
|
||||
|
||||
@@ -131,7 +131,12 @@ class Webhook(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
unique_together = ('payload_url', 'type_create', 'type_update', 'type_delete',)
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('payload_url', 'type_create', 'type_update', 'type_delete'),
|
||||
name='%(app_label)s_%(class)s_unique_payload_url_types'
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -297,9 +302,12 @@ class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['content_type', 'name']
|
||||
unique_together = [
|
||||
['content_type', 'name']
|
||||
]
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('content_type', 'name'),
|
||||
name='%(app_label)s_%(class)s_unique_content_type_name'
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.content_type}: {self.name}"
|
||||
@@ -463,6 +471,14 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, WebhooksMixin
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:journalentry', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Prevent the creation of journal entries on unsupported models
|
||||
permitted_types = ContentType.objects.filter(FeatureQuery('journaling').get_query())
|
||||
if self.assigned_object_type not in permitted_types:
|
||||
raise ValidationError(f"Journaling is not supported for this object type ({self.assigned_object_type}).")
|
||||
|
||||
def get_kind_color(self):
|
||||
return JournalEntryKindChoices.colors.get(self.kind)
|
||||
|
||||
|
||||
@@ -6,15 +6,16 @@ from django.apps import AppConfig
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.template.loader import get_template
|
||||
|
||||
from extras.registry import registry
|
||||
from utilities.choices import ButtonColorChoices
|
||||
|
||||
from extras.plugins.utils import import_object
|
||||
from extras.registry import registry
|
||||
from netbox.navigation import MenuGroup
|
||||
from utilities.choices import ButtonColorChoices
|
||||
|
||||
|
||||
# Initialize plugin registry
|
||||
registry['plugins'] = {
|
||||
'graphql_schemas': [],
|
||||
'menus': [],
|
||||
'menu_items': {},
|
||||
'preferences': {},
|
||||
'template_extensions': collections.defaultdict(list),
|
||||
@@ -54,9 +55,13 @@ class PluginConfig(AppConfig):
|
||||
# Django-rq queues dedicated to the plugin
|
||||
queues = []
|
||||
|
||||
# Django apps to append to INSTALLED_APPS when plugin requires them.
|
||||
django_apps = []
|
||||
|
||||
# Default integration paths. Plugin authors can override these to customize the paths to
|
||||
# integrated components.
|
||||
graphql_schema = 'graphql.schema'
|
||||
menu = 'navigation.menu'
|
||||
menu_items = 'navigation.menu_items'
|
||||
template_extensions = 'template_content.template_extensions'
|
||||
user_preferences = 'preferences.preferences'
|
||||
@@ -69,9 +74,10 @@ class PluginConfig(AppConfig):
|
||||
if template_extensions is not None:
|
||||
register_template_extensions(template_extensions)
|
||||
|
||||
# Register navigation menu items (if defined)
|
||||
menu_items = import_object(f"{self.__module__}.{self.menu_items}")
|
||||
if menu_items is not None:
|
||||
# Register navigation menu or menu items (if defined)
|
||||
if menu := import_object(f"{self.__module__}.{self.menu}"):
|
||||
register_menu(menu)
|
||||
if menu_items := import_object(f"{self.__module__}.{self.menu_items}"):
|
||||
register_menu_items(self.verbose_name, menu_items)
|
||||
|
||||
# Register GraphQL schema (if defined)
|
||||
@@ -200,6 +206,18 @@ def register_template_extensions(class_list):
|
||||
# Navigation menu links
|
||||
#
|
||||
|
||||
class PluginMenu:
|
||||
icon_class = 'mdi mdi-puzzle'
|
||||
|
||||
def __init__(self, label, groups, icon_class=None):
|
||||
self.label = label
|
||||
self.groups = [
|
||||
MenuGroup(label, items) for label, items in groups
|
||||
]
|
||||
if icon_class is not None:
|
||||
self.icon_class = icon_class
|
||||
|
||||
|
||||
class PluginMenuItem:
|
||||
"""
|
||||
This class represents a navigation menu item. This constitutes primary link and its text, but also allows for
|
||||
@@ -246,6 +264,12 @@ class PluginMenuButton:
|
||||
self.color = color
|
||||
|
||||
|
||||
def register_menu(menu):
|
||||
if not isinstance(menu, PluginMenu):
|
||||
raise TypeError(f"{menu} must be an instance of extras.plugins.PluginMenu")
|
||||
registry['plugins']['menus'].append(menu)
|
||||
|
||||
|
||||
def register_menu_items(section_name, class_list):
|
||||
"""
|
||||
Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name)
|
||||
|
||||
@@ -29,3 +29,4 @@ registry['model_features'] = {
|
||||
feature: collections.defaultdict(set) for feature in EXTRAS_FEATURES
|
||||
}
|
||||
registry['denormalized_fields'] = collections.defaultdict(list)
|
||||
registry['views'] = collections.defaultdict(dict)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from extras.plugins import PluginMenuButton, PluginMenuItem
|
||||
from extras.plugins import PluginMenu, PluginMenuButton, PluginMenuItem
|
||||
|
||||
|
||||
menu_items = (
|
||||
items = (
|
||||
PluginMenuItem(
|
||||
link='plugins:dummy_plugin:dummy_models',
|
||||
link_text='Item 1',
|
||||
@@ -23,3 +23,9 @@ menu_items = (
|
||||
link_text='Item 2',
|
||||
),
|
||||
)
|
||||
|
||||
menu = PluginMenu(
|
||||
label='Dummy',
|
||||
groups=(('Group 1', items),),
|
||||
)
|
||||
menu_items = items
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from django.http import HttpResponse
|
||||
from django.views.generic import View
|
||||
|
||||
from dcim.models import Site
|
||||
from utilities.views import register_model_view
|
||||
from .models import DummyModel
|
||||
|
||||
|
||||
@@ -9,3 +11,10 @@ class DummyModelsView(View):
|
||||
def get(self, request):
|
||||
instance_count = DummyModel.objects.count()
|
||||
return HttpResponse(f"Instances: {instance_count}")
|
||||
|
||||
|
||||
@register_model_view(Site, 'extra', path='other-stuff')
|
||||
class ExtraCoreModelView(View):
|
||||
|
||||
def get(self, request, pk):
|
||||
return HttpResponse("Success!")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
@@ -102,6 +104,32 @@ class CustomFieldTest(TestCase):
|
||||
instance.refresh_from_db()
|
||||
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||
|
||||
def test_decimal_field(self):
|
||||
|
||||
# Create a custom field & check that initial value is null
|
||||
cf = CustomField.objects.create(
|
||||
name='decimal_field',
|
||||
type=CustomFieldTypeChoices.TYPE_DECIMAL,
|
||||
required=False
|
||||
)
|
||||
cf.content_types.set([self.object_type])
|
||||
instance = Site.objects.first()
|
||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||
|
||||
for value in (123456.54, 0, -123456.78):
|
||||
|
||||
# Assign a value and check that it is saved
|
||||
instance.custom_field_data[cf.name] = value
|
||||
instance.save()
|
||||
instance.refresh_from_db()
|
||||
self.assertEqual(instance.custom_field_data[cf.name], value)
|
||||
|
||||
# Delete the stored value and check that it is now null
|
||||
instance.custom_field_data.pop(cf.name)
|
||||
instance.save()
|
||||
instance.refresh_from_db()
|
||||
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||
|
||||
def test_boolean_field(self):
|
||||
|
||||
# Create a custom field & check that initial value is null
|
||||
@@ -373,7 +401,8 @@ class CustomFieldAPITest(APITestCase):
|
||||
custom_fields = (
|
||||
CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo'),
|
||||
CustomField(type=CustomFieldTypeChoices.TYPE_LONGTEXT, name='longtext_field', default='ABC'),
|
||||
CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='number_field', default=123),
|
||||
CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='integer_field', default=123),
|
||||
CustomField(type=CustomFieldTypeChoices.TYPE_DECIMAL, name='decimal_field', default=123.45),
|
||||
CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='boolean_field', default=False),
|
||||
CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='date_field', default='2020-01-01'),
|
||||
CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='url_field', default='http://example.com/1'),
|
||||
@@ -424,14 +453,15 @@ class CustomFieldAPITest(APITestCase):
|
||||
custom_fields[0].name: 'bar',
|
||||
custom_fields[1].name: 'DEF',
|
||||
custom_fields[2].name: 456,
|
||||
custom_fields[3].name: True,
|
||||
custom_fields[4].name: '2020-01-02',
|
||||
custom_fields[5].name: 'http://example.com/2',
|
||||
custom_fields[6].name: '{"foo": 1, "bar": 2}',
|
||||
custom_fields[7].name: 'Bar',
|
||||
custom_fields[8].name: ['Bar', 'Baz'],
|
||||
custom_fields[9].name: vlans[1].pk,
|
||||
custom_fields[10].name: [vlans[2].pk, vlans[3].pk],
|
||||
custom_fields[3].name: Decimal('456.78'),
|
||||
custom_fields[4].name: True,
|
||||
custom_fields[5].name: '2020-01-02',
|
||||
custom_fields[6].name: 'http://example.com/2',
|
||||
custom_fields[7].name: '{"foo": 1, "bar": 2}',
|
||||
custom_fields[8].name: 'Bar',
|
||||
custom_fields[9].name: ['Bar', 'Baz'],
|
||||
custom_fields[10].name: vlans[1].pk,
|
||||
custom_fields[11].name: [vlans[2].pk, vlans[3].pk],
|
||||
}
|
||||
sites[1].save()
|
||||
|
||||
@@ -440,6 +470,7 @@ class CustomFieldAPITest(APITestCase):
|
||||
CustomFieldTypeChoices.TYPE_TEXT: 'string',
|
||||
CustomFieldTypeChoices.TYPE_LONGTEXT: 'string',
|
||||
CustomFieldTypeChoices.TYPE_INTEGER: 'integer',
|
||||
CustomFieldTypeChoices.TYPE_DECIMAL: 'decimal',
|
||||
CustomFieldTypeChoices.TYPE_BOOLEAN: 'boolean',
|
||||
CustomFieldTypeChoices.TYPE_DATE: 'string',
|
||||
CustomFieldTypeChoices.TYPE_URL: 'string',
|
||||
@@ -473,7 +504,8 @@ class CustomFieldAPITest(APITestCase):
|
||||
self.assertEqual(response.data['custom_fields'], {
|
||||
'text_field': None,
|
||||
'longtext_field': None,
|
||||
'number_field': None,
|
||||
'integer_field': None,
|
||||
'decimal_field': None,
|
||||
'boolean_field': None,
|
||||
'date_field': None,
|
||||
'url_field': None,
|
||||
@@ -497,7 +529,8 @@ class CustomFieldAPITest(APITestCase):
|
||||
self.assertEqual(response.data['name'], site2.name)
|
||||
self.assertEqual(response.data['custom_fields']['text_field'], site2_cfvs['text_field'])
|
||||
self.assertEqual(response.data['custom_fields']['longtext_field'], site2_cfvs['longtext_field'])
|
||||
self.assertEqual(response.data['custom_fields']['number_field'], site2_cfvs['number_field'])
|
||||
self.assertEqual(response.data['custom_fields']['integer_field'], site2_cfvs['integer_field'])
|
||||
self.assertEqual(response.data['custom_fields']['decimal_field'], site2_cfvs['decimal_field'])
|
||||
self.assertEqual(response.data['custom_fields']['boolean_field'], site2_cfvs['boolean_field'])
|
||||
self.assertEqual(response.data['custom_fields']['date_field'], site2_cfvs['date_field'])
|
||||
self.assertEqual(response.data['custom_fields']['url_field'], site2_cfvs['url_field'])
|
||||
@@ -531,7 +564,8 @@ class CustomFieldAPITest(APITestCase):
|
||||
response_cf = response.data['custom_fields']
|
||||
self.assertEqual(response_cf['text_field'], cf_defaults['text_field'])
|
||||
self.assertEqual(response_cf['longtext_field'], cf_defaults['longtext_field'])
|
||||
self.assertEqual(response_cf['number_field'], cf_defaults['number_field'])
|
||||
self.assertEqual(response_cf['integer_field'], cf_defaults['integer_field'])
|
||||
self.assertEqual(response_cf['decimal_field'], cf_defaults['decimal_field'])
|
||||
self.assertEqual(response_cf['boolean_field'], cf_defaults['boolean_field'])
|
||||
self.assertEqual(response_cf['date_field'], cf_defaults['date_field'])
|
||||
self.assertEqual(response_cf['url_field'], cf_defaults['url_field'])
|
||||
@@ -548,7 +582,8 @@ class CustomFieldAPITest(APITestCase):
|
||||
site = Site.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(site.custom_field_data['text_field'], cf_defaults['text_field'])
|
||||
self.assertEqual(site.custom_field_data['longtext_field'], cf_defaults['longtext_field'])
|
||||
self.assertEqual(site.custom_field_data['number_field'], cf_defaults['number_field'])
|
||||
self.assertEqual(site.custom_field_data['integer_field'], cf_defaults['integer_field'])
|
||||
self.assertEqual(site.custom_field_data['decimal_field'], cf_defaults['decimal_field'])
|
||||
self.assertEqual(site.custom_field_data['boolean_field'], cf_defaults['boolean_field'])
|
||||
self.assertEqual(str(site.custom_field_data['date_field']), cf_defaults['date_field'])
|
||||
self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field'])
|
||||
@@ -568,7 +603,8 @@ class CustomFieldAPITest(APITestCase):
|
||||
'custom_fields': {
|
||||
'text_field': 'bar',
|
||||
'longtext_field': 'blah blah blah',
|
||||
'number_field': 456,
|
||||
'integer_field': 456,
|
||||
'decimal_field': 456.78,
|
||||
'boolean_field': True,
|
||||
'date_field': '2020-01-02',
|
||||
'url_field': 'http://example.com/2',
|
||||
@@ -590,7 +626,8 @@ class CustomFieldAPITest(APITestCase):
|
||||
data_cf = data['custom_fields']
|
||||
self.assertEqual(response_cf['text_field'], data_cf['text_field'])
|
||||
self.assertEqual(response_cf['longtext_field'], data_cf['longtext_field'])
|
||||
self.assertEqual(response_cf['number_field'], data_cf['number_field'])
|
||||
self.assertEqual(response_cf['integer_field'], data_cf['integer_field'])
|
||||
self.assertEqual(response_cf['decimal_field'], data_cf['decimal_field'])
|
||||
self.assertEqual(response_cf['boolean_field'], data_cf['boolean_field'])
|
||||
self.assertEqual(response_cf['date_field'], data_cf['date_field'])
|
||||
self.assertEqual(response_cf['url_field'], data_cf['url_field'])
|
||||
@@ -607,7 +644,8 @@ class CustomFieldAPITest(APITestCase):
|
||||
site = Site.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(site.custom_field_data['text_field'], data_cf['text_field'])
|
||||
self.assertEqual(site.custom_field_data['longtext_field'], data_cf['longtext_field'])
|
||||
self.assertEqual(site.custom_field_data['number_field'], data_cf['number_field'])
|
||||
self.assertEqual(site.custom_field_data['integer_field'], data_cf['integer_field'])
|
||||
self.assertEqual(site.custom_field_data['decimal_field'], data_cf['decimal_field'])
|
||||
self.assertEqual(site.custom_field_data['boolean_field'], data_cf['boolean_field'])
|
||||
self.assertEqual(str(site.custom_field_data['date_field']), data_cf['date_field'])
|
||||
self.assertEqual(site.custom_field_data['url_field'], data_cf['url_field'])
|
||||
@@ -652,7 +690,8 @@ class CustomFieldAPITest(APITestCase):
|
||||
response_cf = response.data[i]['custom_fields']
|
||||
self.assertEqual(response_cf['text_field'], cf_defaults['text_field'])
|
||||
self.assertEqual(response_cf['longtext_field'], cf_defaults['longtext_field'])
|
||||
self.assertEqual(response_cf['number_field'], cf_defaults['number_field'])
|
||||
self.assertEqual(response_cf['integer_field'], cf_defaults['integer_field'])
|
||||
self.assertEqual(response_cf['decimal_field'], cf_defaults['decimal_field'])
|
||||
self.assertEqual(response_cf['boolean_field'], cf_defaults['boolean_field'])
|
||||
self.assertEqual(response_cf['date_field'], cf_defaults['date_field'])
|
||||
self.assertEqual(response_cf['url_field'], cf_defaults['url_field'])
|
||||
@@ -669,7 +708,8 @@ class CustomFieldAPITest(APITestCase):
|
||||
site = Site.objects.get(pk=response.data[i]['id'])
|
||||
self.assertEqual(site.custom_field_data['text_field'], cf_defaults['text_field'])
|
||||
self.assertEqual(site.custom_field_data['longtext_field'], cf_defaults['longtext_field'])
|
||||
self.assertEqual(site.custom_field_data['number_field'], cf_defaults['number_field'])
|
||||
self.assertEqual(site.custom_field_data['integer_field'], cf_defaults['integer_field'])
|
||||
self.assertEqual(site.custom_field_data['decimal_field'], cf_defaults['decimal_field'])
|
||||
self.assertEqual(site.custom_field_data['boolean_field'], cf_defaults['boolean_field'])
|
||||
self.assertEqual(str(site.custom_field_data['date_field']), cf_defaults['date_field'])
|
||||
self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field'])
|
||||
@@ -686,7 +726,8 @@ class CustomFieldAPITest(APITestCase):
|
||||
custom_field_data = {
|
||||
'text_field': 'bar',
|
||||
'longtext_field': 'abcdefghij',
|
||||
'number_field': 456,
|
||||
'integer_field': 456,
|
||||
'decimal_field': 456.78,
|
||||
'boolean_field': True,
|
||||
'date_field': '2020-01-02',
|
||||
'url_field': 'http://example.com/2',
|
||||
@@ -726,7 +767,8 @@ class CustomFieldAPITest(APITestCase):
|
||||
response_cf = response.data[i]['custom_fields']
|
||||
self.assertEqual(response_cf['text_field'], custom_field_data['text_field'])
|
||||
self.assertEqual(response_cf['longtext_field'], custom_field_data['longtext_field'])
|
||||
self.assertEqual(response_cf['number_field'], custom_field_data['number_field'])
|
||||
self.assertEqual(response_cf['integer_field'], custom_field_data['integer_field'])
|
||||
self.assertEqual(response_cf['decimal_field'], custom_field_data['decimal_field'])
|
||||
self.assertEqual(response_cf['boolean_field'], custom_field_data['boolean_field'])
|
||||
self.assertEqual(response_cf['date_field'], custom_field_data['date_field'])
|
||||
self.assertEqual(response_cf['url_field'], custom_field_data['url_field'])
|
||||
@@ -743,7 +785,8 @@ class CustomFieldAPITest(APITestCase):
|
||||
site = Site.objects.get(pk=response.data[i]['id'])
|
||||
self.assertEqual(site.custom_field_data['text_field'], custom_field_data['text_field'])
|
||||
self.assertEqual(site.custom_field_data['longtext_field'], custom_field_data['longtext_field'])
|
||||
self.assertEqual(site.custom_field_data['number_field'], custom_field_data['number_field'])
|
||||
self.assertEqual(site.custom_field_data['integer_field'], custom_field_data['integer_field'])
|
||||
self.assertEqual(site.custom_field_data['decimal_field'], custom_field_data['decimal_field'])
|
||||
self.assertEqual(site.custom_field_data['boolean_field'], custom_field_data['boolean_field'])
|
||||
self.assertEqual(str(site.custom_field_data['date_field']), custom_field_data['date_field'])
|
||||
self.assertEqual(site.custom_field_data['url_field'], custom_field_data['url_field'])
|
||||
@@ -763,7 +806,7 @@ class CustomFieldAPITest(APITestCase):
|
||||
data = {
|
||||
'custom_fields': {
|
||||
'text_field': 'ABCD',
|
||||
'number_field': 1234,
|
||||
'integer_field': 1234,
|
||||
},
|
||||
}
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
|
||||
@@ -775,8 +818,9 @@ class CustomFieldAPITest(APITestCase):
|
||||
# Validate response data
|
||||
response_cf = response.data['custom_fields']
|
||||
self.assertEqual(response_cf['text_field'], data['custom_fields']['text_field'])
|
||||
self.assertEqual(response_cf['number_field'], data['custom_fields']['number_field'])
|
||||
self.assertEqual(response_cf['longtext_field'], original_cfvs['longtext_field'])
|
||||
self.assertEqual(response_cf['integer_field'], data['custom_fields']['integer_field'])
|
||||
self.assertEqual(response_cf['decimal_field'], original_cfvs['decimal_field'])
|
||||
self.assertEqual(response_cf['boolean_field'], original_cfvs['boolean_field'])
|
||||
self.assertEqual(response_cf['date_field'], original_cfvs['date_field'])
|
||||
self.assertEqual(response_cf['url_field'], original_cfvs['url_field'])
|
||||
@@ -792,8 +836,9 @@ class CustomFieldAPITest(APITestCase):
|
||||
# Validate database data
|
||||
site2.refresh_from_db()
|
||||
self.assertEqual(site2.custom_field_data['text_field'], data['custom_fields']['text_field'])
|
||||
self.assertEqual(site2.custom_field_data['number_field'], data['custom_fields']['number_field'])
|
||||
self.assertEqual(site2.custom_field_data['longtext_field'], original_cfvs['longtext_field'])
|
||||
self.assertEqual(site2.custom_field_data['integer_field'], data['custom_fields']['integer_field'])
|
||||
self.assertEqual(site2.custom_field_data['decimal_field'], original_cfvs['decimal_field'])
|
||||
self.assertEqual(site2.custom_field_data['boolean_field'], original_cfvs['boolean_field'])
|
||||
self.assertEqual(site2.custom_field_data['date_field'], original_cfvs['date_field'])
|
||||
self.assertEqual(site2.custom_field_data['url_field'], original_cfvs['url_field'])
|
||||
@@ -808,20 +853,20 @@ class CustomFieldAPITest(APITestCase):
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
|
||||
self.add_permissions('dcim.change_site')
|
||||
|
||||
cf_integer = CustomField.objects.get(name='number_field')
|
||||
cf_integer = CustomField.objects.get(name='integer_field')
|
||||
cf_integer.validation_minimum = 10
|
||||
cf_integer.validation_maximum = 20
|
||||
cf_integer.save()
|
||||
|
||||
data = {'custom_fields': {'number_field': 9}}
|
||||
data = {'custom_fields': {'integer_field': 9}}
|
||||
response = self.client.patch(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
data = {'custom_fields': {'number_field': 21}}
|
||||
data = {'custom_fields': {'integer_field': 21}}
|
||||
response = self.client.patch(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
data = {'custom_fields': {'number_field': 15}}
|
||||
data = {'custom_fields': {'integer_field': 15}}
|
||||
response = self.client.patch(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
|
||||
@@ -860,6 +905,7 @@ class CustomFieldImportTest(TestCase):
|
||||
CustomField(name='text', type=CustomFieldTypeChoices.TYPE_TEXT),
|
||||
CustomField(name='longtext', type=CustomFieldTypeChoices.TYPE_LONGTEXT),
|
||||
CustomField(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER),
|
||||
CustomField(name='decimal', type=CustomFieldTypeChoices.TYPE_DECIMAL),
|
||||
CustomField(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN),
|
||||
CustomField(name='date', type=CustomFieldTypeChoices.TYPE_DATE),
|
||||
CustomField(name='url', type=CustomFieldTypeChoices.TYPE_URL),
|
||||
@@ -880,10 +926,10 @@ class CustomFieldImportTest(TestCase):
|
||||
Import a Site in CSV format, including a value for each CustomField.
|
||||
"""
|
||||
data = (
|
||||
('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect'),
|
||||
('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', 'True', '2020-01-01', 'http://example.com/1', '{"foo": 123}', 'Choice A', '"Choice A,Choice B"'),
|
||||
('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', 'False', '2020-01-02', 'http://example.com/2', '{"bar": 456}', 'Choice B', '"Choice B,Choice C"'),
|
||||
('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', ''),
|
||||
('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_decimal', 'cf_boolean', 'cf_date', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect'),
|
||||
('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', '123.45', 'True', '2020-01-01', 'http://example.com/1', '{"foo": 123}', 'Choice A', '"Choice A,Choice B"'),
|
||||
('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', '456.78', 'False', '2020-01-02', 'http://example.com/2', '{"bar": 456}', 'Choice B', '"Choice B,Choice C"'),
|
||||
('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', '', ''),
|
||||
)
|
||||
csv_data = '\n'.join(','.join(row) for row in data)
|
||||
|
||||
@@ -893,10 +939,11 @@ class CustomFieldImportTest(TestCase):
|
||||
|
||||
# Validate data for site 1
|
||||
site1 = Site.objects.get(name='Site 1')
|
||||
self.assertEqual(len(site1.custom_field_data), 9)
|
||||
self.assertEqual(len(site1.custom_field_data), 10)
|
||||
self.assertEqual(site1.custom_field_data['text'], 'ABC')
|
||||
self.assertEqual(site1.custom_field_data['longtext'], 'Foo')
|
||||
self.assertEqual(site1.custom_field_data['integer'], 123)
|
||||
self.assertEqual(site1.custom_field_data['decimal'], 123.45)
|
||||
self.assertEqual(site1.custom_field_data['boolean'], True)
|
||||
self.assertEqual(site1.custom_field_data['date'], '2020-01-01')
|
||||
self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1')
|
||||
@@ -906,10 +953,11 @@ class CustomFieldImportTest(TestCase):
|
||||
|
||||
# Validate data for site 2
|
||||
site2 = Site.objects.get(name='Site 2')
|
||||
self.assertEqual(len(site2.custom_field_data), 9)
|
||||
self.assertEqual(len(site2.custom_field_data), 10)
|
||||
self.assertEqual(site2.custom_field_data['text'], 'DEF')
|
||||
self.assertEqual(site2.custom_field_data['longtext'], 'Bar')
|
||||
self.assertEqual(site2.custom_field_data['integer'], 456)
|
||||
self.assertEqual(site2.custom_field_data['decimal'], 456.78)
|
||||
self.assertEqual(site2.custom_field_data['boolean'], False)
|
||||
self.assertEqual(site2.custom_field_data['date'], '2020-01-02')
|
||||
self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2')
|
||||
@@ -1034,53 +1082,78 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Decimal filtering
|
||||
cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_DECIMAL)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Boolean filtering
|
||||
cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
|
||||
cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Exact text filtering
|
||||
cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT)
|
||||
cf = CustomField(
|
||||
name='cf4',
|
||||
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Loose text filtering
|
||||
cf = CustomField(name='cf4', type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE)
|
||||
cf = CustomField(
|
||||
name='cf5',
|
||||
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Date filtering
|
||||
cf = CustomField(name='cf5', type=CustomFieldTypeChoices.TYPE_DATE)
|
||||
cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_DATE)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Exact URL filtering
|
||||
cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_URL,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT)
|
||||
cf = CustomField(
|
||||
name='cf7',
|
||||
type=CustomFieldTypeChoices.TYPE_URL,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Loose URL filtering
|
||||
cf = CustomField(name='cf7', type=CustomFieldTypeChoices.TYPE_URL,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE)
|
||||
cf = CustomField(
|
||||
name='cf8',
|
||||
type=CustomFieldTypeChoices.TYPE_URL,
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Selection filtering
|
||||
cf = CustomField(name='cf8', type=CustomFieldTypeChoices.TYPE_SELECT, choices=['Foo', 'Bar', 'Baz'])
|
||||
cf = CustomField(
|
||||
name='cf9',
|
||||
type=CustomFieldTypeChoices.TYPE_SELECT,
|
||||
choices=['Foo', 'Bar', 'Baz']
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Multiselect filtering
|
||||
cf = CustomField(name='cf9', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choices=['A', 'B', 'C', 'X'])
|
||||
cf = CustomField(
|
||||
name='cf10',
|
||||
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
|
||||
choices=['A', 'B', 'C', 'X']
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
|
||||
# Object filtering
|
||||
cf = CustomField(
|
||||
name='cf10',
|
||||
name='cf11',
|
||||
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
||||
object_type=ContentType.objects.get_for_model(Manufacturer)
|
||||
)
|
||||
@@ -1089,7 +1162,7 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
|
||||
# Multi-object filtering
|
||||
cf = CustomField(
|
||||
name='cf11',
|
||||
name='cf12',
|
||||
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
||||
object_type=ContentType.objects.get_for_model(Manufacturer)
|
||||
)
|
||||
@@ -1099,42 +1172,45 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
Site.objects.bulk_create([
|
||||
Site(name='Site 1', slug='site-1', custom_field_data={
|
||||
'cf1': 100,
|
||||
'cf2': True,
|
||||
'cf3': 'foo',
|
||||
'cf2': 100.1,
|
||||
'cf3': True,
|
||||
'cf4': 'foo',
|
||||
'cf5': '2016-06-26',
|
||||
'cf6': 'http://a.example.com',
|
||||
'cf5': 'foo',
|
||||
'cf6': '2016-06-26',
|
||||
'cf7': 'http://a.example.com',
|
||||
'cf8': 'Foo',
|
||||
'cf9': ['A', 'X'],
|
||||
'cf10': manufacturers[0].pk,
|
||||
'cf11': [manufacturers[0].pk, manufacturers[3].pk],
|
||||
'cf8': 'http://a.example.com',
|
||||
'cf9': 'Foo',
|
||||
'cf10': ['A', 'X'],
|
||||
'cf11': manufacturers[0].pk,
|
||||
'cf12': [manufacturers[0].pk, manufacturers[3].pk],
|
||||
}),
|
||||
Site(name='Site 2', slug='site-2', custom_field_data={
|
||||
'cf1': 200,
|
||||
'cf2': True,
|
||||
'cf3': 'foobar',
|
||||
'cf2': 200.2,
|
||||
'cf3': True,
|
||||
'cf4': 'foobar',
|
||||
'cf5': '2016-06-27',
|
||||
'cf6': 'http://b.example.com',
|
||||
'cf5': 'foobar',
|
||||
'cf6': '2016-06-27',
|
||||
'cf7': 'http://b.example.com',
|
||||
'cf8': 'Bar',
|
||||
'cf9': ['B', 'X'],
|
||||
'cf10': manufacturers[1].pk,
|
||||
'cf11': [manufacturers[1].pk, manufacturers[3].pk],
|
||||
'cf8': 'http://b.example.com',
|
||||
'cf9': 'Bar',
|
||||
'cf10': ['B', 'X'],
|
||||
'cf11': manufacturers[1].pk,
|
||||
'cf12': [manufacturers[1].pk, manufacturers[3].pk],
|
||||
}),
|
||||
Site(name='Site 3', slug='site-3', custom_field_data={
|
||||
'cf1': 300,
|
||||
'cf2': False,
|
||||
'cf3': 'bar',
|
||||
'cf2': 300.3,
|
||||
'cf3': False,
|
||||
'cf4': 'bar',
|
||||
'cf5': '2016-06-28',
|
||||
'cf6': 'http://c.example.com',
|
||||
'cf5': 'bar',
|
||||
'cf6': '2016-06-28',
|
||||
'cf7': 'http://c.example.com',
|
||||
'cf8': 'Baz',
|
||||
'cf9': ['C', 'X'],
|
||||
'cf10': manufacturers[2].pk,
|
||||
'cf11': [manufacturers[2].pk, manufacturers[3].pk],
|
||||
'cf8': 'http://c.example.com',
|
||||
'cf9': 'Baz',
|
||||
'cf10': ['C', 'X'],
|
||||
'cf11': manufacturers[2].pk,
|
||||
'cf12': [manufacturers[2].pk, manufacturers[3].pk],
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -1146,60 +1222,68 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
self.assertEqual(self.filterset({'cf_cf1__lt': [200]}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf1__lte': [200]}, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_filter_decimal(self):
|
||||
self.assertEqual(self.filterset({'cf_cf2': [100.1, 200.2]}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf2__n': [200.2]}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf2__gt': [200.2]}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf2__gte': [200.2]}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf2__lt': [200.2]}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf2__lte': [200.2]}, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_filter_boolean(self):
|
||||
self.assertEqual(self.filterset({'cf_cf2': True}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf2': False}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf3': True}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf3': False}, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_filter_text_strict(self):
|
||||
self.assertEqual(self.filterset({'cf_cf3': ['foo']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf3__n': ['foo']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf3__ic': ['foo']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf3__nic': ['foo']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf3__isw': ['foo']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf3__nisw': ['foo']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf3__iew': ['bar']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf3__niew': ['bar']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf3__ie': ['FOO']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf3__nie': ['FOO']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf4': ['foo']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf4__n': ['foo']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf4__ic': ['foo']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf4__nic': ['foo']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf4__isw': ['foo']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf4__nisw': ['foo']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf4__iew': ['bar']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf4__niew': ['bar']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf4__ie': ['FOO']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf4__nie': ['FOO']}, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_filter_text_loose(self):
|
||||
self.assertEqual(self.filterset({'cf_cf4': ['foo']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf5': ['foo']}, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_filter_date(self):
|
||||
self.assertEqual(self.filterset({'cf_cf5': ['2016-06-26', '2016-06-27']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf5__n': ['2016-06-27']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf5__gt': ['2016-06-27']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf5__gte': ['2016-06-27']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf5__lt': ['2016-06-27']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf5__lte': ['2016-06-27']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf6': ['2016-06-26', '2016-06-27']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf6__n': ['2016-06-27']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf6__gt': ['2016-06-27']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf6__gte': ['2016-06-27']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf6__lt': ['2016-06-27']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf6__lte': ['2016-06-27']}, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_filter_url_strict(self):
|
||||
self.assertEqual(self.filterset({'cf_cf6': ['http://a.example.com', 'http://b.example.com']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf6__n': ['http://b.example.com']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf6__ic': ['b']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf6__nic': ['b']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf6__isw': ['http://']}, self.queryset).qs.count(), 3)
|
||||
self.assertEqual(self.filterset({'cf_cf6__nisw': ['http://']}, self.queryset).qs.count(), 0)
|
||||
self.assertEqual(self.filterset({'cf_cf6__iew': ['.com']}, self.queryset).qs.count(), 3)
|
||||
self.assertEqual(self.filterset({'cf_cf6__niew': ['.com']}, self.queryset).qs.count(), 0)
|
||||
self.assertEqual(self.filterset({'cf_cf6__ie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf6__nie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf7': ['http://a.example.com', 'http://b.example.com']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf7__n': ['http://b.example.com']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf7__ic': ['b']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf7__nic': ['b']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf7__isw': ['http://']}, self.queryset).qs.count(), 3)
|
||||
self.assertEqual(self.filterset({'cf_cf7__nisw': ['http://']}, self.queryset).qs.count(), 0)
|
||||
self.assertEqual(self.filterset({'cf_cf7__iew': ['.com']}, self.queryset).qs.count(), 3)
|
||||
self.assertEqual(self.filterset({'cf_cf7__niew': ['.com']}, self.queryset).qs.count(), 0)
|
||||
self.assertEqual(self.filterset({'cf_cf7__ie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 1)
|
||||
self.assertEqual(self.filterset({'cf_cf7__nie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_filter_url_loose(self):
|
||||
self.assertEqual(self.filterset({'cf_cf7': ['example.com']}, self.queryset).qs.count(), 3)
|
||||
self.assertEqual(self.filterset({'cf_cf8': ['example.com']}, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_filter_select(self):
|
||||
self.assertEqual(self.filterset({'cf_cf8': ['Foo', 'Bar']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf9': ['Foo', 'Bar']}, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_filter_multiselect(self):
|
||||
self.assertEqual(self.filterset({'cf_cf9': ['A', 'B']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf9': ['X']}, self.queryset).qs.count(), 3)
|
||||
self.assertEqual(self.filterset({'cf_cf10': ['A', 'B']}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf10': ['X']}, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_filter_object(self):
|
||||
manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
|
||||
self.assertEqual(self.filterset({'cf_cf10': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf11': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_filter_multiobject(self):
|
||||
manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
|
||||
self.assertEqual(self.filterset({'cf_cf11': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf11': [manufacturer_ids[3]]}, self.queryset).qs.count(), 3)
|
||||
self.assertEqual(self.filterset({'cf_cf12': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(), 2)
|
||||
self.assertEqual(self.filterset({'cf_cf12': [manufacturer_ids[3]]}, self.queryset).qs.count(), 3)
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from circuits.models import Provider
|
||||
from ipam.models import ASN, RIR
|
||||
from dcim.models import Site
|
||||
from extras.validators import CustomValidator
|
||||
|
||||
@@ -67,21 +67,25 @@ custom_validator = MyValidator()
|
||||
|
||||
class CustomValidatorTest(TestCase):
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={'circuits.provider': [min_validator]})
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
RIR.objects.create(name='RIR 1', slug='rir-1')
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={'ipam.asn': [min_validator]})
|
||||
def test_configuration(self):
|
||||
self.assertIn('circuits.provider', settings.CUSTOM_VALIDATORS)
|
||||
validator = settings.CUSTOM_VALIDATORS['circuits.provider'][0]
|
||||
self.assertIn('ipam.asn', settings.CUSTOM_VALIDATORS)
|
||||
validator = settings.CUSTOM_VALIDATORS['ipam.asn'][0]
|
||||
self.assertIsInstance(validator, CustomValidator)
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={'circuits.provider': [min_validator]})
|
||||
@override_settings(CUSTOM_VALIDATORS={'ipam.asn': [min_validator]})
|
||||
def test_min(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
Provider(name='Provider 1', slug='provider-1', asn=1).clean()
|
||||
ASN(asn=1, rir=RIR.objects.first()).clean()
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={'circuits.provider': [max_validator]})
|
||||
@override_settings(CUSTOM_VALIDATORS={'ipam.asn': [max_validator]})
|
||||
def test_max(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
Provider(name='Provider 1', slug='provider-1', asn=65535).clean()
|
||||
ASN(asn=65535, rir=RIR.objects.first()).clean()
|
||||
|
||||
@override_settings(CUSTOM_VALIDATORS={'dcim.site': [min_length_validator]})
|
||||
def test_min_length(self):
|
||||
|
||||
@@ -23,6 +23,9 @@ class CustomFieldModelFormTest(TestCase):
|
||||
cf_integer = CustomField.objects.create(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER)
|
||||
cf_integer.content_types.set([obj_type])
|
||||
|
||||
cf_integer = CustomField.objects.create(name='decimal', type=CustomFieldTypeChoices.TYPE_DECIMAL)
|
||||
cf_integer.content_types.set([obj_type])
|
||||
|
||||
cf_boolean = CustomField.objects.create(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
|
||||
cf_boolean.content_types.set([obj_type])
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import Client, TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from extras.plugins import PluginMenu
|
||||
from extras.registry import registry
|
||||
from extras.tests.dummy_plugin import config as dummy_config
|
||||
from netbox.graphql.schema import Query
|
||||
@@ -58,9 +59,28 @@ class PluginTest(TestCase):
|
||||
response = client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_registered_views(self):
|
||||
|
||||
# Test URL resolution
|
||||
url = reverse('dcim:site_extra', kwargs={'pk': 1})
|
||||
self.assertEqual(url, '/dcim/sites/1/other-stuff/')
|
||||
|
||||
# Test GET request
|
||||
client = Client()
|
||||
response = client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_menu(self):
|
||||
"""
|
||||
Check menu registration.
|
||||
"""
|
||||
menu = registry['plugins']['menus'][0]
|
||||
self.assertIsInstance(menu, PluginMenu)
|
||||
self.assertEqual(menu.label, 'Dummy')
|
||||
|
||||
def test_menu_items(self):
|
||||
"""
|
||||
Check that plugin MenuItems and MenuButtons are registered.
|
||||
Check menu_items registration.
|
||||
"""
|
||||
self.assertIn('Dummy plugin', registry['plugins']['menu_items'])
|
||||
menu_items = registry['plugins']['menu_items']['Dummy plugin']
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.urls import path, re_path
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
from extras import models, views
|
||||
from netbox.views.generic import ObjectChangeLogView
|
||||
from extras import views
|
||||
from utilities.urls import get_model_urls
|
||||
|
||||
|
||||
app_name = 'extras'
|
||||
@@ -13,11 +13,7 @@ urlpatterns = [
|
||||
path('custom-fields/import/', views.CustomFieldBulkImportView.as_view(), name='customfield_import'),
|
||||
path('custom-fields/edit/', views.CustomFieldBulkEditView.as_view(), name='customfield_bulk_edit'),
|
||||
path('custom-fields/delete/', views.CustomFieldBulkDeleteView.as_view(), name='customfield_bulk_delete'),
|
||||
path('custom-fields/<int:pk>/', views.CustomFieldView.as_view(), name='customfield'),
|
||||
path('custom-fields/<int:pk>/edit/', views.CustomFieldEditView.as_view(), name='customfield_edit'),
|
||||
path('custom-fields/<int:pk>/delete/', views.CustomFieldDeleteView.as_view(), name='customfield_delete'),
|
||||
path('custom-fields/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='customfield_changelog',
|
||||
kwargs={'model': models.CustomField}),
|
||||
path('custom-fields/<int:pk>/', include(get_model_urls('extras', 'customfield'))),
|
||||
|
||||
# Custom links
|
||||
path('custom-links/', views.CustomLinkListView.as_view(), name='customlink_list'),
|
||||
@@ -25,11 +21,7 @@ urlpatterns = [
|
||||
path('custom-links/import/', views.CustomLinkBulkImportView.as_view(), name='customlink_import'),
|
||||
path('custom-links/edit/', views.CustomLinkBulkEditView.as_view(), name='customlink_bulk_edit'),
|
||||
path('custom-links/delete/', views.CustomLinkBulkDeleteView.as_view(), name='customlink_bulk_delete'),
|
||||
path('custom-links/<int:pk>/', views.CustomLinkView.as_view(), name='customlink'),
|
||||
path('custom-links/<int:pk>/edit/', views.CustomLinkEditView.as_view(), name='customlink_edit'),
|
||||
path('custom-links/<int:pk>/delete/', views.CustomLinkDeleteView.as_view(), name='customlink_delete'),
|
||||
path('custom-links/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='customlink_changelog',
|
||||
kwargs={'model': models.CustomLink}),
|
||||
path('custom-links/<int:pk>/', include(get_model_urls('extras', 'customlink'))),
|
||||
|
||||
# Export templates
|
||||
path('export-templates/', views.ExportTemplateListView.as_view(), name='exporttemplate_list'),
|
||||
@@ -37,11 +29,7 @@ urlpatterns = [
|
||||
path('export-templates/import/', views.ExportTemplateBulkImportView.as_view(), name='exporttemplate_import'),
|
||||
path('export-templates/edit/', views.ExportTemplateBulkEditView.as_view(), name='exporttemplate_bulk_edit'),
|
||||
path('export-templates/delete/', views.ExportTemplateBulkDeleteView.as_view(), name='exporttemplate_bulk_delete'),
|
||||
path('export-templates/<int:pk>/', views.ExportTemplateView.as_view(), name='exporttemplate'),
|
||||
path('export-templates/<int:pk>/edit/', views.ExportTemplateEditView.as_view(), name='exporttemplate_edit'),
|
||||
path('export-templates/<int:pk>/delete/', views.ExportTemplateDeleteView.as_view(), name='exporttemplate_delete'),
|
||||
path('export-templates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='exporttemplate_changelog',
|
||||
kwargs={'model': models.ExportTemplate}),
|
||||
path('export-templates/<int:pk>/', include(get_model_urls('extras', 'exporttemplate'))),
|
||||
|
||||
# Webhooks
|
||||
path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'),
|
||||
@@ -49,11 +37,7 @@ urlpatterns = [
|
||||
path('webhooks/import/', views.WebhookBulkImportView.as_view(), name='webhook_import'),
|
||||
path('webhooks/edit/', views.WebhookBulkEditView.as_view(), name='webhook_bulk_edit'),
|
||||
path('webhooks/delete/', views.WebhookBulkDeleteView.as_view(), name='webhook_bulk_delete'),
|
||||
path('webhooks/<int:pk>/', views.WebhookView.as_view(), name='webhook'),
|
||||
path('webhooks/<int:pk>/edit/', views.WebhookEditView.as_view(), name='webhook_edit'),
|
||||
path('webhooks/<int:pk>/delete/', views.WebhookDeleteView.as_view(), name='webhook_delete'),
|
||||
path('webhooks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='webhook_changelog',
|
||||
kwargs={'model': models.Webhook}),
|
||||
path('webhooks/<int:pk>/', include(get_model_urls('extras', 'webhook'))),
|
||||
|
||||
# Tags
|
||||
path('tags/', views.TagListView.as_view(), name='tag_list'),
|
||||
@@ -61,42 +45,29 @@ urlpatterns = [
|
||||
path('tags/import/', views.TagBulkImportView.as_view(), name='tag_import'),
|
||||
path('tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'),
|
||||
path('tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
path('tags/<int:pk>/', views.TagView.as_view(), name='tag'),
|
||||
path('tags/<int:pk>/edit/', views.TagEditView.as_view(), name='tag_edit'),
|
||||
path('tags/<int:pk>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||
path('tags/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='tag_changelog',
|
||||
kwargs={'model': models.Tag}),
|
||||
path('tags/<int:pk>/', include(get_model_urls('extras', 'tag'))),
|
||||
|
||||
# Config contexts
|
||||
path('config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
||||
path('config-contexts/add/', views.ConfigContextEditView.as_view(), name='configcontext_add'),
|
||||
path('config-contexts/edit/', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'),
|
||||
path('config-contexts/delete/', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
|
||||
path('config-contexts/<int:pk>/', views.ConfigContextView.as_view(), name='configcontext'),
|
||||
path('config-contexts/<int:pk>/edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
|
||||
path('config-contexts/<int:pk>/delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
|
||||
path('config-contexts/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='configcontext_changelog',
|
||||
kwargs={'model': models.ConfigContext}),
|
||||
path('config-contexts/<int:pk>/', include(get_model_urls('extras', 'configcontext'))),
|
||||
|
||||
# Image attachments
|
||||
path('image-attachments/add/', views.ImageAttachmentEditView.as_view(), name='imageattachment_add'),
|
||||
path('image-attachments/<int:pk>/edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
|
||||
path('image-attachments/<int:pk>/delete/', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
|
||||
path('image-attachments/<int:pk>/', include(get_model_urls('extras', 'imageattachment'))),
|
||||
|
||||
# Journal entries
|
||||
path('journal-entries/', views.JournalEntryListView.as_view(), name='journalentry_list'),
|
||||
path('journal-entries/add/', views.JournalEntryEditView.as_view(), name='journalentry_add'),
|
||||
path('journal-entries/edit/', views.JournalEntryBulkEditView.as_view(), name='journalentry_bulk_edit'),
|
||||
path('journal-entries/delete/', views.JournalEntryBulkDeleteView.as_view(), name='journalentry_bulk_delete'),
|
||||
path('journal-entries/<int:pk>/', views.JournalEntryView.as_view(), name='journalentry'),
|
||||
path('journal-entries/<int:pk>/edit/', views.JournalEntryEditView.as_view(), name='journalentry_edit'),
|
||||
path('journal-entries/<int:pk>/delete/', views.JournalEntryDeleteView.as_view(), name='journalentry_delete'),
|
||||
path('journal-entries/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='journalentry_changelog',
|
||||
kwargs={'model': models.JournalEntry}),
|
||||
path('journal-entries/<int:pk>/', include(get_model_urls('extras', 'journalentry'))),
|
||||
|
||||
# Change logging
|
||||
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
||||
path('changelog/<int:pk>/', views.ObjectChangeView.as_view(), name='objectchange'),
|
||||
path('changelog/<int:pk>/', include(get_model_urls('extras', 'objectchange'))),
|
||||
|
||||
# Reports
|
||||
path('reports/', views.ReportListView.as_view(), name='report_list'),
|
||||
|
||||
@@ -12,7 +12,7 @@ from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.htmx import is_htmx
|
||||
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
|
||||
from utilities.views import ContentTypePermissionRequiredMixin
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
from .choices import JobResultStatusChoices
|
||||
from .forms.reports import ReportForm
|
||||
@@ -32,15 +32,18 @@ class CustomFieldListView(generic.ObjectListView):
|
||||
table = tables.CustomFieldTable
|
||||
|
||||
|
||||
@register_model_view(CustomField)
|
||||
class CustomFieldView(generic.ObjectView):
|
||||
queryset = CustomField.objects.all()
|
||||
|
||||
|
||||
@register_model_view(CustomField, 'edit')
|
||||
class CustomFieldEditView(generic.ObjectEditView):
|
||||
queryset = CustomField.objects.all()
|
||||
form = forms.CustomFieldForm
|
||||
|
||||
|
||||
@register_model_view(CustomField, 'delete')
|
||||
class CustomFieldDeleteView(generic.ObjectDeleteView):
|
||||
queryset = CustomField.objects.all()
|
||||
|
||||
@@ -75,15 +78,18 @@ class CustomLinkListView(generic.ObjectListView):
|
||||
table = tables.CustomLinkTable
|
||||
|
||||
|
||||
@register_model_view(CustomLink)
|
||||
class CustomLinkView(generic.ObjectView):
|
||||
queryset = CustomLink.objects.all()
|
||||
|
||||
|
||||
@register_model_view(CustomLink, 'edit')
|
||||
class CustomLinkEditView(generic.ObjectEditView):
|
||||
queryset = CustomLink.objects.all()
|
||||
form = forms.CustomLinkForm
|
||||
|
||||
|
||||
@register_model_view(CustomLink, 'delete')
|
||||
class CustomLinkDeleteView(generic.ObjectDeleteView):
|
||||
queryset = CustomLink.objects.all()
|
||||
|
||||
@@ -118,15 +124,18 @@ class ExportTemplateListView(generic.ObjectListView):
|
||||
table = tables.ExportTemplateTable
|
||||
|
||||
|
||||
@register_model_view(ExportTemplate)
|
||||
class ExportTemplateView(generic.ObjectView):
|
||||
queryset = ExportTemplate.objects.all()
|
||||
|
||||
|
||||
@register_model_view(ExportTemplate, 'edit')
|
||||
class ExportTemplateEditView(generic.ObjectEditView):
|
||||
queryset = ExportTemplate.objects.all()
|
||||
form = forms.ExportTemplateForm
|
||||
|
||||
|
||||
@register_model_view(ExportTemplate, 'delete')
|
||||
class ExportTemplateDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ExportTemplate.objects.all()
|
||||
|
||||
@@ -161,15 +170,18 @@ class WebhookListView(generic.ObjectListView):
|
||||
table = tables.WebhookTable
|
||||
|
||||
|
||||
@register_model_view(Webhook)
|
||||
class WebhookView(generic.ObjectView):
|
||||
queryset = Webhook.objects.all()
|
||||
|
||||
|
||||
@register_model_view(Webhook, 'edit')
|
||||
class WebhookEditView(generic.ObjectEditView):
|
||||
queryset = Webhook.objects.all()
|
||||
form = forms.WebhookForm
|
||||
|
||||
|
||||
@register_model_view(Webhook, 'delete')
|
||||
class WebhookDeleteView(generic.ObjectDeleteView):
|
||||
queryset = Webhook.objects.all()
|
||||
|
||||
@@ -206,6 +218,7 @@ class TagListView(generic.ObjectListView):
|
||||
table = tables.TagTable
|
||||
|
||||
|
||||
@register_model_view(Tag)
|
||||
class TagView(generic.ObjectView):
|
||||
queryset = Tag.objects.all()
|
||||
|
||||
@@ -231,11 +244,13 @@ class TagView(generic.ObjectView):
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(Tag, 'edit')
|
||||
class TagEditView(generic.ObjectEditView):
|
||||
queryset = Tag.objects.all()
|
||||
form = forms.TagForm
|
||||
|
||||
|
||||
@register_model_view(Tag, 'delete')
|
||||
class TagDeleteView(generic.ObjectDeleteView):
|
||||
queryset = Tag.objects.all()
|
||||
|
||||
@@ -273,6 +288,7 @@ class ConfigContextListView(generic.ObjectListView):
|
||||
actions = ('add', 'bulk_edit', 'bulk_delete')
|
||||
|
||||
|
||||
@register_model_view(ConfigContext)
|
||||
class ConfigContextView(generic.ObjectView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
|
||||
@@ -310,6 +326,7 @@ class ConfigContextView(generic.ObjectView):
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ConfigContext, 'edit')
|
||||
class ConfigContextEditView(generic.ObjectEditView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
form = forms.ConfigContextForm
|
||||
@@ -322,6 +339,7 @@ class ConfigContextBulkEditView(generic.BulkEditView):
|
||||
form = forms.ConfigContextBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(ConfigContext, 'delete')
|
||||
class ConfigContextDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
|
||||
@@ -353,7 +371,6 @@ class ObjectConfigContextView(generic.ObjectView):
|
||||
'source_contexts': source_contexts,
|
||||
'format': format,
|
||||
'base_template': self.base_template,
|
||||
'active_tab': 'config-context',
|
||||
}
|
||||
|
||||
|
||||
@@ -370,6 +387,7 @@ class ObjectChangeListView(generic.ObjectListView):
|
||||
actions = ('export',)
|
||||
|
||||
|
||||
@register_model_view(ObjectChange)
|
||||
class ObjectChangeView(generic.ObjectView):
|
||||
queryset = ObjectChange.objects.all()
|
||||
|
||||
@@ -427,6 +445,7 @@ class ObjectChangeView(generic.ObjectView):
|
||||
# Image attachments
|
||||
#
|
||||
|
||||
@register_model_view(ImageAttachment, 'edit')
|
||||
class ImageAttachmentEditView(generic.ObjectEditView):
|
||||
queryset = ImageAttachment.objects.all()
|
||||
form = forms.ImageAttachmentForm
|
||||
@@ -449,6 +468,7 @@ class ImageAttachmentEditView(generic.ObjectEditView):
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ImageAttachment, 'delete')
|
||||
class ImageAttachmentDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ImageAttachment.objects.all()
|
||||
|
||||
@@ -468,10 +488,12 @@ class JournalEntryListView(generic.ObjectListView):
|
||||
actions = ('export', 'bulk_edit', 'bulk_delete')
|
||||
|
||||
|
||||
@register_model_view(JournalEntry)
|
||||
class JournalEntryView(generic.ObjectView):
|
||||
queryset = JournalEntry.objects.all()
|
||||
|
||||
|
||||
@register_model_view(JournalEntry, 'edit')
|
||||
class JournalEntryEditView(generic.ObjectEditView):
|
||||
queryset = JournalEntry.objects.all()
|
||||
form = forms.JournalEntryForm
|
||||
@@ -489,6 +511,7 @@ class JournalEntryEditView(generic.ObjectEditView):
|
||||
return reverse(viewname, kwargs={'pk': obj.pk})
|
||||
|
||||
|
||||
@register_model_view(JournalEntry, 'delete')
|
||||
class JournalEntryDeleteView(generic.ObjectDeleteView):
|
||||
queryset = JournalEntry.objects.all()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user