Skip to content

Commit ade9bed

Browse files
authored
Pluralized URL slugs for Custom Object Types (#179)
1 parent 802967b commit ade9bed

File tree

18 files changed

+143
-59
lines changed

18 files changed

+143
-59
lines changed

netbox_custom_objects/api/serializers.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,16 @@ class Meta:
115115
"id",
116116
"url",
117117
"name",
118+
"verbose_name",
119+
"verbose_name_plural",
120+
"slug",
118121
"description",
119122
"tags",
120123
"created",
121124
"last_updated",
122125
"fields",
123126
]
124-
brief_fields = ("id", "url", "name", "description")
127+
brief_fields = ("id", "url", "name", "slug", "description")
125128

126129
def create(self, validated_data):
127130
return super().create(validated_data)
@@ -192,7 +195,7 @@ def get_url(self, obj):
192195
lookup_value = getattr(obj, "pk")
193196
kwargs = {
194197
"pk": lookup_value,
195-
"custom_object_type": obj.custom_object_type.name.lower(),
198+
"custom_object_type": obj.custom_object_type.slug,
196199
}
197200
request = self.context["request"]
198201
format = self.context.get("format")
@@ -230,7 +233,7 @@ def get_url(self, obj):
230233
lookup_value = getattr(obj, "pk")
231234
kwargs = {
232235
"pk": lookup_value,
233-
"custom_object_type": obj.custom_object_type.name.lower(),
236+
"custom_object_type": obj.custom_object_type.slug,
234237
}
235238
request = self.context["request"]
236239
format = self.context.get("format")

netbox_custom_objects/api/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def get(self, request, *args, **kwargs):
5151
# Extra logic to populate roots for custom object type lists
5252
for custom_object_type in CustomObjectType.objects.all():
5353
local_kwargs = deepcopy(kwargs)
54-
cot_name = custom_object_type.name.lower()
54+
cot_name = custom_object_type.slug
5555
url_name = 'customobject-list'
5656
local_kwargs['custom_object_type'] = cot_name
5757
if namespace:

netbox_custom_objects/api/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class CustomObjectViewSet(ModelViewSet):
2323

2424
def get_view_name(self):
2525
if self.model:
26-
return self.model.custom_object_type.name
26+
return self.model.custom_object_type.verbose_name or self.model.custom_object_type.name
2727
return 'Custom Object'
2828

2929
def get_serializer_class(self):
@@ -32,7 +32,7 @@ def get_serializer_class(self):
3232
def get_queryset(self):
3333
try:
3434
custom_object_type = CustomObjectType.objects.get(
35-
name__iexact=self.kwargs["custom_object_type"]
35+
slug=self.kwargs["custom_object_type"]
3636
)
3737
except CustomObjectType.DoesNotExist:
3838
raise Http404

netbox_custom_objects/fields.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def get_bound_field(self, form, field_name):
5959
widget.attrs["data-url"] = reverse(
6060
viewname,
6161
kwargs={
62-
"custom_object_type": form.instance.custom_object_type.name.lower()
62+
"custom_object_type": form.instance.custom_object_type.slug
6363
},
6464
)
6565

@@ -70,7 +70,7 @@ def get_bound_field(self, form, field_name):
7070
"url": reverse(
7171
viewname,
7272
kwargs={
73-
"custom_object_type": form.instance.custom_object_type.name.lower()
73+
"custom_object_type": form.instance.custom_object_type.slug
7474
},
7575
),
7676
"params": {},

netbox_custom_objects/forms.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from netbox.forms import (NetBoxModelBulkEditForm, NetBoxModelFilterSetForm,
66
NetBoxModelForm, NetBoxModelImportForm)
77
from utilities.forms.fields import (CommentField, ContentTypeChoiceField,
8-
DynamicModelChoiceField, TagFilterField)
8+
DynamicModelChoiceField, SlugField, TagFilterField)
99
from utilities.forms.rendering import FieldSet
1010
from utilities.object_types import object_type_name
1111

@@ -26,16 +26,36 @@
2626

2727

2828
class CustomObjectTypeForm(NetBoxModelForm):
29+
verbose_name = forms.CharField(
30+
label=_("Readable name"),
31+
max_length=100,
32+
required=False,
33+
help_text=_("Displayed object type name, e.g. \"Vendor Policy\""),
34+
)
2935
verbose_name_plural = forms.CharField(
30-
label=_("Readable plural name"), max_length=100, required=False
36+
label=_("Readable plural name"),
37+
max_length=100,
38+
required=False,
39+
help_text=_("Displayed plural object type name, e.g. \"Vendor Policies\""),
40+
)
41+
slug = SlugField(
42+
slug_source="verbose_name_plural",
43+
help_text=_("URL-friendly unique plural shorthand, e.g. \"vendor-policies\""),
3144
)
3245

33-
fieldsets = (FieldSet("name", "verbose_name_plural", "description", "tags"),)
46+
fieldsets = (
47+
FieldSet(
48+
"name", "verbose_name", "verbose_name_plural", "slug", "description", "tags",
49+
),
50+
)
3451
comments = CommentField()
3552

3653
class Meta:
3754
model = CustomObjectType
38-
fields = ("name", "verbose_name_plural", "description", "comments", "tags")
55+
fields = (
56+
"name", "verbose_name", "verbose_name_plural", "slug", "description",
57+
"comments", "tags",
58+
)
3959

4060

4161
class CustomObjectTypeBulkEditForm(NetBoxModelBulkEditForm):

netbox_custom_objects/migrations/0001_initial.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 5.2.5 on 2025-08-18 17:15
1+
# Generated by Django 5.2.5 on 2025-08-22 18:57
22

33
import django.core.validators
44
import django.db.models.deletion
@@ -52,9 +52,29 @@ class Migration(migrations.Migration):
5252
),
5353
("description", models.CharField(blank=True, max_length=200)),
5454
("comments", models.TextField(blank=True)),
55-
("name", models.CharField(max_length=100, unique=True)),
55+
(
56+
"name",
57+
models.CharField(
58+
max_length=100,
59+
unique=True,
60+
validators=[
61+
django.core.validators.RegexValidator(
62+
message="Only lowercase alphanumeric characters and underscores are allowed.",
63+
regex="^[a-z0-9_]+$",
64+
),
65+
django.core.validators.RegexValidator(
66+
flags=re.RegexFlag["IGNORECASE"],
67+
inverse_match=True,
68+
message="Double underscores are not permitted in custom object object type names.",
69+
regex="__",
70+
),
71+
],
72+
),
73+
),
5674
("schema", models.JSONField(blank=True, default=dict)),
75+
("verbose_name", models.CharField(blank=True, max_length=100)),
5776
("verbose_name_plural", models.CharField(blank=True, max_length=100)),
77+
("slug", models.SlugField(max_length=100, unique=True)),
5878
(
5979
"tags",
6080
taggit.managers.TaggableManager(

netbox_custom_objects/models.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def __str__(self):
104104
self, primary_field["name"]
105105
)
106106
if not primary_field_value:
107-
return f"{self.custom_object_type.name} {self.id}"
107+
return f"{self.custom_object_type.display_name} {self.id}"
108108
return str(primary_field_value) or str(self.id)
109109

110110
@property
@@ -134,14 +134,14 @@ def get_absolute_url(self):
134134
"plugins:netbox_custom_objects:customobject",
135135
kwargs={
136136
"pk": self.pk,
137-
"custom_object_type": self.custom_object_type.name.lower(),
137+
"custom_object_type": self.custom_object_type.slug,
138138
},
139139
)
140140

141141
def get_list_url(self):
142142
return reverse(
143143
"plugins:netbox_custom_objects:customobject_list",
144-
kwargs={"custom_object_type": self.custom_object_type.name.lower()},
144+
kwargs={"custom_object_type": self.custom_object_type.slug},
145145
)
146146

147147
@classmethod
@@ -154,7 +154,7 @@ def _get_viewname(cls, action=None, rest_api=False):
154154
def _get_action_url(cls, action=None, rest_api=False, kwargs=None):
155155
if kwargs is None:
156156
kwargs = {}
157-
kwargs["custom_object_type"] = cls.custom_object_type.name.lower()
157+
kwargs["custom_object_type"] = cls.custom_object_type.slug
158158
return reverse(cls._get_viewname(action, rest_api), kwargs=kwargs)
159159

160160

@@ -164,9 +164,29 @@ class CustomObjectType(PrimaryModel):
164164
_through_model_cache = (
165165
{}
166166
) # Now stores {custom_object_type_id: {through_model_name: through_model}}
167-
name = models.CharField(max_length=100, unique=True)
167+
name = models.CharField(
168+
max_length=100,
169+
unique=True,
170+
help_text=_("Internal lowercased object name, e.g. \"vendor_policy\""),
171+
validators=(
172+
RegexValidator(
173+
regex=r"^[a-z0-9_]+$",
174+
message=_("Only lowercase alphanumeric characters and underscores are allowed."),
175+
),
176+
RegexValidator(
177+
regex=r"__",
178+
message=_(
179+
"Double underscores are not permitted in custom object object type names."
180+
),
181+
flags=re.IGNORECASE,
182+
inverse_match=True,
183+
),
184+
),
185+
)
168186
schema = models.JSONField(blank=True, default=dict)
187+
verbose_name = models.CharField(max_length=100, blank=True)
169188
verbose_name_plural = models.CharField(max_length=100, blank=True)
189+
slug = models.SlugField(max_length=100, unique=True, db_index=True)
170190

171191
class Meta:
172192
verbose_name = "Custom Object Type"
@@ -182,7 +202,7 @@ class Meta:
182202
]
183203

184204
def __str__(self):
185-
return self.name
205+
return self.display_name
186206

187207
@classmethod
188208
def clear_model_cache(cls, custom_object_type_id=None):
@@ -266,7 +286,7 @@ def get_absolute_url(self):
266286
def get_list_url(self):
267287
return reverse(
268288
"plugins:netbox_custom_objects:customobject_list",
269-
kwargs={"custom_object_type": self.name.lower()},
289+
kwargs={"custom_object_type": self.slug},
270290
)
271291

272292
@classmethod
@@ -354,16 +374,24 @@ def get_collision_safe_order_id_idx_name(self):
354374
def get_database_table_name(self):
355375
return f"{USER_TABLE_DATABASE_NAME_PREFIX}{self.id}"
356376

377+
@property
378+
def title_case_name(self):
379+
return title(self.verbose_name or self.name)
380+
357381
@property
358382
def title_case_name_plural(self):
359-
return title(self.name) + "s"
383+
return title(self.verbose_name or self.name) + "s"
360384

361385
def get_verbose_name(self):
362-
return self.name
386+
return self.verbose_name or self.title_case_name
363387

364388
def get_verbose_name_plural(self):
365389
return self.verbose_name_plural or self.title_case_name_plural
366390

391+
@property
392+
def display_name(self):
393+
return self.get_verbose_name()
394+
367395
@staticmethod
368396
def get_content_type_label(custom_object_type_id):
369397
custom_object_type = CustomObjectType.objects.get(pk=custom_object_type_id)

netbox_custom_objects/navigation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def __iter__(self):
3535
add_button.url = reverse_lazy(
3636
f"plugins:{APP_LABEL}:customobject_add",
3737
kwargs={
38-
"custom_object_type": custom_object_type.name.lower()
38+
"custom_object_type": custom_object_type.slug
3939
},
4040
)
4141
menu_item = PluginMenuItem(
@@ -45,7 +45,7 @@ def __iter__(self):
4545
)
4646
menu_item.url = reverse_lazy(
4747
f"plugins:{APP_LABEL}:customobject_list",
48-
kwargs={"custom_object_type": custom_object_type.name.lower()},
48+
kwargs={"custom_object_type": custom_object_type.slug},
4949
)
5050
yield menu_item
5151

netbox_custom_objects/tables.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def render(self, record, table, **kwargs):
112112
get_viewname(model, action),
113113
kwargs={
114114
"pk": record.pk,
115-
"custom_object_type": record.custom_object_type.name.lower(),
115+
"custom_object_type": record.custom_object_type.slug,
116116
},
117117
)
118118

netbox_custom_objects/templates/netbox_custom_objects/customobject.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
<tr>
9191
<th scope="row">{% trans "Type" %}</th>
9292
<td>
93-
{{ object.custom_object_type|linkify:"name" }}
93+
{{ object.custom_object_type|linkify:"display_name" }}
9494
</td>
9595
</tr>
9696
<tr>

0 commit comments

Comments
 (0)