1
0
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:
Jeremy Stretch
2020-08-21 15:53:38 -04:00
parent 879166d939
commit 2276603ac3
20 changed files with 87 additions and 314 deletions

View File

@ -185,7 +185,7 @@ To delete an object, simply call `delete()` on its instance. This will return a
>>> vlan
<VLAN: 123 (BetterName)>
>>> 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.
@ -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()
27
>>> Device.objects.filter(name__icontains='test').delete()
(35, {'extras.CustomFieldValue': 0, 'dcim.DeviceBay': 0, 'secrets.Secret': 0,
'dcim.InterfaceConnection': 4, 'extras.ImageAttachment': 0, 'dcim.Device': 27,
'dcim.Interface': 4, 'dcim.ConsolePort': 0, 'dcim.PowerPort': 0})
(35, {'dcim.DeviceBay': 0, 'secrets.Secret': 0, 'dcim.InterfaceConnection': 4,
'extras.ImageAttachment': 0, 'dcim.Device': 27, 'dcim.Interface': 4,
'dcim.ConsolePort': 0, 'dcim.PowerPort': 0})
```
!!! warning

View File

@ -1,4 +1,3 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.urls import reverse
from taggit.managers import TaggableManager
@ -61,11 +60,6 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
comments = models.TextField(
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()
@ -186,11 +180,6 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
comments = models.TextField(
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
objects = CircuitQuerySet.as_manager()
tags = TaggableManager(through=TaggedItem)

View File

@ -134,11 +134,6 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
comments = models.TextField(
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()
@ -584,11 +579,6 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
comments = models.TextField(
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
images = GenericRelation(
to='extras.ImageAttachment'
)

View File

@ -1,4 +1,3 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
@ -144,11 +143,6 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
comments = models.TextField(
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()

View File

@ -261,11 +261,6 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
comments = models.TextField(
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
images = GenericRelation(
to='extras.ImageAttachment'
)

View File

@ -183,11 +183,6 @@ class Site(ChangeLoggedModel, CustomFieldModel):
comments = models.TextField(
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
images = GenericRelation(
to='extras.ImageAttachment'
)

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):
"""

View File

@ -1,6 +1,6 @@
import netaddr
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.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
@ -70,11 +70,6 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
max_length=200,
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()
@ -178,11 +173,6 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
max_length=200,
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()
@ -364,11 +354,6 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
max_length=200,
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
objects = PrefixQuerySet.as_manager()
@ -647,11 +632,6 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
max_length=200,
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
objects = IPAddressManager()
@ -928,11 +908,6 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
max_length=200,
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()
@ -1043,11 +1018,6 @@ class Service(ChangeLoggedModel, CustomFieldModel):
max_length=200,
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()

View File

@ -6,7 +6,6 @@ from Crypto.Util import strxor
from django.conf import settings
from django.contrib.auth.hashers import make_password, check_password
from django.contrib.auth.models import Group, User
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
@ -306,11 +305,6 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
max_length=128,
editable=False
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()

View File

@ -1,4 +1,3 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey
@ -102,11 +101,6 @@ class Tenant(ChangeLoggedModel, CustomFieldModel):
comments = models.TextField(
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()

View File

@ -26,8 +26,7 @@ from django.views.defaults import ERROR_500_TEMPLATE_NAME
from django.views.generic import View
from django_tables2 import RequestConfig
from extras.models import CustomField, CustomFieldValue, ExportTemplate
from extras.querysets import CustomFieldQueryset
from extras.models import CustomField, ExportTemplate
from utilities.exceptions import AbortTransaction
from utilities.forms import BootstrapMixin, BulkRenameForm, CSVDataField, TableConfigForm, restrict_form_fields
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()
# Add custom field headers, if any
if hasattr(self.queryset.model, 'get_custom_fields'):
for custom_field in self.queryset.model().get_custom_fields():
if hasattr(self.queryset.model, 'custom_field_data'):
for custom_field in CustomField.objects.get_for_model(self.queryset.model):
headers.append(custom_field.name)
custom_fields.append(custom_field.name)
@ -255,19 +254,11 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
if self.filterset:
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
if 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:
return et.render_to_response(queryset)
return et.render_to_response(self.queryset)
except Exception as e:
messages.error(
request,
@ -949,38 +940,18 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
elif form.cleaned_data[name] not in (None, ''):
setattr(obj, name, form.cleaned_data[name])
# Cache custom fields on instance prior to save()
if custom_fields:
obj._cf = {
name: form.cleaned_data[name] for name in custom_fields
}
# Update custom fields
for name in custom_fields:
if name in form.nullable_fields and name in nullified_fields:
obj.custom_field_data.pop(name, None)
else:
obj.custom_field_data[name] = form.cleaned_data[name]
obj.full_clean()
obj.save()
updated_objects.append(obj)
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
if form.cleaned_data.get('add_tags', None):
obj.tags.add(*form.cleaned_data['add_tags'])

View File

@ -150,11 +150,6 @@ class Cluster(ChangeLoggedModel, CustomFieldModel):
comments = models.TextField(
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()
@ -275,11 +270,6 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
comments = models.TextField(
blank=True
)
custom_field_values = GenericRelation(
to='extras.CustomFieldValue',
content_type_field='obj_type',
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()