Skip to content

Commit 254f1aa

Browse files
authored
Merge main into feature (#204)
1 parent b5c947e commit 254f1aa

File tree

6 files changed

+145
-117
lines changed

6 files changed

+145
-117
lines changed

.github/workflows/lint-tests.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: Setup Python
2424
uses: actions/setup-python@v5
2525
with:
26-
python-version: "3.10"
26+
python-version: "3.12"
2727
- name: Install dependencies
2828
run: |
2929
python -m pip install --upgrade pip
@@ -37,7 +37,7 @@ jobs:
3737
timeout-minutes: 10
3838
strategy:
3939
matrix:
40-
python-version: [ "3.10", "3.11", "3.12" ]
40+
python-version: [ "3.12" ]
4141
services:
4242
redis:
4343
image: redis

netbox_custom_objects/api/views.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from rest_framework.routers import APIRootView
44
from rest_framework.viewsets import ModelViewSet
55

6+
from netbox_custom_objects.filtersets import get_filterset_class
67
from netbox_custom_objects.models import CustomObjectType, CustomObjectTypeField
78

89
from . import serializers
@@ -52,6 +53,10 @@ def get_queryset(self):
5253
self.model = custom_object_type.get_model()
5354
return self.model.objects.all()
5455

56+
@property
57+
def filterset_class(self):
58+
return get_filterset_class(self.model)
59+
5560
def list(self, request, *args, **kwargs):
5661
return super().list(request, *args, **kwargs)
5762

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
from netbox.filtersets import NetBoxModelFilterSet
1+
import django_filters
2+
from django.contrib.postgres.fields import ArrayField
3+
from django.db.models import JSONField
4+
from netbox.filtersets import BaseFilterSet, NetBoxModelFilterSet
25

36
from .models import CustomObjectType
47

5-
__all__ = ("CustomObjectTypeFilterSet",)
8+
__all__ = (
9+
"CustomObjectTypeFilterSet",
10+
"get_filterset_class",
11+
)
612

713

814
class CustomObjectTypeFilterSet(NetBoxModelFilterSet):
@@ -14,18 +20,44 @@ class Meta:
1420
)
1521

1622

17-
"""
18-
class CustomObjectFilterSet(NetBoxModelFilterSet):
19-
class Meta:
20-
model = CustomObject
21-
fields = (
22-
"id",
23-
"name",
24-
"custom_object_type",
25-
)
23+
def get_filterset_class(model):
24+
"""
25+
Create and return a filterset class for the given custom object model.
26+
"""
27+
fields = [field.name for field in model._meta.fields]
28+
29+
meta = type(
30+
"Meta",
31+
(),
32+
{
33+
"model": model,
34+
"fields": fields,
35+
# TODO: overrides should come from FieldType
36+
# These are placeholders; should use different logic
37+
"filter_overrides": {
38+
JSONField: {
39+
"filter_class": django_filters.CharFilter,
40+
"extra": lambda f: {
41+
"lookup_expr": "icontains",
42+
},
43+
},
44+
ArrayField: {
45+
"filter_class": django_filters.CharFilter,
46+
"extra": lambda f: {
47+
"lookup_expr": "icontains",
48+
},
49+
},
50+
},
51+
},
52+
)
53+
54+
attrs = {
55+
"Meta": meta,
56+
"__module__": "database.filtersets",
57+
}
2658

27-
def search(self, queryset, name, value):
28-
if not value.strip():
29-
return queryset
30-
return queryset.filter(Q(name__icontains=value))
31-
"""
59+
return type(
60+
f"{model._meta.object_name}FilterSet",
61+
(BaseFilterSet,), # TODO: Should be a NetBoxModelFilterSet
62+
attrs,
63+
)

netbox_custom_objects/models.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import django_filters
66
from core.models import ObjectType, ObjectChange
7-
from core.models.contenttypes import ObjectTypeManager
7+
from core.models.object_types import ObjectTypeManager
88
from django.apps import apps
99
from django.conf import settings
1010

@@ -698,6 +698,11 @@ def delete(self, *args, **kwargs):
698698
self.clear_model_cache(self.id)
699699

700700
model = self.get_model()
701+
702+
# Delete all CustomObjectTypeFields that reference this CustomObjectType
703+
for field in CustomObjectTypeField.objects.filter(related_object_type=self.content_type):
704+
field.delete()
705+
701706
object_type = ObjectType.objects.get_for_model(model)
702707
ObjectChange.objects.filter(changed_object_type=object_type).delete()
703708
super().delete(*args, **kwargs)

netbox_custom_objects/templates/netbox_custom_objects/customobjecttype.html

Lines changed: 56 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -58,55 +58,62 @@ <h5 class="card-header">{% trans "Custom Object Type" %}</h5>
5858
<div class="card">
5959
<h2 class="card-header">Fields</h2>
6060
<table class="table table-hover attr-table">
61-
{% for field in object.fields.all %}
62-
<tr>
63-
<th scope="row">{{ field }}</th>
64-
<td>{{ field|get_field_type_verbose_name }}</td>
65-
<td>{% if field.type == "object" or field.type == "multiobject" %}{{ field|get_field_object_type }}{% endif %}</td>
66-
<td>
67-
{% if field.required %}
68-
<i
69-
class="mdi mdi-asterisk text-primary"
70-
data-bs-toggle="tooltip"
71-
data-bs-placement="right"
72-
title="Required"
73-
></i>
74-
{% endif %}
75-
</td>
76-
<td>
77-
{% if field.unique %}
78-
<i
79-
class="mdi mdi-snowflake text-primary"
80-
data-bs-toggle="tooltip"
81-
data-bs-placement="right"
82-
title="Unique"
83-
></i>
84-
{% endif %}
85-
</td>
86-
<td>
87-
{% if field.primary %}
88-
<i
89-
class="mdi mdi-text-short text-primary"
90-
data-bs-toggle="tooltip"
91-
data-bs-placement="right"
92-
title="Primary name field"
93-
></i>
94-
{% endif %}
95-
</td>
96-
<td>
97-
<span class="btn-group dropdown">
98-
<a class="btn btn-sm btn-warning" href="{% url 'plugins:netbox_custom_objects:customobjecttypefield_edit' pk=field.pk %}?return_url={{ object.get_absolute_url }}" type="button"
99-
aria-label="{attrs.title}"><i class="mdi mdi-pencil"></i></a>
100-
<a class="btn btn-sm btn-warning dropdown-toggle" type="button" data-bs-toggle="dropdown"
101-
style="padding-left: 2px">
102-
<span class="visually-hidden">{toggle_text}</span></a>
103-
<ul class="dropdown-menu">
104-
<li><a class="dropdown-item" href="{% url 'plugins:netbox_custom_objects:customobjecttypefield_delete' pk=field.pk %}?return_url={{ object.get_absolute_url }}">
105-
<i class="mdi mdi-trash-can-outline"></i> Delete</a></li>
106-
</ul>
107-
</span>
108-
</td>
109-
</tr>
61+
{% for group_name, group_fields in field_groups.items %}
62+
{% if group_name %}
63+
<tr class="table-group-header">
64+
<th scope="row" colspan="7" class="fw-bold">{{ group_name }}</th>
65+
</tr>
66+
{% endif %}
67+
{% for field in group_fields %}
68+
<tr>
69+
<th scope="row">{{ field }}</th>
70+
<td>{{ field|get_field_type_verbose_name }}</td>
71+
<td>{% if field.type == "object" or field.type == "multiobject" %}{{ field|get_field_object_type }}{% endif %}</td>
72+
<td>
73+
{% if field.required %}
74+
<i
75+
class="mdi mdi-asterisk text-primary"
76+
data-bs-toggle="tooltip"
77+
data-bs-placement="right"
78+
title="Required"
79+
></i>
80+
{% endif %}
81+
</td>
82+
<td>
83+
{% if field.unique %}
84+
<i
85+
class="mdi mdi-snowflake text-primary"
86+
data-bs-toggle="tooltip"
87+
data-bs-placement="right"
88+
title="Unique"
89+
></i>
90+
{% endif %}
91+
</td>
92+
<td>
93+
{% if field.primary %}
94+
<i
95+
class="mdi mdi-text-short text-primary"
96+
data-bs-toggle="tooltip"
97+
data-bs-placement="right"
98+
title="Primary name field"
99+
></i>
100+
{% endif %}
101+
</td>
102+
<td>
103+
<span class="btn-group dropdown">
104+
<a class="btn btn-sm btn-warning" href="{% url 'plugins:netbox_custom_objects:customobjecttypefield_edit' pk=field.pk %}?return_url={{ object.get_absolute_url }}" type="button"
105+
aria-label="{attrs.title}"><i class="mdi mdi-pencil"></i></a>
106+
<a class="btn btn-sm btn-warning dropdown-toggle" type="button" data-bs-toggle="dropdown"
107+
style="padding-left: 2px">
108+
<span class="visually-hidden">{toggle_text}</span></a>
109+
<ul class="dropdown-menu">
110+
<li><a class="dropdown-item" href="{% url 'plugins:netbox_custom_objects:customobjecttypefield_delete' pk=field.pk %}?return_url={{ object.get_absolute_url }}">
111+
<i class="mdi mdi-trash-can-outline"></i> Delete</a></li>
112+
</ul>
113+
</span>
114+
</td>
115+
</tr>
116+
{% endfor %}
110117
{% endfor %}
111118
</table>
112119
</div>

netbox_custom_objects/views.py

Lines changed: 28 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import django_filters
21
import logging
2+
33
from core.models import ObjectChange
44
from core.tables import ObjectChangeTable
55
from django.contrib.contenttypes.models import ContentType
6-
from django.contrib.postgres.fields import ArrayField
7-
from django.db.models import JSONField, Q
6+
from django.db.models import Q
87
from django.shortcuts import get_object_or_404, render
98
from django.urls import reverse
109
from django.utils.translation import gettext_lazy as _
@@ -13,7 +12,6 @@
1312
from extras.forms import JournalEntryForm
1413
from extras.models import JournalEntry
1514
from extras.tables import JournalEntryTable
16-
from netbox.filtersets import BaseFilterSet
1715
from netbox.forms import (
1816
NetBoxModelBulkEditForm,
1917
NetBoxModelFilterSetForm,
@@ -23,15 +21,10 @@
2321
from netbox.views.generic.mixins import TableMixin
2422
from utilities.forms import ConfirmationForm
2523
from utilities.htmx import htmx_partial
26-
from utilities.views import (
27-
ConditionalLoginRequiredMixin,
28-
ViewTab,
29-
get_viewname,
30-
register_model_view,
31-
)
24+
from utilities.views import ConditionalLoginRequiredMixin, ViewTab, get_viewname, register_model_view
3225

26+
from netbox_custom_objects.filtersets import get_filterset_class
3327
from netbox_custom_objects.tables import CustomObjectTable
34-
3528
from . import field_types, filtersets, forms, tables
3629
from .models import CustomObject, CustomObjectType, CustomObjectTypeField
3730
from extras.choices import CustomFieldTypeChoices
@@ -172,9 +165,22 @@ def get_table(self, data, request, bulk_actions=True):
172165

173166
def get_extra_context(self, request, instance):
174167
model = instance.get_model()
168+
169+
# Get fields and group them by group_name
170+
fields = instance.fields.all().order_by("group_name", "weight", "name")
171+
172+
# Group fields by group_name
173+
field_groups = {}
174+
for field in fields:
175+
group_name = field.group_name or None # Use None for ungrouped fields
176+
if group_name not in field_groups:
177+
field_groups[group_name] = []
178+
field_groups[group_name].append(field)
179+
175180
return {
176181
"custom_objects": model.objects.all(),
177182
"table": self.get_table(self.queryset, request),
183+
"field_groups": field_groups,
178184
}
179185

180186

@@ -194,6 +200,16 @@ def _get_dependent_objects(self, obj):
194200
dependent_objects = super()._get_dependent_objects(obj)
195201
model = obj.get_model()
196202
dependent_objects[model] = list(model.objects.all())
203+
204+
# Find CustomObjectTypeFields that reference this CustomObjectType
205+
referencing_fields = CustomObjectTypeField.objects.filter(
206+
related_object_type=obj.content_type
207+
)
208+
209+
# Add the CustomObjectTypeFields that reference this CustomObjectType
210+
if referencing_fields.exists():
211+
dependent_objects[CustomObjectTypeField] = list(referencing_fields)
212+
197213
return dependent_objects
198214

199215

@@ -319,44 +335,7 @@ def get_queryset(self, request):
319335
return model.objects.all()
320336

321337
def get_filterset(self):
322-
model = self.queryset.model
323-
fields = [field.name for field in model._meta.fields]
324-
325-
meta = type(
326-
"Meta",
327-
(),
328-
{
329-
"model": model,
330-
"fields": fields,
331-
# TODO: overrides should come from FieldType
332-
# These are placeholders; should use different logic
333-
"filter_overrides": {
334-
JSONField: {
335-
"filter_class": django_filters.CharFilter,
336-
"extra": lambda f: {
337-
"lookup_expr": "icontains",
338-
},
339-
},
340-
ArrayField: {
341-
"filter_class": django_filters.CharFilter,
342-
"extra": lambda f: {
343-
"lookup_expr": "icontains",
344-
},
345-
},
346-
},
347-
},
348-
)
349-
350-
attrs = {
351-
"Meta": meta,
352-
"__module__": "database.filtersets",
353-
}
354-
355-
return type(
356-
f"{model._meta.object_name}FilterSet",
357-
(BaseFilterSet,), # TODO: Should be a NetBoxModelFilterSet
358-
attrs,
359-
)
338+
return get_filterset_class(self.queryset.model)
360339

361340
def get_filterset_form(self):
362341
model = self.queryset.model

0 commit comments

Comments
 (0)