Add attributes property on ModuleType

This commit is contained in:
Jeremy Stretch 2025-03-26 09:49:24 -04:00
parent 69e67d0258
commit 3cda074cbd
7 changed files with 72 additions and 18 deletions

View File

@ -436,7 +436,7 @@ class ModuleTypeForm(NetBoxModelForm):
fieldsets = ( fieldsets = (
FieldSet('profile', 'manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')), 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')), FieldSet('airflow', 'weight', 'weight_unit', name=_('Hardware')),
) )
@ -444,7 +444,7 @@ class ModuleTypeForm(NetBoxModelForm):
model = ModuleType model = ModuleType
fields = [ fields = [
'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit',
'attributes', 'comments', 'tags', 'attribute_data', 'comments', 'tags',
] ]

View File

@ -36,7 +36,7 @@ class Migration(migrations.Migration):
), ),
migrations.AddField( migrations.AddField(
model_name='moduletype', model_name='moduletype',
name='attributes', name='attribute_data',
field=models.JSONField(blank=True, null=True), field=models.JSONField(blank=True, null=True),
), ),
migrations.AddField( migrations.AddField(

View File

@ -93,7 +93,7 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
blank=True, blank=True,
null=True null=True
) )
attributes = models.JSONField( attribute_data = models.JSONField(
blank=True, blank=True,
null=True, null=True,
verbose_name=_('attributes') verbose_name=_('attributes')
@ -122,19 +122,32 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
def full_name(self): def full_name(self):
return f"{self.manufacturer} {self.model}" 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): def clean(self):
super().clean() super().clean()
# Validate any attributes against the assigned profile's schema # Validate any attributes against the assigned profile's schema
if self.profile: if self.profile:
try: try:
jsonschema.validate(self.attributes, schema=self.profile.schema) jsonschema.validate(self.attribute_data, schema=self.profile.schema)
except JSONValidationError as e: except JSONValidationError as e:
raise ValidationError({ raise ValidationError({
'attributes': _("Invalid schema: {error}").format(error=e) 'attributes': _("Invalid schema: {error}").format(error=e)
}) })
else: else:
self.attributes = None self.attribute_data = None
def to_yaml(self): def to_yaml(self):
data = { data = {

View File

@ -1,7 +1,7 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import django_tables2 as tables 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 netbox.tables import NetBoxTable, columns
from .template_code import WEIGHT from .template_code import WEIGHT
@ -13,15 +13,19 @@ __all__ = (
class ModuleTypeProfileTable(NetBoxTable): class ModuleTypeProfileTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
comments = columns.MarkdownColumn( comments = columns.MarkdownColumn(
verbose_name=_('Comments'), verbose_name=_('Comments'),
) )
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:moduletype_list' url_name='dcim:moduletypeprofile_list'
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = ModuleType model = ModuleTypeProfile
fields = ( fields = (
'pk', 'id', 'name', 'description', 'comments', 'tags', 'created', 'last_updated', 'pk', 'id', 'name', 'description', 'comments', 'tags', 'created', 'last_updated',
) )
@ -43,6 +47,12 @@ class ModuleTypeTable(NetBoxTable):
linkify=True, linkify=True,
verbose_name=_('Module Type') 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( instance_count = columns.LinkedCountColumn(
viewname='dcim:module_list', viewname='dcim:module_list',
url_params={'module_type_id': 'pk'}, url_params={'module_type_id': 'pk'},
@ -54,17 +64,12 @@ class ModuleTypeTable(NetBoxTable):
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:moduletype_list' url_name='dcim:moduletype_list'
) )
weight = columns.TemplateColumn(
verbose_name=_('Weight'),
template_code=WEIGHT,
order_by=('_abs_weight', 'weight_unit')
)
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = ModuleType model = ModuleType
fields = ( fields = (
'pk', 'id', 'model', 'profile', 'manufacturer', 'part_number', 'airflow', 'weight', 'description', 'pk', 'id', 'model', 'profile', 'manufacturer', 'part_number', 'airflow', 'weight', 'description',
'comments', 'tags', 'created', 'last_updated', 'attributes', 'comments', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'model', 'profile', 'manufacturer', 'part_number', 'pk', 'model', 'profile', 'manufacturer', 'part_number',

View File

@ -35,6 +35,7 @@ __all__ = (
'ContentTypesColumn', 'ContentTypesColumn',
'CustomFieldColumn', 'CustomFieldColumn',
'CustomLinkColumn', 'CustomLinkColumn',
'DictColumn',
'DistanceColumn', 'DistanceColumn',
'DurationColumn', 'DurationColumn',
'LinkedCountColumn', 'LinkedCountColumn',
@ -707,3 +708,14 @@ class DistanceColumn(TemplateColumn):
def __init__(self, template_code=template_code, order_by='_abs_distance', **kwargs): def __init__(self, template_code=template_code, order_by='_abs_distance', **kwargs):
super().__init__(template_code=template_code, order_by=order_by, **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 = '<br />'.join([
f'{escape(k)}: {escape(v)}' for k, v in value.items()
])
return mark_safe(output)

View File

@ -25,7 +25,7 @@
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">{% trans "Profile" %}</th> <th scope="row">{% trans "Profile" %}</th>
<td>{{ object.profile|linkify }}</td> <td>{{ object.profile|linkify|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">{% trans "Manufacturer" %}</th> <th scope="row">{% trans "Manufacturer" %}</th>
@ -64,6 +64,27 @@
{% plugin_left_page object %} {% plugin_left_page object %}
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Attributes" %}</h2>
{% if not object.profile %}
<div class="card-body text-muted">
{% trans "No profile assigned" %}
</div>
{% elif object.attributes %}
<table class="table table-hover attr-table">
{% for k, v in object.attributes.items %}
<tr>
<th scope="row">{{ k }}</th>
<td>{{ v|placeholder }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="card-body text-muted">
{% trans "None" %}
</div>
{% endif %}
</div>
{% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/image_attachments.html' %} {% include 'inc/panels/image_attachments.html' %}

View File

@ -28,8 +28,11 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h2 class="card-header">{% trans "Schema" %}</h2> <h2 class="card-header d-flex justify-content-between">
<pre>{{ object.schema|json }}</pre> {% trans "Schema" %}
{% copy_content 'profile_schema' %}
</h2>
<pre id="profile_schema">{{ object.schema|json }}</pre>
</div> </div>
{% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}