diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index b1c5931e2..1a16142d0 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -436,7 +436,7 @@ class ModuleTypeForm(NetBoxModelForm): fieldsets = ( FieldSet('profile', 'manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')), - FieldSet('attributes', name=_('Profile Attributes')), + FieldSet('attribute_data', name=_('Profile Attributes')), FieldSet('airflow', 'weight', 'weight_unit', name=_('Hardware')), ) @@ -444,7 +444,7 @@ class ModuleTypeForm(NetBoxModelForm): model = ModuleType fields = [ 'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', - 'attributes', 'comments', 'tags', + 'attribute_data', 'comments', 'tags', ] diff --git a/netbox/dcim/migrations/0203_moduletypeprofile.py b/netbox/dcim/migrations/0203_moduletypeprofile.py index 69c373340..33b14e3bb 100644 --- a/netbox/dcim/migrations/0203_moduletypeprofile.py +++ b/netbox/dcim/migrations/0203_moduletypeprofile.py @@ -36,7 +36,7 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='moduletype', - name='attributes', + name='attribute_data', field=models.JSONField(blank=True, null=True), ), migrations.AddField( diff --git a/netbox/dcim/models/modules.py b/netbox/dcim/models/modules.py index 40c4beb4a..6029eccd3 100644 --- a/netbox/dcim/models/modules.py +++ b/netbox/dcim/models/modules.py @@ -93,7 +93,7 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): blank=True, null=True ) - attributes = models.JSONField( + attribute_data = models.JSONField( blank=True, null=True, verbose_name=_('attributes') @@ -122,19 +122,32 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): def full_name(self): return f"{self.manufacturer} {self.model}" + @property + def attributes(self): + """ + Returns a human-friendly representation of the attributes defined for a ModuleType according to its profile. + """ + if self.profile is None or not self.profile.schema: + return {} + attrs = {} + for name, options in self.profile.schema.get('properties', {}).items(): + key = options.get('title', name) + attrs[key] = self.attribute_data.get(name) + return dict(sorted(attrs.items())) + def clean(self): super().clean() # Validate any attributes against the assigned profile's schema if self.profile: try: - jsonschema.validate(self.attributes, schema=self.profile.schema) + jsonschema.validate(self.attribute_data, schema=self.profile.schema) except JSONValidationError as e: raise ValidationError({ 'attributes': _("Invalid schema: {error}").format(error=e) }) else: - self.attributes = None + self.attribute_data = None def to_yaml(self): data = { diff --git a/netbox/dcim/tables/modules.py b/netbox/dcim/tables/modules.py index 4fc6d866c..78615ed71 100644 --- a/netbox/dcim/tables/modules.py +++ b/netbox/dcim/tables/modules.py @@ -1,7 +1,7 @@ from django.utils.translation import gettext_lazy as _ import django_tables2 as tables -from dcim.models import Module, ModuleType +from dcim.models import Module, ModuleType, ModuleTypeProfile from netbox.tables import NetBoxTable, columns from .template_code import WEIGHT @@ -13,15 +13,19 @@ __all__ = ( class ModuleTypeProfileTable(NetBoxTable): + name = tables.Column( + verbose_name=_('Name'), + linkify=True + ) comments = columns.MarkdownColumn( verbose_name=_('Comments'), ) tags = columns.TagColumn( - url_name='dcim:moduletype_list' + url_name='dcim:moduletypeprofile_list' ) class Meta(NetBoxTable.Meta): - model = ModuleType + model = ModuleTypeProfile fields = ( 'pk', 'id', 'name', 'description', 'comments', 'tags', 'created', 'last_updated', ) @@ -43,6 +47,12 @@ class ModuleTypeTable(NetBoxTable): linkify=True, verbose_name=_('Module Type') ) + weight = columns.TemplateColumn( + verbose_name=_('Weight'), + template_code=WEIGHT, + order_by=('_abs_weight', 'weight_unit') + ) + attributes = columns.DictColumn() instance_count = columns.LinkedCountColumn( viewname='dcim:module_list', url_params={'module_type_id': 'pk'}, @@ -54,17 +64,12 @@ class ModuleTypeTable(NetBoxTable): tags = columns.TagColumn( url_name='dcim:moduletype_list' ) - weight = columns.TemplateColumn( - verbose_name=_('Weight'), - template_code=WEIGHT, - order_by=('_abs_weight', 'weight_unit') - ) class Meta(NetBoxTable.Meta): model = ModuleType fields = ( 'pk', 'id', 'model', 'profile', 'manufacturer', 'part_number', 'airflow', 'weight', 'description', - 'comments', 'tags', 'created', 'last_updated', + 'attributes', 'comments', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'model', 'profile', 'manufacturer', 'part_number', diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index cf6e1f133..f0e55176d 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -35,6 +35,7 @@ __all__ = ( 'ContentTypesColumn', 'CustomFieldColumn', 'CustomLinkColumn', + 'DictColumn', 'DistanceColumn', 'DurationColumn', 'LinkedCountColumn', @@ -707,3 +708,14 @@ class DistanceColumn(TemplateColumn): def __init__(self, template_code=template_code, order_by='_abs_distance', **kwargs): super().__init__(template_code=template_code, order_by=order_by, **kwargs) + + +class DictColumn(tables.Column): + """ + Render a dictionary of data in a simple key: value format, one pair per line. + """ + def render(self, value): + output = '
'.join([ + f'{escape(k)}: {escape(v)}' for k, v in value.items() + ]) + return mark_safe(output) diff --git a/netbox/templates/dcim/moduletype.html b/netbox/templates/dcim/moduletype.html index 2b1039abb..adc5e2a98 100644 --- a/netbox/templates/dcim/moduletype.html +++ b/netbox/templates/dcim/moduletype.html @@ -25,7 +25,7 @@ - + @@ -64,6 +64,27 @@ {% plugin_left_page object %}
+
+

{% trans "Attributes" %}

+ {% if not object.profile %} +
+ {% trans "No profile assigned" %} +
+ {% elif object.attributes %} +
{% trans "Profile" %}{{ object.profile|linkify }}{{ object.profile|linkify|placeholder }}
{% trans "Manufacturer" %}
+ {% for k, v in object.attributes.items %} + + + + + {% endfor %} +
{{ k }}{{ v|placeholder }}
+ {% else %} +
+ {% trans "None" %} +
+ {% endif %} + {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/image_attachments.html' %} diff --git a/netbox/templates/dcim/moduletypeprofile.html b/netbox/templates/dcim/moduletypeprofile.html index 7277fdc87..61d8daee8 100644 --- a/netbox/templates/dcim/moduletypeprofile.html +++ b/netbox/templates/dcim/moduletypeprofile.html @@ -28,8 +28,11 @@
-

{% trans "Schema" %}

-
{{ object.schema|json }}
+

+ {% trans "Schema" %} + {% copy_content 'profile_schema' %} +

+
{{ object.schema|json }}
{% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %}