Skip to content

Commit 58c7ad2

Browse files
committed
Refactor to remove CustomObjectRelations and store object IDs in data for performance
1 parent 1262204 commit 58c7ad2

File tree

5 files changed

+131
-26
lines changed

5 files changed

+131
-26
lines changed

netbox_custom_objects/api/serializers.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.contrib.contenttypes.models import ContentType
2+
from django.db.models import Model, QuerySet
23
from rest_framework import serializers
34
from rest_framework.exceptions import ValidationError
45

@@ -103,10 +104,10 @@ def get_display(self, obj):
103104
return f'{obj.custom_object_type}: {obj.name}'
104105

105106
def validate(self, attrs):
106-
self.relation_fields = {}
107-
object_field_types = [CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT]
108-
for field in attrs['custom_object_type'].fields.filter(type__in=object_field_types):
109-
self.relation_fields[field.name] = attrs['data'].pop(field.name, None)
107+
# self.relation_fields = {}
108+
# object_field_types = [CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT]
109+
# for field in attrs['custom_object_type'].fields.filter(type__in=object_field_types):
110+
# self.relation_fields[field.name] = attrs['data'].pop(field.name, None)
110111
return super().validate(attrs)
111112

112113
def update_relation_fields(self, instance):
@@ -132,24 +133,29 @@ def update_relation_fields(self, instance):
132133

133134
def create(self, validated_data):
134135
instance = super().create(validated_data)
135-
self.update_relation_fields(instance)
136+
# self.update_relation_fields(instance)
136137
return instance
137138

138139
def update(self, instance, validated_data):
139140
instance = super().update(instance, validated_data)
140-
self.update_relation_fields(instance)
141+
# self.update_relation_fields(instance)
141142
return instance
142143

143144
def get_field_data(self, obj):
144145
result = {}
145146
for field_name, value in obj.fields.items():
146-
field = obj.custom_object_type.fields.get(name=field_name)
147-
if field.type in [CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT]:
148-
serializer = get_serializer_for_model(field.model_class)
147+
if isinstance(value, Model) or isinstance(value, QuerySet):
148+
# serializer = get_serializer_for_model(field.model_class)
149+
if isinstance(value, QuerySet):
150+
serializer = get_serializer_for_model(value.model)
151+
many = True
152+
else:
153+
serializer = get_serializer_for_model(value._meta.model)
154+
many = False
149155
context = {'request': self.context['request']}
150-
result[field.name] = serializer(value, nested=True, context=context, many=field.many).data
156+
result[field_name] = serializer(value, nested=True, context=context, many=many).data
151157
continue
152-
result[field_name] = obj.get_field_value(field_name)
158+
result[field_name] = obj.data.get(field_name)
153159
return result
154160

155161

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Generated by Django 5.2b1 on 2025-04-07 20:21
2+
3+
import django.core.validators
4+
import django.db.models.deletion
5+
import re
6+
import taggit.managers
7+
import utilities.json
8+
import utilities.validators
9+
from django.db import migrations, models
10+
11+
12+
class Migration(migrations.Migration):
13+
14+
initial = True
15+
16+
dependencies = [
17+
('core', '0012_job_object_type_optional'),
18+
('extras', '0123_journalentry_kind_default'),
19+
]
20+
21+
operations = [
22+
migrations.CreateModel(
23+
name='CustomObjectType',
24+
fields=[
25+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
26+
('created', models.DateTimeField(auto_now_add=True, null=True)),
27+
('last_updated', models.DateTimeField(auto_now=True, null=True)),
28+
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
29+
('name', models.CharField(max_length=100, unique=True)),
30+
('slug', models.SlugField(max_length=100, unique=True)),
31+
('description', models.TextField(blank=True)),
32+
('schema', models.JSONField(blank=True, default=dict)),
33+
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
34+
],
35+
options={
36+
'verbose_name': 'Custom Object Type',
37+
},
38+
),
39+
migrations.CreateModel(
40+
name='CustomObject',
41+
fields=[
42+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
43+
('created', models.DateTimeField(auto_now_add=True, null=True)),
44+
('last_updated', models.DateTimeField(auto_now=True, null=True)),
45+
('name', models.CharField(max_length=100, unique=True)),
46+
('data', models.JSONField(blank=True, default=dict)),
47+
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
48+
('custom_object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_objects', to='netbox_custom_objects.customobjecttype')),
49+
],
50+
options={
51+
'verbose_name': 'Custom Object',
52+
},
53+
),
54+
migrations.CreateModel(
55+
name='CustomObjectTypeField',
56+
fields=[
57+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
58+
('created', models.DateTimeField(auto_now_add=True, null=True)),
59+
('last_updated', models.DateTimeField(auto_now=True, null=True)),
60+
('type', models.CharField(default='text', max_length=50)),
61+
('name', models.CharField(max_length=50, validators=[django.core.validators.RegexValidator(flags=re.RegexFlag['IGNORECASE'], message='Only alphanumeric characters and underscores are allowed.', regex='^[a-z0-9_]+$'), django.core.validators.RegexValidator(flags=re.RegexFlag['IGNORECASE'], inverse_match=True, message='Double underscores are not permitted in custom field names.', regex='__')])),
62+
('label', models.CharField(blank=True, max_length=50)),
63+
('group_name', models.CharField(blank=True, max_length=50)),
64+
('description', models.CharField(blank=True, max_length=200)),
65+
('required', models.BooleanField(default=False)),
66+
('unique', models.BooleanField(default=False)),
67+
('search_weight', models.PositiveSmallIntegerField(default=1000)),
68+
('filter_logic', models.CharField(default='loose', max_length=50)),
69+
('default', models.JSONField(blank=True, null=True)),
70+
('related_object_filter', models.JSONField(blank=True, null=True)),
71+
('weight', models.PositiveSmallIntegerField(default=100)),
72+
('validation_minimum', models.BigIntegerField(blank=True, null=True)),
73+
('validation_maximum', models.BigIntegerField(blank=True, null=True)),
74+
('validation_regex', models.CharField(blank=True, max_length=500, validators=[utilities.validators.validate_regex])),
75+
('ui_visible', models.CharField(default='always', max_length=50)),
76+
('ui_editable', models.CharField(default='yes', max_length=50)),
77+
('is_cloneable', models.BooleanField(default=False)),
78+
('comments', models.TextField(blank=True)),
79+
('choice_set', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='choices_for_object_type', to='extras.customfieldchoiceset')),
80+
('custom_object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='netbox_custom_objects.customobjecttype')),
81+
('related_object_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.objecttype')),
82+
],
83+
options={
84+
'verbose_name': 'custom object type field',
85+
'verbose_name_plural': 'custom object type fields',
86+
'ordering': ['group_name', 'weight', 'name'],
87+
},
88+
),
89+
migrations.CreateModel(
90+
name='CustomObjectRelation',
91+
fields=[
92+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
93+
('object_id', models.PositiveIntegerField(db_index=True)),
94+
('custom_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='netbox_custom_objects.customobject')),
95+
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='relations', to='netbox_custom_objects.customobjecttypefield')),
96+
],
97+
),
98+
migrations.AddConstraint(
99+
model_name='customobjecttypefield',
100+
constraint=models.UniqueConstraint(fields=('name', 'custom_object_type'), name='netbox_custom_objects_customobjecttypefield_unique_name'),
101+
),
102+
]

netbox_custom_objects/models.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,18 +174,15 @@ def custom_field_data(self):
174174
def fields(self):
175175
result = {}
176176
for field in self.custom_object_type.fields.all():
177-
result[field.name] = self.get_field_value(field.name)
177+
result[field.name] = self.get_field_value(field)
178178
return result
179179

180-
def get_field_value(self, field_name):
181-
custom_object_type_field = self.custom_object_type.fields.get(name=field_name)
182-
if custom_object_type_field.type in [CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT]:
183-
object_ids = CustomObjectRelation.objects.filter(
184-
custom_object=self, field=custom_object_type_field
185-
).values_list('object_id', flat=True)
186-
field_objects = custom_object_type_field.model_class.objects.filter(pk__in=object_ids)
187-
return field_objects if custom_object_type_field.many else field_objects.first()
188-
return self.data.get(field_name)
180+
def get_field_value(self, field):
181+
if field.type == CustomFieldTypeChoices.TYPE_OBJECT:
182+
return field.model_class.objects.filter(pk=self.data.get(field.name))
183+
if field.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
184+
return field.model_class.objects.filter(pk__in=self.data.get(field.name) or [])
185+
return self.data.get(field.name)
189186

190187
def get_absolute_url(self):
191188
return reverse('plugins:netbox_custom_objects:customobject', args=[self.pk])
@@ -423,7 +420,7 @@ def many(self):
423420
return self.type in ['multiobject', 'multiselect']
424421

425422
def get_child_relations(self, instance):
426-
return self.relations.filter(custom_object=instance)
423+
return instance.get_field_value(self)
427424

428425
def get_absolute_url(self):
429426
return reverse('plugins:netbox_custom_objects:customobjecttype', args=[self.custom_object_type.pk])

netbox_custom_objects/templates/netbox_custom_objects/customobject.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ <h5 class="card-header">{% trans "Custom Objects" %}</h5>
4141
<td>
4242
{% if field.type == 'object' %}
4343
{% with field_values=object|get_child_relations:field %}
44-
{{ field_values.first.instance|linkify }}
44+
{{ field_values.first|linkify }}
4545
{% endwith %}
4646
{% else %}
4747
{{ object|get_field_value:field }}
@@ -67,8 +67,8 @@ <h2 class="card-header">{{ field }}</h2>
6767
<table class="table table-hover attr-table">
6868
{% for relation in field_values.all %}
6969
<tr>
70-
<th scope="row">{{ relation.instance|linkify }}</th>
71-
<td>{{ relation.object_id }}</td>
70+
<th scope="row">{{ relation|linkify }}</th>
71+
<td>{{ relation.id }}</td>
7272
</tr>
7373
{% endfor %}
7474
</table>

netbox_custom_objects/templates/netbox_custom_objects/customobjecttype.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ <h2 class="card-header">Custom Objects</h2>
9494
{% if field.is_single_value %}
9595
{% if field.type == 'object' %}
9696
{% with instance|get_child_relations:field as relations %}
97-
<td>{{ relations.first.instance|linkify }}</td>
97+
<td>{{ relations.first|linkify }}</td>
9898
{% endwith %}
9999
{% else %}
100100
<td>{{ instance|get_field_value:field }}</td>

0 commit comments

Comments
 (0)