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
<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

View File

@ -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)

View File

@ -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'
) )

View File

@ -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()

View File

@ -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'
) )

View File

@ -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'
) )

View File

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

View File

@ -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

View File

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

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 .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',

View File

@ -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)

View File

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

View File

@ -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})

View File

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

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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'])

View File

@ -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()