Skip to content

Commit

Permalink
Closes #8495: Enable custom field grouping
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremystretch committed Apr 15, 2022
1 parent 4fac10a commit 17df8a5
Show file tree
Hide file tree
Showing 13 changed files with 119 additions and 56 deletions.
6 changes: 6 additions & 0 deletions docs/release-notes/version-3.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@

### Enhancements

* [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping
* [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results

### REST API Changes

* extras.CustomField
* Added `group_name` field
4 changes: 2 additions & 2 deletions netbox/extras/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ class CustomFieldSerializer(ValidatedModelSerializer):
class Meta:
model = CustomField
fields = [
'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'description',
'required', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum',
'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
'description', 'required', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum',
'validation_regex', 'choices', 'created', 'last_updated',
]

Expand Down
3 changes: 2 additions & 1 deletion netbox/extras/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,15 @@ class CustomFieldFilterSet(BaseFilterSet):

class Meta:
model = CustomField
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight', 'description']
fields = ['id', 'content_types', 'name', 'group_name', 'required', 'filter_logic', 'weight', 'description']

def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(label__icontains=value) |
Q(group_name__icontains=value) |
Q(description__icontains=value)
)

Expand Down
5 changes: 4 additions & 1 deletion netbox/extras/forms/bulk_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class CustomFieldBulkEditForm(BulkEditForm):
queryset=CustomField.objects.all(),
widget=forms.MultipleHiddenInput
)
group_name = forms.CharField(
required=False
)
description = forms.CharField(
required=False
)
Expand All @@ -35,7 +38,7 @@ class CustomFieldBulkEditForm(BulkEditForm):
required=False
)

nullable_fields = ('description',)
nullable_fields = ('group_name', 'description',)


class CustomLinkBulkEditForm(BulkEditForm):
Expand Down
4 changes: 2 additions & 2 deletions netbox/extras/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ class CustomFieldCSVForm(CSVModelForm):
class Meta:
model = CustomField
fields = (
'name', 'label', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', 'default',
'choices', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
'name', 'label', 'group_name', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic',
'default', 'choices', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
)


Expand Down
5 changes: 4 additions & 1 deletion netbox/extras/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
class CustomFieldFilterForm(FilterForm):
fieldsets = (
(None, ('q',)),
('Attributes', ('type', 'content_types', 'weight', 'required')),
('Attributes', ('content_types', 'type', 'group_name', 'weight', 'required')),
)
content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
Expand All @@ -44,6 +44,9 @@ class CustomFieldFilterForm(FilterForm):
required=False,
label=_('Field type')
)
group_name = forms.CharField(
required=False
)
weight = forms.IntegerField(
required=False
)
Expand Down
4 changes: 3 additions & 1 deletion netbox/extras/forms/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
)

fieldsets = (
('Custom Field', ('content_types', 'name', 'label', 'type', 'object_type', 'weight', 'required', 'description')),
('Custom Field', (
'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'weight', 'required', 'description',
)),
('Behavior', ('filter_logic',)),
('Values', ('default', 'choices')),
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
Expand Down
22 changes: 22 additions & 0 deletions netbox/extras/migrations/0074_customfield_group_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.0.4 on 2022-04-15 17:13

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('extras', '0073_journalentry_tags_custom_fields'),
]

operations = [
migrations.AlterModelOptions(
name='customfield',
options={'ordering': ['group_name', 'weight', 'name']},
),
migrations.AddField(
model_name='customfield',
name='group_name',
field=models.CharField(blank=True, max_length=50),
),
]
7 changes: 6 additions & 1 deletion netbox/extras/models/customfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
help_text='Name of the field as displayed to users (if not provided, '
'the field\'s name will be used)'
)
group_name = models.CharField(
max_length=50,
blank=True,
help_text="Custom fields within the same group will be displayed together"
)
description = models.CharField(
max_length=200,
blank=True
Expand Down Expand Up @@ -134,7 +139,7 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
objects = CustomFieldManager()

class Meta:
ordering = ['weight', 'name']
ordering = ['group_name', 'weight', 'name']

def __str__(self):
return self.label or self.name.replace('_', ' ').capitalize()
Expand Down
4 changes: 2 additions & 2 deletions netbox/extras/tables/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ class CustomFieldTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = CustomField
fields = (
'pk', 'id', 'name', 'content_types', 'label', 'type', 'required', 'weight', 'default',
'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'weight', 'default',
'description', 'filter_logic', 'choices', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'content_types', 'label', 'type', 'required', 'description')
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')


#
Expand Down
12 changes: 12 additions & 0 deletions netbox/netbox/models/features.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from collections import defaultdict

from django.contrib.contenttypes.fields import GenericRelation
from django.db.models.signals import class_prepared
from django.dispatch import receiver
Expand Down Expand Up @@ -117,6 +119,16 @@ def get_custom_fields(self):

return data

def get_custom_fields_by_group(self):
"""
Return a dictionary of custom field/value mappings organized by group.
"""
grouped_custom_fields = defaultdict(dict)
for cf, value in self.get_custom_fields().items():
grouped_custom_fields[cf.group_name][cf] = value

return dict(grouped_custom_fields)

def clean(self):
super().clean()
from extras.models import CustomField
Expand Down
4 changes: 4 additions & 0 deletions netbox/templates/extras/customfield.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ <h5 class="card-header">
<th scope="row">Label</th>
<td>{{ object.label|placeholder }}</td>
</tr>
<tr>
<th scope="row">Group Name</th>
<td>{{ object.group_name|placeholder }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.get_type_display }}</td>
Expand Down
95 changes: 50 additions & 45 deletions netbox/templates/inc/panels/custom_fields.html
Original file line number Diff line number Diff line change
@@ -1,49 +1,54 @@
{% load helpers %}

{% with custom_fields=object.get_custom_fields %}
{% if custom_fields %}
<div class="card">
<h5 class="card-header">Custom Fields</h5>
<div class="card-body">
<table class="table table-hover attr-table">
{% for field, value in custom_fields.items %}
<tr>
<td>
<span title="{{ field.description|escape }}">{{ field }}</span>
</td>
<td>
{% if field.type == 'integer' and value is not None %}
{{ value }}
{% elif field.type == 'longtext' and value %}
{{ value|markdown }}
{% elif field.type == 'boolean' and value == True %}
{% checkmark value true="True" %}
{% elif field.type == 'boolean' and value == False %}
{% checkmark value false="False" %}
{% elif field.type == 'url' and value %}
<a href="{{ value }}">{{ value|truncatechars:70 }}</a>
{% elif field.type == 'json' and value %}
<pre>{{ value|json }}</pre>
{% elif field.type == 'multiselect' and value %}
{{ value|join:", " }}
{% elif field.type == 'object' and value %}
{{ value|linkify }}
{% elif field.type == 'multiobject' and value %}
{% for obj in value %}
{{ obj|linkify }}{% if not forloop.last %}<br />{% endif %}
{% endfor %}
{% elif value %}
{{ value }}
{% elif field.required %}
<span class="text-warning">Not defined</span>
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
{% with custom_fields=object.get_custom_fields_by_group %}
{% if custom_fields %}
<div class="card">
<h5 class="card-header">Custom Fields</h5>
<div class="card-body">
{% for group_name, fields in custom_fields.items %}
{% if group_name %}
<h6><strong>{{ group_name }}</strong></h6>
{% endif %}
<table class="table table-hover attr-table">
{% for field, value in fields.items %}
<tr>
<td>
<span title="{{ field.description|escape }}">{{ field }}</span>
</td>
<td>
{% if field.type == 'integer' and value is not None %}
{{ value }}
{% elif field.type == 'longtext' and value %}
{{ value|markdown }}
{% elif field.type == 'boolean' and value == True %}
{% checkmark value true="True" %}
{% elif field.type == 'boolean' and value == False %}
{% checkmark value false="False" %}
{% elif field.type == 'url' and value %}
<a href="{{ value }}">{{ value|truncatechars:70 }}</a>
{% elif field.type == 'json' and value %}
<pre>{{ value|json }}</pre>
{% elif field.type == 'multiselect' and value %}
{{ value|join:", " }}
{% elif field.type == 'object' and value %}
{{ value|linkify }}
{% elif field.type == 'multiobject' and value %}
{% for obj in value %}
{{ obj|linkify }}{% if not forloop.last %}<br />{% endif %}
{% endfor %}
</table>
</div>
</div>
{% endif %}
{% elif value %}
{{ value }}
{% elif field.required %}
<span class="text-warning"><i class="mdi mdi-alert"></i> Not defined</span>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}

0 comments on commit 17df8a5

Please sign in to comment.