diff --git a/docs/administration/netbox-shell.md b/docs/administration/netbox-shell.md index 51a06156a..2a8d31494 100644 --- a/docs/administration/netbox-shell.md +++ b/docs/administration/netbox-shell.md @@ -185,7 +185,7 @@ To delete an object, simply call `delete()` on its instance. This will return a >>> vlan >>> 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 diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 14f555cc6..fbe568f18 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -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) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 8bb56101e..b10fd6b74 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -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' ) diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index f760fea13..08ae194ae 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -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() diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 3169272b4..e30481cd6 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -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' ) diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index 1ea083367..0d33da806 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -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' ) diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 5ef983977..a0238129b 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -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): diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 5fa26a0d7..5be8276b6 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -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 diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 90ec828c7..40c675c4d 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -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): diff --git a/netbox/extras/migrations/0051_delete_customfieldvalue.py b/netbox/extras/migrations/0051_delete_customfieldvalue.py new file mode 100644 index 000000000..3369289a0 --- /dev/null +++ b/netbox/extras/migrations/0051_delete_customfieldvalue.py @@ -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', + ), + ] diff --git a/netbox/extras/models/__init__.py b/netbox/extras/models/__init__.py index 9437fe01f..a4178b911 100644 --- a/netbox/extras/models/__init__.py +++ b/netbox/extras/models/__init__.py @@ -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', diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 1c86812e0..b0ea76cef 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -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 {: 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) diff --git a/netbox/extras/querysets.py b/netbox/extras/querysets.py index 9d9b55778..b7019897b 100644 --- a/netbox/extras/querysets.py +++ b/netbox/extras/querysets.py @@ -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): diff --git a/netbox/extras/tests/test_changelog.py b/netbox/extras/tests/test_changelog.py index 8c9d09564..5d1dee864 100644 --- a/netbox/extras/tests/test_changelog.py +++ b/netbox/extras/tests/test_changelog.py @@ -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}) diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index e543b63e3..74c0e7c3b 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -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): """ diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 58dd96089..e5ae122ab 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -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() diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index 6209b5700..23a883103 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -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() diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index cc3abf19a..5a6108e09 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -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() diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index c7db2f649..51c7a26f9 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -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']) diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index fb61c5b9e..00d444de9 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -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()