Drop CustomFieldValue

This commit is contained in:
Jeremy Stretch
2020-08-21 15:53:38 -04:00
parent 68cbcf20cf
commit 2e6fb7fcf5
20 changed files with 87 additions and 314 deletions

View File

@@ -8,7 +8,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.fields import CreateOnlyDefault
from extras.choices import *
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
from extras.models import CustomField, CustomFieldChoice
from utilities.api import ValidatedModelSerializer
@@ -164,15 +164,8 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
instance.custom_fields[field.name] = value
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():
custom_field = CustomField.objects.get(name=field_name)
CustomFieldValue.objects.update_or_create(
field=custom_field,
obj_type=content_type,
obj_id=instance.pk,
defaults={'serialized_value': custom_field.serialize_value(value)},
)
instance.custom_field_data[field_name] = value
def create(self, validated_data):

View File

@@ -93,10 +93,6 @@ class CustomFieldModelViewSet(ModelViewSet):
})
return context
def get_queryset(self):
# Prefetch custom field values
return super().get_queryset().prefetch_related('custom_field_values__field')
#
# Export templates

View File

@@ -12,7 +12,7 @@ from utilities.forms import (
)
from virtualization.models import Cluster, ClusterGroup
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
if self.instance.pk:
for cfv in CustomFieldValue.objects.filter(
obj_type=self.obj_type,
obj_id=self.instance.pk
).prefetch_related('field'):
self.custom_field_values[cfv.field.name] = cfv.serialized_value
self.custom_field_values = self.instance.custom_field_data
# Append form fields; assign initial values if modifying and existing object
for cf in CustomField.objects.filter(obj_type=self.obj_type):
@@ -64,23 +60,7 @@ class CustomFieldModelForm(forms.ModelForm):
def _save_custom_fields(self):
for field_name in self.custom_fields:
try:
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()
self.instance.custom_field_data[field_name[3:]] = self.cleaned_data[field_name]
def save(self, commit=True):

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

View File

@@ -1,5 +1,5 @@
from .change_logging import ChangeLoggedModel, ObjectChange
from .customfields import CustomField, CustomFieldChoice, CustomFieldModel, CustomFieldValue
from .customfields import CustomField, CustomFieldChoice, CustomFieldModel
from .models import (
ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, ImageAttachment, JobResult, Report, Script,
Webhook,
@@ -13,7 +13,6 @@ __all__ = (
'CustomField',
'CustomFieldChoice',
'CustomFieldModel',
'CustomFieldValue',
'CustomLink',
'ExportTemplate',
'ImageAttachment',

View File

@@ -1,8 +1,6 @@
from collections import OrderedDict
from datetime import date
from django import forms
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.validators import ValidationError
from django.db import models
@@ -29,36 +27,12 @@ class CustomFieldModel(models.Model):
self._cf = custom_fields
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
def cf(self):
"""
Name-based CustomFieldValue accessor for use in templates
Convenience wrapper for custom field data.
"""
if self._cf is None:
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])
return self.custom_field_data
class CustomFieldManager(models.Manager):
@@ -235,49 +209,6 @@ class CustomField(models.Model):
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):
field = models.ForeignKey(
to='extras.CustomField',
@@ -304,11 +235,13 @@ class CustomFieldChoice(models.Model):
if self.field.type != CustomFieldTypeChoices.TYPE_SELECT:
raise ValidationError("Custom field choices can only be assigned to selection fields.")
def delete(self, using=None, keep_parents=False):
# When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
pk = self.pk
super().delete(using, keep_parents)
CustomFieldValue.objects.filter(
field__type=CustomFieldTypeChoices.TYPE_SELECT,
serialized_value=str(pk)
).delete()
def delete(self, *args, **kwargs):
# TODO: Prevent deletion of CustomFieldChoices which are in use?
field_name = f'custom_field_data__{self.field.name}'
for ct in self.field.obj_type.all():
model = ct.model_class()
for instance in model.objects.filter(**{field_name: self.pk}):
instance.custom_field_data.pop(self.field.name)
instance.save()
super().delete(*args, **kwargs)

View File

@@ -1,26 +1,8 @@
from collections import OrderedDict
from django.db.models import Q, QuerySet
from django.db.models import Q
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):
def get_for_object(self, obj):

View File

@@ -5,7 +5,7 @@ from rest_framework import status
from dcim.choices import SiteStatusChoices
from dcim.models import Site
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.utils import post_data
from utilities.testing.views import ModelViewTestCase
@@ -93,16 +93,14 @@ class ChangeLogViewTest(ModelViewTestCase):
def test_delete_object(self):
site = Site(
name='Test Site 1',
slug='test-site-1'
slug='test-site-1',
custom_field_data={
'my_field': 'ABC'
}
)
site.save()
self.create_tags('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 = {
'path': self._get_url('delete', instance=site),
@@ -209,15 +207,13 @@ class ChangeLogAPITest(APITestCase):
def test_delete_object(self):
site = Site(
name='Test Site 1',
slug='test-site-1'
slug='test-site-1',
custom_field_data={
'my_field': 'ABC'
}
)
site.save()
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.add_permissions('dcim.delete_site')
url = reverse('dcim-api:site-detail', kwargs={'pk': site.pk})

View File

@@ -7,7 +7,7 @@ from rest_framework import status
from dcim.forms import SiteCSVForm
from dcim.models import Site
from extras.choices import *
from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
from extras.models import CustomField, CustomFieldChoice
from utilities.testing import APITestCase, TestCase
from virtualization.models import VirtualMachine
@@ -46,18 +46,18 @@ class CustomFieldTest(TestCase):
# Assign a value to the first Site
site = Site.objects.first()
cfv = CustomFieldValue(field=cf, obj_type=obj_type, obj_id=site.id)
cfv.value = data['field_value']
cfv.save()
site.custom_field_data[cf.name] = data['field_value']
site.save()
# Retrieve the stored value
cfv = CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site.pk).first()
self.assertEqual(cfv.value, data['field_value'])
site.refresh_from_db()
self.assertEqual(site.custom_field_data[cf.name], data['field_value'])
# Delete the stored value
cfv.value = data['empty_value']
cfv.save()
self.assertEqual(CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site.pk).count(), 0)
site.custom_field_data.pop(cf.name)
site.save()
site.refresh_from_db()
self.assertIsNone(site.custom_field_data.get(cf.name))
# Delete the custom field
cf.delete()
@@ -81,18 +81,18 @@ class CustomFieldTest(TestCase):
# Assign a value to the first Site
site = Site.objects.first()
cfv = CustomFieldValue(field=cf, obj_type=obj_type, obj_id=site.id)
cfv.value = cf.choices.first()
cfv.save()
site.custom_field_data[cf.name] = cf.choices.first().pk
site.save()
# Retrieve the stored value
cfv = CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site.pk).first()
self.assertEqual(str(cfv.value), 'Option A')
site.refresh_from_db()
self.assertEqual(site.custom_field_data[cf.name], 'Option A')
# Delete the stored value
cfv.value = None
cfv.save()
self.assertEqual(CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site.pk).count(), 0)
site.custom_field_data.pop(cf.name)
site.save()
site.refresh_from_db()
self.assertIsNone(site.custom_field_data.get(cf.name))
# Delete the custom field
cf.delete()
@@ -164,18 +164,15 @@ class CustomFieldAPITest(APITestCase):
Site.objects.bulk_create(cls.sites)
# Assign custom field values for site 2
site2_cfvs = {
cls.cf_text: 'bar',
cls.cf_integer: 456,
cls.cf_boolean: True,
cls.cf_date: '2020-01-02',
cls.cf_url: 'http://example.com/2',
cls.cf_select: cls.cf_select_choice2.pk,
cls.sites[1].custom_field_data = {
cls.cf_text.name: 'bar',
cls.cf_integer.name: 456,
cls.cf_boolean.name: True,
cls.cf_date.name: '2020-01-02',
cls.cf_url.name: 'http://example.com/2',
cls.cf_select.name: cls.cf_select_choice2.pk,
}
for field, value in site2_cfvs.items():
cfv = CustomFieldValue(field=field, obj=cls.sites[1])
cfv.value = value
cfv.save()
cls.sites[1].save()
def test_get_single_object_without_custom_field_values(self):
"""
@@ -518,7 +515,7 @@ class CustomFieldImportTest(TestCase):
# Validate data for site 1
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(custom_field_values['text'], 'ABC')
@@ -530,7 +527,7 @@ class CustomFieldImportTest(TestCase):
# Validate data for site 2
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(custom_field_values['text'], 'DEF')
@@ -543,8 +540,7 @@ class CustomFieldImportTest(TestCase):
# No CustomFieldValues should be created for site 3
obj_type = ContentType.objects.get_for_model(Site)
site3 = Site.objects.get(name='Site 3')
self.assertFalse(CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site3.pk).exists())
self.assertEqual(CustomFieldValue.objects.count(), 12) # Sanity check
self.assertEqual(site3.custom_field_data, {})
def test_import_missing_required(self):
"""