mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Drop CustomFieldValue
This commit is contained in:
@ -185,7 +185,7 @@ To delete an object, simply call `delete()` on its instance. This will return a
|
|||||||
>>> vlan
|
>>> vlan
|
||||||
<VLAN: 123 (BetterName)>
|
<VLAN: 123 (BetterName)>
|
||||||
>>> vlan.delete()
|
>>> vlan.delete()
|
||||||
(1, {'extras.CustomFieldValue': 0, 'ipam.VLAN': 1})
|
(1, {'ipam.VLAN': 1})
|
||||||
```
|
```
|
||||||
|
|
||||||
To delete multiple objects at once, call `delete()` on a filtered queryset. It's a good idea to always sanity-check the count of selected objects _before_ deleting them.
|
To delete multiple objects at once, call `delete()` on a filtered queryset. It's a good idea to always sanity-check the count of selected objects _before_ deleting them.
|
||||||
@ -194,9 +194,9 @@ To delete multiple objects at once, call `delete()` on a filtered queryset. It's
|
|||||||
>>> Device.objects.filter(name__icontains='test').count()
|
>>> Device.objects.filter(name__icontains='test').count()
|
||||||
27
|
27
|
||||||
>>> Device.objects.filter(name__icontains='test').delete()
|
>>> Device.objects.filter(name__icontains='test').delete()
|
||||||
(35, {'extras.CustomFieldValue': 0, 'dcim.DeviceBay': 0, 'secrets.Secret': 0,
|
(35, {'dcim.DeviceBay': 0, 'secrets.Secret': 0, 'dcim.InterfaceConnection': 4,
|
||||||
'dcim.InterfaceConnection': 4, 'extras.ImageAttachment': 0, 'dcim.Device': 27,
|
'extras.ImageAttachment': 0, 'dcim.Device': 27, 'dcim.Interface': 4,
|
||||||
'dcim.Interface': 4, 'dcim.ConsolePort': 0, 'dcim.PowerPort': 0})
|
'dcim.ConsolePort': 0, 'dcim.PowerPort': 0})
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from django.contrib.contenttypes.fields import GenericRelation
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
@ -61,11 +60,6 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
|
|||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
@ -186,11 +180,6 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
|
|||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
|
|
||||||
objects = CircuitQuerySet.as_manager()
|
objects = CircuitQuerySet.as_manager()
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
@ -134,11 +134,6 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
|||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
@ -584,11 +579,6 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
images = GenericRelation(
|
images = GenericRelation(
|
||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment'
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from django.contrib.contenttypes.fields import GenericRelation
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -144,11 +143,6 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
|
|||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
@ -261,11 +261,6 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
|||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
images = GenericRelation(
|
images = GenericRelation(
|
||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment'
|
||||||
)
|
)
|
||||||
|
@ -183,11 +183,6 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
|||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
images = GenericRelation(
|
images = GenericRelation(
|
||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment'
|
||||||
)
|
)
|
||||||
|
@ -8,7 +8,7 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from rest_framework.fields import CreateOnlyDefault
|
from rest_framework.fields import CreateOnlyDefault
|
||||||
|
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
|
from extras.models import CustomField, CustomFieldChoice
|
||||||
from utilities.api import ValidatedModelSerializer
|
from utilities.api import ValidatedModelSerializer
|
||||||
|
|
||||||
|
|
||||||
@ -164,15 +164,8 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
|||||||
instance.custom_fields[field.name] = value
|
instance.custom_fields[field.name] = value
|
||||||
|
|
||||||
def _save_custom_fields(self, instance, custom_fields):
|
def _save_custom_fields(self, instance, custom_fields):
|
||||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
|
||||||
for field_name, value in custom_fields.items():
|
for field_name, value in custom_fields.items():
|
||||||
custom_field = CustomField.objects.get(name=field_name)
|
instance.custom_field_data[field_name] = value
|
||||||
CustomFieldValue.objects.update_or_create(
|
|
||||||
field=custom_field,
|
|
||||||
obj_type=content_type,
|
|
||||||
obj_id=instance.pk,
|
|
||||||
defaults={'serialized_value': custom_field.serialize_value(value)},
|
|
||||||
)
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
|
|
||||||
|
@ -93,10 +93,6 @@ class CustomFieldModelViewSet(ModelViewSet):
|
|||||||
})
|
})
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
# Prefetch custom field values
|
|
||||||
return super().get_queryset().prefetch_related('custom_field_values__field')
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Export templates
|
# Export templates
|
||||||
|
@ -12,7 +12,7 @@ from utilities.forms import (
|
|||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
|
from .models import ConfigContext, CustomField, ImageAttachment, ObjectChange, Tag
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -40,11 +40,7 @@ class CustomFieldModelForm(forms.ModelForm):
|
|||||||
"""
|
"""
|
||||||
# Retrieve initial CustomField values for the instance
|
# Retrieve initial CustomField values for the instance
|
||||||
if self.instance.pk:
|
if self.instance.pk:
|
||||||
for cfv in CustomFieldValue.objects.filter(
|
self.custom_field_values = self.instance.custom_field_data
|
||||||
obj_type=self.obj_type,
|
|
||||||
obj_id=self.instance.pk
|
|
||||||
).prefetch_related('field'):
|
|
||||||
self.custom_field_values[cfv.field.name] = cfv.serialized_value
|
|
||||||
|
|
||||||
# Append form fields; assign initial values if modifying and existing object
|
# Append form fields; assign initial values if modifying and existing object
|
||||||
for cf in CustomField.objects.filter(obj_type=self.obj_type):
|
for cf in CustomField.objects.filter(obj_type=self.obj_type):
|
||||||
@ -64,23 +60,7 @@ class CustomFieldModelForm(forms.ModelForm):
|
|||||||
def _save_custom_fields(self):
|
def _save_custom_fields(self):
|
||||||
|
|
||||||
for field_name in self.custom_fields:
|
for field_name in self.custom_fields:
|
||||||
try:
|
self.instance.custom_field_data[field_name[3:]] = self.cleaned_data[field_name]
|
||||||
cfv = CustomFieldValue.objects.prefetch_related('field').get(
|
|
||||||
field=self.fields[field_name].model,
|
|
||||||
obj_type=self.obj_type,
|
|
||||||
obj_id=self.instance.pk
|
|
||||||
)
|
|
||||||
except CustomFieldValue.DoesNotExist:
|
|
||||||
# Skip this field if none exists already and its value is empty
|
|
||||||
if self.cleaned_data[field_name] in [None, '']:
|
|
||||||
continue
|
|
||||||
cfv = CustomFieldValue(
|
|
||||||
field=self.fields[field_name].model,
|
|
||||||
obj_type=self.obj_type,
|
|
||||||
obj_id=self.instance.pk
|
|
||||||
)
|
|
||||||
cfv.value = self.cleaned_data[field_name]
|
|
||||||
cfv.save()
|
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
|
|
||||||
|
16
netbox/extras/migrations/0051_delete_customfieldvalue.py
Normal file
16
netbox/extras/migrations/0051_delete_customfieldvalue.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Generated by Django 3.1 on 2020-08-21 19:52
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0050_migrate_customfieldvalues'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='CustomFieldValue',
|
||||||
|
),
|
||||||
|
]
|
@ -1,5 +1,5 @@
|
|||||||
from .change_logging import ChangeLoggedModel, ObjectChange
|
from .change_logging import ChangeLoggedModel, ObjectChange
|
||||||
from .customfields import CustomField, CustomFieldChoice, CustomFieldModel, CustomFieldValue
|
from .customfields import CustomField, CustomFieldChoice, CustomFieldModel
|
||||||
from .models import (
|
from .models import (
|
||||||
ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, ImageAttachment, JobResult, Report, Script,
|
ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, ImageAttachment, JobResult, Report, Script,
|
||||||
Webhook,
|
Webhook,
|
||||||
@ -13,7 +13,6 @@ __all__ = (
|
|||||||
'CustomField',
|
'CustomField',
|
||||||
'CustomFieldChoice',
|
'CustomFieldChoice',
|
||||||
'CustomFieldModel',
|
'CustomFieldModel',
|
||||||
'CustomFieldValue',
|
|
||||||
'CustomLink',
|
'CustomLink',
|
||||||
'ExportTemplate',
|
'ExportTemplate',
|
||||||
'ImageAttachment',
|
'ImageAttachment',
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
from collections import OrderedDict
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.validators import ValidationError
|
from django.core.validators import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -29,36 +27,12 @@ class CustomFieldModel(models.Model):
|
|||||||
self._cf = custom_fields
|
self._cf = custom_fields
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def cache_custom_fields(self):
|
|
||||||
"""
|
|
||||||
Cache all custom field values for this instance
|
|
||||||
"""
|
|
||||||
self._cf = {
|
|
||||||
field.name: value for field, value in self.get_custom_fields().items()
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cf(self):
|
def cf(self):
|
||||||
"""
|
"""
|
||||||
Name-based CustomFieldValue accessor for use in templates
|
Convenience wrapper for custom field data.
|
||||||
"""
|
"""
|
||||||
if self._cf is None:
|
return self.custom_field_data
|
||||||
self.cache_custom_fields()
|
|
||||||
return self._cf
|
|
||||||
|
|
||||||
def get_custom_fields(self):
|
|
||||||
"""
|
|
||||||
Return a dictionary of custom fields for a single object in the form {<field>: value}.
|
|
||||||
"""
|
|
||||||
fields = CustomField.objects.get_for_model(self)
|
|
||||||
|
|
||||||
# If the object exists, populate its custom fields with values
|
|
||||||
if hasattr(self, 'pk'):
|
|
||||||
values = self.custom_field_values.all()
|
|
||||||
values_dict = {cfv.field_id: cfv.value for cfv in values}
|
|
||||||
return OrderedDict([(field, values_dict.get(field.pk)) for field in fields])
|
|
||||||
else:
|
|
||||||
return OrderedDict([(field, None) for field in fields])
|
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldManager(models.Manager):
|
class CustomFieldManager(models.Manager):
|
||||||
@ -235,49 +209,6 @@ class CustomField(models.Model):
|
|||||||
return field
|
return field
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldValue(models.Model):
|
|
||||||
field = models.ForeignKey(
|
|
||||||
to='extras.CustomField',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='values'
|
|
||||||
)
|
|
||||||
obj_type = models.ForeignKey(
|
|
||||||
to=ContentType,
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
related_name='+'
|
|
||||||
)
|
|
||||||
obj_id = models.PositiveIntegerField()
|
|
||||||
obj = GenericForeignKey(
|
|
||||||
ct_field='obj_type',
|
|
||||||
fk_field='obj_id'
|
|
||||||
)
|
|
||||||
serialized_value = models.CharField(
|
|
||||||
max_length=255
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ('obj_type', 'obj_id', 'pk') # (obj_type, obj_id) may be non-unique
|
|
||||||
unique_together = ('field', 'obj_type', 'obj_id')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return '{} {}'.format(self.obj, self.field)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
return self.field.deserialize_value(self.serialized_value)
|
|
||||||
|
|
||||||
@value.setter
|
|
||||||
def value(self, value):
|
|
||||||
self.serialized_value = self.field.serialize_value(value)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
# Delete this object if it no longer has a value to store
|
|
||||||
if self.pk and self.value is None:
|
|
||||||
self.delete()
|
|
||||||
else:
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldChoice(models.Model):
|
class CustomFieldChoice(models.Model):
|
||||||
field = models.ForeignKey(
|
field = models.ForeignKey(
|
||||||
to='extras.CustomField',
|
to='extras.CustomField',
|
||||||
@ -304,11 +235,13 @@ class CustomFieldChoice(models.Model):
|
|||||||
if self.field.type != CustomFieldTypeChoices.TYPE_SELECT:
|
if self.field.type != CustomFieldTypeChoices.TYPE_SELECT:
|
||||||
raise ValidationError("Custom field choices can only be assigned to selection fields.")
|
raise ValidationError("Custom field choices can only be assigned to selection fields.")
|
||||||
|
|
||||||
def delete(self, using=None, keep_parents=False):
|
def delete(self, *args, **kwargs):
|
||||||
# When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
|
# TODO: Prevent deletion of CustomFieldChoices which are in use?
|
||||||
pk = self.pk
|
field_name = f'custom_field_data__{self.field.name}'
|
||||||
super().delete(using, keep_parents)
|
for ct in self.field.obj_type.all():
|
||||||
CustomFieldValue.objects.filter(
|
model = ct.model_class()
|
||||||
field__type=CustomFieldTypeChoices.TYPE_SELECT,
|
for instance in model.objects.filter(**{field_name: self.pk}):
|
||||||
serialized_value=str(pk)
|
instance.custom_field_data.pop(self.field.name)
|
||||||
).delete()
|
instance.save()
|
||||||
|
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
@ -1,26 +1,8 @@
|
|||||||
from collections import OrderedDict
|
from django.db.models import Q
|
||||||
|
|
||||||
from django.db.models import Q, QuerySet
|
|
||||||
|
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldQueryset:
|
|
||||||
"""
|
|
||||||
Annotate custom fields on objects within a QuerySet.
|
|
||||||
"""
|
|
||||||
def __init__(self, queryset, custom_fields):
|
|
||||||
self.queryset = queryset
|
|
||||||
self.model = queryset.model
|
|
||||||
self.custom_fields = custom_fields
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
for obj in self.queryset:
|
|
||||||
values_dict = {cfv.field_id: cfv.value for cfv in obj.custom_field_values.all()}
|
|
||||||
obj.custom_fields = OrderedDict([(field, values_dict.get(field.pk)) for field in self.custom_fields])
|
|
||||||
yield obj
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextQuerySet(RestrictedQuerySet):
|
class ConfigContextQuerySet(RestrictedQuerySet):
|
||||||
|
|
||||||
def get_for_object(self, obj):
|
def get_for_object(self, obj):
|
||||||
|
@ -5,7 +5,7 @@ from rest_framework import status
|
|||||||
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 *
|
||||||
from extras.models import CustomField, CustomFieldValue, ObjectChange, Tag
|
from extras.models import CustomField, ObjectChange, Tag
|
||||||
from utilities.testing import APITestCase
|
from utilities.testing import APITestCase
|
||||||
from utilities.testing.utils import post_data
|
from utilities.testing.utils import post_data
|
||||||
from utilities.testing.views import ModelViewTestCase
|
from utilities.testing.views import ModelViewTestCase
|
||||||
@ -93,16 +93,14 @@ class ChangeLogViewTest(ModelViewTestCase):
|
|||||||
def test_delete_object(self):
|
def test_delete_object(self):
|
||||||
site = Site(
|
site = Site(
|
||||||
name='Test Site 1',
|
name='Test Site 1',
|
||||||
slug='test-site-1'
|
slug='test-site-1',
|
||||||
|
custom_field_data={
|
||||||
|
'my_field': 'ABC'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
site.save()
|
site.save()
|
||||||
self.create_tags('Tag 1', 'Tag 2')
|
self.create_tags('Tag 1', 'Tag 2')
|
||||||
site.tags.set('Tag 1', 'Tag 2')
|
site.tags.set('Tag 1', 'Tag 2')
|
||||||
CustomFieldValue.objects.create(
|
|
||||||
field=CustomField.objects.get(name='my_field'),
|
|
||||||
obj=site,
|
|
||||||
value='ABC'
|
|
||||||
)
|
|
||||||
|
|
||||||
request = {
|
request = {
|
||||||
'path': self._get_url('delete', instance=site),
|
'path': self._get_url('delete', instance=site),
|
||||||
@ -209,15 +207,13 @@ class ChangeLogAPITest(APITestCase):
|
|||||||
def test_delete_object(self):
|
def test_delete_object(self):
|
||||||
site = Site(
|
site = Site(
|
||||||
name='Test Site 1',
|
name='Test Site 1',
|
||||||
slug='test-site-1'
|
slug='test-site-1',
|
||||||
|
custom_field_data={
|
||||||
|
'my_field': 'ABC'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
site.save()
|
site.save()
|
||||||
site.tags.set(*Tag.objects.all()[:2])
|
site.tags.set(*Tag.objects.all()[:2])
|
||||||
CustomFieldValue.objects.create(
|
|
||||||
field=CustomField.objects.get(name='my_field'),
|
|
||||||
obj=site,
|
|
||||||
value='ABC'
|
|
||||||
)
|
|
||||||
self.assertEqual(ObjectChange.objects.count(), 0)
|
self.assertEqual(ObjectChange.objects.count(), 0)
|
||||||
self.add_permissions('dcim.delete_site')
|
self.add_permissions('dcim.delete_site')
|
||||||
url = reverse('dcim-api:site-detail', kwargs={'pk': site.pk})
|
url = reverse('dcim-api:site-detail', kwargs={'pk': site.pk})
|
||||||
|
@ -7,7 +7,7 @@ from rest_framework import status
|
|||||||
from dcim.forms import SiteCSVForm
|
from dcim.forms import SiteCSVForm
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
|
from extras.models import CustomField, CustomFieldChoice
|
||||||
from utilities.testing import APITestCase, TestCase
|
from utilities.testing import APITestCase, TestCase
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
|
|
||||||
@ -46,18 +46,18 @@ class CustomFieldTest(TestCase):
|
|||||||
|
|
||||||
# Assign a value to the first Site
|
# Assign a value to the first Site
|
||||||
site = Site.objects.first()
|
site = Site.objects.first()
|
||||||
cfv = CustomFieldValue(field=cf, obj_type=obj_type, obj_id=site.id)
|
site.custom_field_data[cf.name] = data['field_value']
|
||||||
cfv.value = data['field_value']
|
site.save()
|
||||||
cfv.save()
|
|
||||||
|
|
||||||
# Retrieve the stored value
|
# Retrieve the stored value
|
||||||
cfv = CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site.pk).first()
|
site.refresh_from_db()
|
||||||
self.assertEqual(cfv.value, data['field_value'])
|
self.assertEqual(site.custom_field_data[cf.name], data['field_value'])
|
||||||
|
|
||||||
# Delete the stored value
|
# Delete the stored value
|
||||||
cfv.value = data['empty_value']
|
site.custom_field_data.pop(cf.name)
|
||||||
cfv.save()
|
site.save()
|
||||||
self.assertEqual(CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site.pk).count(), 0)
|
site.refresh_from_db()
|
||||||
|
self.assertIsNone(site.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
# Delete the custom field
|
# Delete the custom field
|
||||||
cf.delete()
|
cf.delete()
|
||||||
@ -81,18 +81,18 @@ class CustomFieldTest(TestCase):
|
|||||||
|
|
||||||
# Assign a value to the first Site
|
# Assign a value to the first Site
|
||||||
site = Site.objects.first()
|
site = Site.objects.first()
|
||||||
cfv = CustomFieldValue(field=cf, obj_type=obj_type, obj_id=site.id)
|
site.custom_field_data[cf.name] = cf.choices.first().pk
|
||||||
cfv.value = cf.choices.first()
|
site.save()
|
||||||
cfv.save()
|
|
||||||
|
|
||||||
# Retrieve the stored value
|
# Retrieve the stored value
|
||||||
cfv = CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site.pk).first()
|
site.refresh_from_db()
|
||||||
self.assertEqual(str(cfv.value), 'Option A')
|
self.assertEqual(site.custom_field_data[cf.name], 'Option A')
|
||||||
|
|
||||||
# Delete the stored value
|
# Delete the stored value
|
||||||
cfv.value = None
|
site.custom_field_data.pop(cf.name)
|
||||||
cfv.save()
|
site.save()
|
||||||
self.assertEqual(CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site.pk).count(), 0)
|
site.refresh_from_db()
|
||||||
|
self.assertIsNone(site.custom_field_data.get(cf.name))
|
||||||
|
|
||||||
# Delete the custom field
|
# Delete the custom field
|
||||||
cf.delete()
|
cf.delete()
|
||||||
@ -164,18 +164,15 @@ class CustomFieldAPITest(APITestCase):
|
|||||||
Site.objects.bulk_create(cls.sites)
|
Site.objects.bulk_create(cls.sites)
|
||||||
|
|
||||||
# Assign custom field values for site 2
|
# Assign custom field values for site 2
|
||||||
site2_cfvs = {
|
cls.sites[1].custom_field_data = {
|
||||||
cls.cf_text: 'bar',
|
cls.cf_text.name: 'bar',
|
||||||
cls.cf_integer: 456,
|
cls.cf_integer.name: 456,
|
||||||
cls.cf_boolean: True,
|
cls.cf_boolean.name: True,
|
||||||
cls.cf_date: '2020-01-02',
|
cls.cf_date.name: '2020-01-02',
|
||||||
cls.cf_url: 'http://example.com/2',
|
cls.cf_url.name: 'http://example.com/2',
|
||||||
cls.cf_select: cls.cf_select_choice2.pk,
|
cls.cf_select.name: cls.cf_select_choice2.pk,
|
||||||
}
|
}
|
||||||
for field, value in site2_cfvs.items():
|
cls.sites[1].save()
|
||||||
cfv = CustomFieldValue(field=field, obj=cls.sites[1])
|
|
||||||
cfv.value = value
|
|
||||||
cfv.save()
|
|
||||||
|
|
||||||
def test_get_single_object_without_custom_field_values(self):
|
def test_get_single_object_without_custom_field_values(self):
|
||||||
"""
|
"""
|
||||||
@ -518,7 +515,7 @@ class CustomFieldImportTest(TestCase):
|
|||||||
|
|
||||||
# Validate data for site 1
|
# Validate data for site 1
|
||||||
custom_field_values = {
|
custom_field_values = {
|
||||||
cf.name: value for cf, value in Site.objects.get(name='Site 1').get_custom_fields().items()
|
cf.name: value for cf, value in Site.objects.get(name='Site 1').custom_field_data
|
||||||
}
|
}
|
||||||
self.assertEqual(len(custom_field_values), 6)
|
self.assertEqual(len(custom_field_values), 6)
|
||||||
self.assertEqual(custom_field_values['text'], 'ABC')
|
self.assertEqual(custom_field_values['text'], 'ABC')
|
||||||
@ -530,7 +527,7 @@ class CustomFieldImportTest(TestCase):
|
|||||||
|
|
||||||
# Validate data for site 2
|
# Validate data for site 2
|
||||||
custom_field_values = {
|
custom_field_values = {
|
||||||
cf.name: value for cf, value in Site.objects.get(name='Site 2').get_custom_fields().items()
|
cf.name: value for cf, value in Site.objects.get(name='Site 2').custom_field_data
|
||||||
}
|
}
|
||||||
self.assertEqual(len(custom_field_values), 6)
|
self.assertEqual(len(custom_field_values), 6)
|
||||||
self.assertEqual(custom_field_values['text'], 'DEF')
|
self.assertEqual(custom_field_values['text'], 'DEF')
|
||||||
@ -543,8 +540,7 @@ class CustomFieldImportTest(TestCase):
|
|||||||
# No CustomFieldValues should be created for site 3
|
# No CustomFieldValues should be created for site 3
|
||||||
obj_type = ContentType.objects.get_for_model(Site)
|
obj_type = ContentType.objects.get_for_model(Site)
|
||||||
site3 = Site.objects.get(name='Site 3')
|
site3 = Site.objects.get(name='Site 3')
|
||||||
self.assertFalse(CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site3.pk).exists())
|
self.assertEqual(site3.custom_field_data, {})
|
||||||
self.assertEqual(CustomFieldValue.objects.count(), 12) # Sanity check
|
|
||||||
|
|
||||||
def test_import_missing_required(self):
|
def test_import_missing_required(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import netaddr
|
import netaddr
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
@ -70,11 +70,6 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
|
|||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
@ -178,11 +173,6 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
|
|||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
@ -364,11 +354,6 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
|
|||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = PrefixQuerySet.as_manager()
|
objects = PrefixQuerySet.as_manager()
|
||||||
@ -647,11 +632,6 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = IPAddressManager()
|
objects = IPAddressManager()
|
||||||
@ -928,11 +908,6 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
|
|||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
@ -1043,11 +1018,6 @@ class Service(ChangeLoggedModel, CustomFieldModel):
|
|||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
@ -6,7 +6,6 @@ from Crypto.Util import strxor
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import make_password, check_password
|
from django.contrib.auth.hashers import make_password, check_password
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -306,11 +305,6 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
|
|||||||
max_length=128,
|
max_length=128,
|
||||||
editable=False
|
editable=False
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from django.contrib.contenttypes.fields import GenericRelation
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
@ -102,11 +101,6 @@ class Tenant(ChangeLoggedModel, CustomFieldModel):
|
|||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
@ -26,8 +26,7 @@ from django.views.defaults import ERROR_500_TEMPLATE_NAME
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
|
|
||||||
from extras.models import CustomField, CustomFieldValue, ExportTemplate
|
from extras.models import CustomField, ExportTemplate
|
||||||
from extras.querysets import CustomFieldQueryset
|
|
||||||
from utilities.exceptions import AbortTransaction
|
from utilities.exceptions import AbortTransaction
|
||||||
from utilities.forms import BootstrapMixin, BulkRenameForm, CSVDataField, TableConfigForm, restrict_form_fields
|
from utilities.forms import BootstrapMixin, BulkRenameForm, CSVDataField, TableConfigForm, restrict_form_fields
|
||||||
from utilities.permissions import get_permission_for_model, resolve_permission
|
from utilities.permissions import get_permission_for_model, resolve_permission
|
||||||
@ -229,8 +228,8 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|||||||
headers = self.queryset.model.csv_headers.copy()
|
headers = self.queryset.model.csv_headers.copy()
|
||||||
|
|
||||||
# Add custom field headers, if any
|
# Add custom field headers, if any
|
||||||
if hasattr(self.queryset.model, 'get_custom_fields'):
|
if hasattr(self.queryset.model, 'custom_field_data'):
|
||||||
for custom_field in self.queryset.model().get_custom_fields():
|
for custom_field in CustomField.objects.get_for_model(self.queryset.model):
|
||||||
headers.append(custom_field.name)
|
headers.append(custom_field.name)
|
||||||
custom_fields.append(custom_field.name)
|
custom_fields.append(custom_field.name)
|
||||||
|
|
||||||
@ -255,19 +254,11 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|||||||
if self.filterset:
|
if self.filterset:
|
||||||
self.queryset = self.filterset(request.GET, self.queryset).qs
|
self.queryset = self.filterset(request.GET, self.queryset).qs
|
||||||
|
|
||||||
# If this type of object has one or more custom fields, prefetch any relevant custom field values
|
|
||||||
custom_fields = CustomField.objects.filter(
|
|
||||||
obj_type=ContentType.objects.get_for_model(model)
|
|
||||||
).prefetch_related('choices')
|
|
||||||
if custom_fields:
|
|
||||||
self.queryset = self.queryset.prefetch_related('custom_field_values')
|
|
||||||
|
|
||||||
# Check for export template rendering
|
# Check for export template rendering
|
||||||
if request.GET.get('export'):
|
if request.GET.get('export'):
|
||||||
et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET.get('export'))
|
et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET.get('export'))
|
||||||
queryset = CustomFieldQueryset(self.queryset, custom_fields) if custom_fields else self.queryset
|
|
||||||
try:
|
try:
|
||||||
return et.render_to_response(queryset)
|
return et.render_to_response(self.queryset)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messages.error(
|
messages.error(
|
||||||
request,
|
request,
|
||||||
@ -949,38 +940,18 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
elif form.cleaned_data[name] not in (None, ''):
|
elif form.cleaned_data[name] not in (None, ''):
|
||||||
setattr(obj, name, form.cleaned_data[name])
|
setattr(obj, name, form.cleaned_data[name])
|
||||||
|
|
||||||
# Cache custom fields on instance prior to save()
|
# Update custom fields
|
||||||
if custom_fields:
|
for name in custom_fields:
|
||||||
obj._cf = {
|
if name in form.nullable_fields and name in nullified_fields:
|
||||||
name: form.cleaned_data[name] for name in custom_fields
|
obj.custom_field_data.pop(name, None)
|
||||||
}
|
else:
|
||||||
|
obj.custom_field_data[name] = form.cleaned_data[name]
|
||||||
|
|
||||||
obj.full_clean()
|
obj.full_clean()
|
||||||
obj.save()
|
obj.save()
|
||||||
updated_objects.append(obj)
|
updated_objects.append(obj)
|
||||||
logger.debug(f"Saved {obj} (PK: {obj.pk})")
|
logger.debug(f"Saved {obj} (PK: {obj.pk})")
|
||||||
|
|
||||||
# Update custom fields
|
|
||||||
obj_type = ContentType.objects.get_for_model(model)
|
|
||||||
for name in custom_fields:
|
|
||||||
field = form.fields[name].model
|
|
||||||
if name in form.nullable_fields and name in nullified_fields:
|
|
||||||
CustomFieldValue.objects.filter(
|
|
||||||
field=field, obj_type=obj_type, obj_id=obj.pk
|
|
||||||
).delete()
|
|
||||||
elif form.cleaned_data[name] not in [None, '']:
|
|
||||||
try:
|
|
||||||
cfv = CustomFieldValue.objects.get(
|
|
||||||
field=field, obj_type=obj_type, obj_id=obj.pk
|
|
||||||
)
|
|
||||||
except CustomFieldValue.DoesNotExist:
|
|
||||||
cfv = CustomFieldValue(
|
|
||||||
field=field, obj_type=obj_type, obj_id=obj.pk
|
|
||||||
)
|
|
||||||
cfv.value = form.cleaned_data[name]
|
|
||||||
cfv.save()
|
|
||||||
logger.debug(f"Saved custom fields for {obj} (PK: {obj.pk})")
|
|
||||||
|
|
||||||
# Add/remove tags
|
# Add/remove tags
|
||||||
if form.cleaned_data.get('add_tags', None):
|
if form.cleaned_data.get('add_tags', None):
|
||||||
obj.tags.add(*form.cleaned_data['add_tags'])
|
obj.tags.add(*form.cleaned_data['add_tags'])
|
||||||
|
@ -150,11 +150,6 @@ class Cluster(ChangeLoggedModel, CustomFieldModel):
|
|||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
@ -275,11 +270,6 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
comments = models.TextField(
|
comments = models.TextField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
Reference in New Issue
Block a user