mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Drop CustomFieldValue
This commit is contained in:
@ -185,7 +185,7 @@ To delete an object, simply call `delete()` on its instance. This will return a
|
||||
>>> vlan
|
||||
<VLAN: 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
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
)
|
||||
|
@ -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()
|
||||
|
@ -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'
|
||||
)
|
||||
|
@ -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'
|
||||
)
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
||||
|
16
netbox/extras/migrations/0051_delete_customfieldvalue.py
Normal file
16
netbox/extras/migrations/0051_delete_customfieldvalue.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Generated by Django 3.1 on 2020-08-21 19:52
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0050_migrate_customfieldvalues'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='CustomFieldValue',
|
||||
),
|
||||
]
|
@ -1,5 +1,5 @@
|
||||
from .change_logging import ChangeLoggedModel, ObjectChange
|
||||
from .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',
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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})
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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'])
|
||||
|
@ -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()
|
||||
|
Reference in New Issue
Block a user