Merge branch 'feature' of https://github.com/netbox-community/netbox into feature

# Conflicts:
#	netbox/templates/generic/object_list.html
This commit is contained in:
checktheroads
2021-04-19 19:40:15 -07:00
26 changed files with 405 additions and 249 deletions

View File

@@ -114,6 +114,24 @@ class CustomField(BigIDModel):
def __str__(self):
return self.label or self.name.replace('_', ' ').capitalize()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Cache instance's original name so we can check later whether it has changed
self._name = self.name
def rename_object_data(self, old_name, new_name):
"""
Called when a CustomField has been renamed. Updates all assigned object data.
"""
for ct in self.content_types.all():
model = ct.model_class()
params = {f'custom_field_data__{old_name}__isnull': False}
instances = model.objects.filter(**params)
for instance in instances:
instance.custom_field_data[new_name] = instance.custom_field_data.pop(old_name)
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
def remove_stale_data(self, content_types):
"""
Delete custom field data which is no longer relevant (either because the CustomField is

View File

@@ -391,6 +391,8 @@ class ImageAttachment(BigIDModel):
# Journal entries
#
@extras_features('webhooks')
class JournalEntry(ChangeLoggedModel):
"""
A historical remark concerning an object; collectively, these form an object's journal. The journal is used to

View File

@@ -5,7 +5,7 @@ from cacheops.signals import cache_invalidated, cache_read
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import DEFAULT_DB_ALIAS
from django.db.models.signals import m2m_changed, pre_delete
from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.utils import timezone
from django_prometheus.models import model_deletes, model_inserts, model_updates
from prometheus_client import Counter
@@ -98,6 +98,14 @@ def handle_cf_removed_obj_types(instance, action, pk_set, **kwargs):
instance.remove_stale_data(ContentType.objects.filter(pk__in=pk_set))
def handle_cf_renamed(instance, created, **kwargs):
"""
Handle the renaming of custom field data on objects when a CustomField is renamed.
"""
if not created and instance.name != instance._name:
instance.rename_object_data(old_name=instance._name, new_name=instance.name)
def handle_cf_deleted(instance, **kwargs):
"""
Handle the cleanup of old custom field data when a CustomField is deleted.
@@ -106,6 +114,7 @@ def handle_cf_deleted(instance, **kwargs):
m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through)
post_save.connect(handle_cf_renamed, sender=CustomField)
pre_delete.connect(handle_cf_deleted, sender=CustomField)

View File

@@ -7,7 +7,7 @@ from dcim.models import Site
from extras.choices import *
from extras.models import CustomField, ObjectChange, Tag
from utilities.testing import APITestCase
from utilities.testing.utils import post_data
from utilities.testing.utils import create_tags, post_data
from utilities.testing.views import ModelViewTestCase
@@ -38,7 +38,7 @@ class ChangeLogViewTest(ModelViewTestCase):
cf_select.content_types.set([ct])
def test_create_object(self):
tags = self.create_tags('Tag 1', 'Tag 2')
tags = create_tags('Tag 1', 'Tag 2')
form_data = {
'name': 'Site 1',
'slug': 'site-1',
@@ -72,7 +72,7 @@ class ChangeLogViewTest(ModelViewTestCase):
def test_update_object(self):
site = Site(name='Site 1', slug='site-1')
site.save()
tags = self.create_tags('Tag 1', 'Tag 2', 'Tag 3')
tags = create_tags('Tag 1', 'Tag 2', 'Tag 3')
site.tags.set('Tag 1', 'Tag 2')
form_data = {
@@ -116,7 +116,7 @@ class ChangeLogViewTest(ModelViewTestCase):
}
)
site.save()
self.create_tags('Tag 1', 'Tag 2')
create_tags('Tag 1', 'Tag 2')
site.tags.set('Tag 1', 'Tag 2')
request = {

View File

@@ -91,6 +91,33 @@ class CustomFieldTest(TestCase):
# Delete the custom field
cf.delete()
def test_rename_customfield(self):
obj_type = ContentType.objects.get_for_model(Site)
FIELD_DATA = 'abc'
# Create a custom field
cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1')
cf.save()
cf.content_types.set([obj_type])
# Assign custom field data to an object
site = Site.objects.create(
name='Site 1',
slug='site-1',
custom_field_data={'field1': FIELD_DATA}
)
site.refresh_from_db()
self.assertEqual(site.custom_field_data['field1'], FIELD_DATA)
# Rename the custom field
cf.name = 'field2'
cf.save()
# Check that custom field data on the object has been updated
site.refresh_from_db()
self.assertNotIn('field1', site.custom_field_data)
self.assertEqual(site.custom_field_data['field2'], FIELD_DATA)
class CustomFieldManagerTest(TestCase):

View File

@@ -2,7 +2,7 @@ from django.urls import reverse
from rest_framework import status
from dcim.models import Site
from utilities.testing import APITestCase
from utilities.testing import APITestCase, create_tags
class TaggedItemTest(APITestCase):
@@ -10,7 +10,7 @@ class TaggedItemTest(APITestCase):
Test the application of Tags to and item (a Site, for example) upon creation (POST) and modification (PATCH).
"""
def test_create_tagged_item(self):
tags = self.create_tags("Foo", "Bar", "Baz")
tags = create_tags("Foo", "Bar", "Baz")
data = {
'name': 'Test Site',
'slug': 'test-site',
@@ -37,7 +37,7 @@ class TaggedItemTest(APITestCase):
slug='test-site'
)
site.tags.add("Foo", "Bar", "Baz")
self.create_tags("New Tag")
create_tags("New Tag")
data = {
'tags': [
{"name": "Foo"},