Parameter types (#4935)

* Add fields to PartParameterTemplateModel

- checkbox: Is the field a 'checkbox'
- choices: List of valid options

* Update javascript

* Adds unit test for PartParameterTemplate

- Checkbox cannot have units
- Checkbox cannot have choices
- Choices must be unique

* Improve API filtering

- Add "has_choices" filter
- Add "has_units" filter

* Prune dead code

* Update js functions for creating / editing parameters

* Update part parameter form

- Rebuild the "data" field based on the selected template
- Supports "string" / "boolean" / "select"

* Adjust data input based on parameter type

- Choice displays available options
- Checkbox displays boolean switch
- Otherwise displays text input
- Adds more unit testing
- Updates to forms.js for improved functionality

* Calculate numeric value for boolean parameters

* Update docs

* Bump API version
This commit is contained in:
Oliver 2023-06-01 07:20:11 +10:00 committed by GitHub
parent 2c05e3e74d
commit e21a5e62b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 519 additions and 112 deletions

View File

@ -2,11 +2,14 @@
# InvenTree API version
INVENTREE_API_VERSION = 117
INVENTREE_API_VERSION = 118
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v118 -> 2023-06-01 : https://github.com/inventree/InvenTree/pull/4935
- Adds extra fields for the PartParameterTemplate model
v117 -> 2023-05-22 : https://github.com/inventree/InvenTree/pull/4854
- Part.units model now supports physical units (e.g. "kg", "m", "mm", etc)
- Replaces SupplierPart "pack_size" field with "pack_quantity"

View File

@ -1349,8 +1349,35 @@ class PartParameterTemplateFilter(rest_filters.FilterSet):
# Simple filter fields
fields = [
'units',
'checkbox',
]
has_choices = rest_filters.BooleanFilter(
method='filter_has_choices',
label='Has Choice',
)
def filter_has_choices(self, queryset, name, value):
"""Filter queryset to include only PartParameterTemplates with choices."""
if str2bool(value):
return queryset.exclude(Q(choices=None) | Q(choices=''))
else:
return queryset.filter(Q(choices=None) | Q(choices=''))
has_units = rest_filters.BooleanFilter(
method='filter_has_units',
label='Has Units',
)
def filter_has_units(self, queryset, name, value):
"""Filter queryset to include only PartParameterTemplates with units."""
if str2bool(value):
return queryset.exclude(Q(units=None) | Q(units=''))
else:
return queryset.filter(Q(units=None) | Q(units=''))
class PartParameterTemplateList(ListCreateAPI):
"""API endpoint for accessing a list of PartParameterTemplate objects.
@ -1377,6 +1404,7 @@ class PartParameterTemplateList(ListCreateAPI):
ordering_fields = [
'name',
'units',
'checkbox',
]
def filter_queryset(self, queryset):
@ -1580,45 +1608,33 @@ class BomFilter(rest_filters.FilterSet):
def filter_available_stock(self, queryset, name, value):
"""Filter the queryset based on whether each line item has any available stock"""
value = str2bool(value)
if value:
queryset = queryset.filter(available_stock__gt=0)
if str2bool(value):
return queryset.filter(available_stock__gt=0)
else:
queryset = queryset.filter(available_stock=0)
return queryset
return queryset.filter(available_stock=0)
on_order = rest_filters.BooleanFilter(label="On order", method="filter_on_order")
def filter_on_order(self, queryset, name, value):
"""Filter the queryset based on whether each line item has any stock on order"""
value = str2bool(value)
if value:
queryset = queryset.filter(on_order__gt=0)
if str2bool(value):
return queryset.filter(on_order__gt=0)
else:
queryset = queryset.filter(on_order=0)
return queryset
return queryset.filter(on_order=0)
has_pricing = rest_filters.BooleanFilter(label="Has Pricing", method="filter_has_pricing")
def filter_has_pricing(self, queryset, name, value):
"""Filter the queryset based on whether pricing information is available for the sub_part"""
value = str2bool(value)
q_a = Q(sub_part__pricing_data=None)
q_b = Q(sub_part__pricing_data__overall_min=None, sub_part__pricing_data__overall_max=None)
if value:
queryset = queryset.exclude(q_a | q_b)
if str2bool(value):
return queryset.exclude(q_a | q_b)
else:
queryset = queryset.filter(q_a | q_b)
return queryset
return queryset.filter(q_a | q_b)
class BomMixin:

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.19 on 2023-05-31 12:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0111_auto_20230521_1350'),
]
operations = [
migrations.AddField(
model_name='partparametertemplate',
name='checkbox',
field=models.BooleanField(default=False, help_text='Is this parameter a checkbox?', verbose_name='Checkbox'),
),
migrations.AddField(
model_name='partparametertemplate',
name='choices',
field=models.CharField(blank=True, help_text='Valid choices for this parameter (comma-separated)', max_length=5000, verbose_name='Choices'),
),
]

View File

@ -46,7 +46,8 @@ from common.settings import currency_code_default
from company.models import SupplierPart
from InvenTree import helpers, validators
from InvenTree.fields import InvenTreeURLField
from InvenTree.helpers import decimal2money, decimal2string, normalize
from InvenTree.helpers import (decimal2money, decimal2string, normalize,
str2bool)
from InvenTree.models import (DataImportMixin, InvenTreeAttachment,
InvenTreeBarcodeMixin, InvenTreeNotesMixin,
InvenTreeTree, MetadataMixin)
@ -3307,6 +3308,8 @@ class PartParameterTemplate(MetadataMixin, models.Model):
name: The name (key) of the Parameter [string]
units: The units of the Parameter [string]
description: Description of the parameter [string]
checkbox: Boolean flag to indicate whether the parameter is a checkbox [bool]
choices: List of valid choices for the parameter [string]
"""
@staticmethod
@ -3321,6 +3324,47 @@ class PartParameterTemplate(MetadataMixin, models.Model):
s += " ({units})".format(units=self.units)
return s
def clean(self):
"""Custom cleaning step for this model:
- A 'checkbox' field cannot have 'choices' set
- A 'checkbox' field cannot have 'units' set
"""
super().clean()
# Check that checkbox parameters do not have units or choices
if self.checkbox:
if self.units:
raise ValidationError({
'units': _('Checkbox parameters cannot have units')
})
if self.choices:
raise ValidationError({
'choices': _('Checkbox parameters cannot have choices')
})
# Check that 'choices' are in fact valid
self.choices = self.choices.strip()
if self.choices:
choice_set = set()
for choice in self.choices.split(','):
choice = choice.strip()
# Ignore empty choices
if not choice:
continue
if choice in choice_set:
raise ValidationError({
'choices': _('Choices must be unique')
})
choice_set.add(choice)
def validate_unique(self, exclude=None):
"""Ensure that PartParameterTemplates cannot be created with the same name.
@ -3337,6 +3381,14 @@ class PartParameterTemplate(MetadataMixin, models.Model):
except PartParameterTemplate.DoesNotExist:
pass
def get_choices(self):
"""Return a list of choices for this parameter template"""
if not self.choices:
return []
return [x.strip() for x in self.choices.split(',') if x.strip()]
name = models.CharField(
max_length=100,
verbose_name=_('Name'),
@ -3360,6 +3412,19 @@ class PartParameterTemplate(MetadataMixin, models.Model):
blank=True,
)
checkbox = models.BooleanField(
default=False,
verbose_name=_('Checkbox'),
help_text=_('Is this parameter a checkbox?')
)
choices = models.CharField(
max_length=5000,
verbose_name=_('Choices'),
help_text=_('Valid choices for this parameter (comma-separated)'),
blank=True,
)
@receiver(post_save, sender=PartParameterTemplate, dispatch_uid='post_save_part_parameter_template')
def post_save_part_parameter_template(sender, instance, created, **kwargs):
@ -3412,6 +3477,11 @@ class PartParameter(models.Model):
# Validate the PartParameter before saving
self.calculate_numeric_value()
# Convert 'boolean' values to 'True' / 'False'
if self.template.checkbox:
self.data = str2bool(self.data)
self.data_numeric = 1 if self.data else 0
super().save(*args, **kwargs)
def clean(self):
@ -3428,6 +3498,13 @@ class PartParameter(models.Model):
'data': e.message
})
# Validate the parameter data against the template choices
if choices := self.template.get_choices():
if self.data not in choices:
raise ValidationError({
'data': _('Invalid choice for parameter value')
})
def calculate_numeric_value(self):
"""Calculate a numeric value for the parameter data.

View File

@ -216,6 +216,8 @@ class PartParameterTemplateSerializer(InvenTree.serializers.InvenTreeModelSerial
'name',
'units',
'description',
'checkbox',
'choices',
]

View File

@ -846,47 +846,13 @@
}
);
$('#param-table').inventreeTable({
});
{% if roles.part.add %}
$('#param-create').click(function() {
constructForm('{% url "api-part-parameter-list" %}', {
method: 'POST',
fields: {
part: {
value: {{ part.pk }},
hidden: true,
},
template: {
filters: {
ordering: 'name',
},
},
data: {},
},
title: '{% trans "Add Parameter" %}',
refreshTable: '#parameter-table',
createPartParameter({{ part.pk }}, {
refreshTable: '#parameter-table'
});
});
{% endif %}
$('.param-edit').click(function() {
var button = $(this);
launchModalForm(button.attr('url'), {
reload: true,
});
});
$('.param-delete').click(function() {
var button = $(this);
launchModalForm(button.attr('url'), {
reload: true,
});
});
});
onPanelLoad("part-attachments", function() {

View File

@ -102,6 +102,29 @@ class ParameterTests(TestCase):
'params'
]
def test_choice_validation(self):
"""Test that parameter choices are correctly validated"""
template = PartParameterTemplate.objects.create(
name='My Template',
description='A template with choices',
choices='red, blue, green'
)
pass_values = ['red', 'blue', 'green']
fail_values = ['rod', 'bleu', 'grene']
part = Part.objects.all().first()
for value in pass_values:
param = PartParameter(part=part, template=template, data=value)
param.full_clean()
for value in fail_values:
param = PartParameter(part=part, template=template, data=value)
with self.assertRaises(django_exceptions.ValidationError):
param.full_clean()
def test_unit_validation(self):
"""Test validation of 'units' field for PartParameterTemplate"""
@ -116,7 +139,7 @@ class ParameterTests(TestCase):
with self.assertRaises(django_exceptions.ValidationError):
tmp.full_clean()
def test_param_validation(self):
def test_param_unit_validation(self):
"""Test that parameters are correctly validated against template units"""
template = PartParameterTemplate.objects.create(
@ -137,7 +160,7 @@ class ParameterTests(TestCase):
with self.assertRaises(django_exceptions.ValidationError):
param.full_clean()
def test_param_conversion(self):
def test_param_unit_conversion(self):
"""Test that parameters are correctly converted to template units"""
template = PartParameterTemplate.objects.create(
@ -202,6 +225,41 @@ class PartParameterTest(InvenTreeAPITestCase):
self.assertEqual(len(response.data), 4)
def test_param_template_validation(self):
"""Test that part parameter template validation routines work correctly."""
# Checkbox parameter cannot have "units" specified
with self.assertRaises(django_exceptions.ValidationError):
template = PartParameterTemplate(
name='test',
description='My description',
units='mm',
checkbox=True
)
template.clean()
# Checkbox parameter cannot have "choices" specified
with self.assertRaises(django_exceptions.ValidationError):
template = PartParameterTemplate(
name='test',
description='My description',
choices='a,b,c',
checkbox=True
)
template.clean()
# Choices must be 'unique'
with self.assertRaises(django_exceptions.ValidationError):
template = PartParameterTemplate(
name='test',
description='My description',
choices='a,a,b',
)
template.clean()
def test_create_param(self):
"""Test that we can create a param via the API."""
url = reverse('api-part-parameter-list')

View File

@ -309,11 +309,7 @@ onPanelLoad('part-parameters', function() {
$("#new-param").click(function() {
constructForm('{% url "api-part-parameter-template-list" %}', {
fields: {
name: {},
units: {},
description: {},
},
fields: partParameterTemplateFields(),
method: 'POST',
title: '{% trans "Create Part Parameter Template" %}',
refreshTable: '#param-table',

View File

@ -18,6 +18,7 @@
showApiError,
showMessage,
showModalSpinner,
toBool,
*/
/* exported
@ -990,15 +991,17 @@ function updateFieldValue(name, value, field, options) {
return;
}
if (field.type == null) {
field.type = guessFieldType(el);
}
switch (field.type) {
case 'decimal':
// Strip trailing zeros
el.val(formatDecimal(value));
break;
case 'boolean':
if (value == true || value.toString().toLowerCase() == 'true') {
el.prop('checked');
}
el.prop('checked', toBool(value));
break;
case 'related field':
// Clear?
@ -1068,6 +1071,34 @@ function validateFormField(name, options) {
}
/*
* Introspect the HTML element to guess the field type
*/
function guessFieldType(element) {
if (!element.exists) {
console.error(`Could not find element '${element}' for guessFieldType`);
return null;
}
switch (element.attr('type')) {
case 'number':
return 'decimal';
case 'checkbox':
return 'boolean';
case 'date':
return 'date';
case 'datetime':
return 'datetime';
case 'text':
return 'string';
default:
// Unknown field type
return null;
}
}
/*
* Extract and field value before sending back to the server
*
@ -1088,9 +1119,16 @@ function getFormFieldValue(name, field={}, options={}) {
var value = null;
let guessed_type = guessFieldType(el);
// If field type is not specified, try to guess it
if (field.type == null || guessed_type == 'boolean') {
field.type = guessed_type;
}
switch (field.type) {
case 'boolean':
value = el.is(':checked');
value = toBool(el.prop("checked"));
break;
case 'date':
case 'datetime':

View File

@ -40,15 +40,42 @@
*/
function yesNoLabel(value, options={}) {
var text = '';
var color = '';
/*
* Convert a value (which may be a string) to a boolean value
*
* @param {string} value: Input value
* @returns {boolean} true or false
*/
function toBool(value) {
if (value) {
text = '{% trans "YES" %}';
if (typeof value == 'string') {
if (value.length == 0) {
return false;
}
value = value.toLowerCase();
if (['true', 't', 'yes', 'y', '1', 'on', 'ok'].includes(value)) {
return true;
} else {
return false;
}
} else {
return value == true;
}
}
function yesNoLabel(value, options={}) {
let text = '';
let color = '';
if (toBool(value)) {
text = options.pass || '{% trans "YES" %}';
color = 'bg-success';
} else {
text = '{% trans "NO" %}';
text = options.fail || '{% trans "NO" %}';
color = 'bg-warning';
}

View File

@ -874,8 +874,8 @@ function insertActionButton(modal, options) {
}
}
/* Attach a provided list of buttons */
function attachButtons(modal, buttons) {
/* Attach a provided list of buttons */
for (var i = 0; i < buttons.length; i++) {
insertActionButton(modal, buttons[i]);
@ -883,14 +883,14 @@ function attachButtons(modal, buttons) {
}
/* Attach a 'callback' function to a given field in the modal form.
* When the value of that field is changed, the callback function is performed.
*
* options:
* - field: The name of the field to attach to
* - action: A function to perform
*/
function attachFieldCallback(modal, callback) {
/* Attach a 'callback' function to a given field in the modal form.
* When the value of that field is changed, the callback function is performed.
*
* options:
* - field: The name of the field to attach to
* - action: A function to perform
*/
// Find the field input in the form
var field = getFieldByName(modal, callback.field);
@ -907,8 +907,8 @@ function attachFieldCallback(modal, callback) {
}
/* Attach a provided list of callback functions */
function attachCallbacks(modal, callbacks) {
/* Attach a provided list of callback functions */
for (var i = 0; i < callbacks.length; i++) {
attachFieldCallback(modal, callbacks[i]);
@ -916,13 +916,13 @@ function attachCallbacks(modal, callbacks) {
}
/* Update a modal form after data are received from the server.
* Manages POST requests until the form is successfully submitted.
*
* The server should respond with a JSON object containing a boolean value 'form_valid'
* Form submission repeats (after user interaction) until 'form_valid' = true
*/
function handleModalForm(url, options) {
/* Update a modal form after data are received from the server.
* Manages POST requests until the form is successfully submitted.
*
* The server should respond with a JSON object containing a boolean value 'form_valid'
* Form submission repeats (after user interaction) until 'form_valid' = true
*/
var modal = options.modal || '#modal-form';

View File

@ -6,6 +6,7 @@
Chart,
constructForm,
constructFormBody,
constructInput,
convertCurrency,
formatCurrency,
formatDecimal,
@ -14,6 +15,7 @@
getFormFieldValue,
getTableData,
global_settings,
guessFieldType,
handleFormErrors,
handleFormSuccess,
imageHoverIcon,
@ -42,6 +44,7 @@
showMessage,
showModalSpinner,
thumbnailImage,
updateFieldValue,
withTitle,
wrapButtons,
yesNoLabel,
@ -1281,6 +1284,137 @@ function loadSimplePartTable(table, url, options={}) {
}
/*
* Construct a set of fields for the PartParameter model.
* Note that the 'data' field changes based on the seleted parameter template
*/
function partParameterFields(options={}) {
let fields = {
part: {
hidden: true, // Part is set by the parent form
},
template: {
filters: {
ordering: 'name',
},
onEdit: function(value, name, field, opts) {
// Callback function when the parameter template is selected.
// We rebuild the 'data' field based on the template selection
let checkbox = false;
let choices = [];
if (value) {
// Request the parameter template data
inventreeGet(`{% url "api-part-parameter-template-list" %}${value}/`, {}, {
async: false,
success: function(response) {
if (response.checkbox) {
// Checkbox input
checkbox = true;
} else if (response.choices) {
// Select input
response.choices.split(',').forEach(function(choice) {
choice = choice.trim();
choices.push({
value: choice,
display_name: choice,
});
});
}
}
});
}
// Find the current field element
let el = $(opts.modal).find('#id_data');
// Extract the current value from the field
let val = getFormFieldValue('data', {}, opts);
// Rebuild the field
let parameters = {};
if (checkbox) {
parameters.type = 'boolean';
} else if (choices.length > 0) {
parameters.type = 'choice';
parameters.choices = choices;
} else {
parameters.type = 'string';
}
let existing_field_type = guessFieldType(el);
// If the field type has changed, we need to replace the field
if (existing_field_type != parameters.type) {
// Construct the new field
let new_field = constructInput('data', parameters, opts);
if (guessFieldType(el) == 'boolean') {
// Boolean fields are wrapped in a parent element
el.parent().replaceWith(new_field);
} else {
el.replaceWith(new_field);
}
}
// Update the field parameters in the form options
opts.fields.data.type = parameters.type;
updateFieldValue('data', val, parameters, opts);
}
},
data: {},
};
if (options.part) {
fields.part.value = options.part;
}
return fields;
}
/*
* Launch a modal form for creating a new PartParameter object
*/
function createPartParameter(part_id, options={}) {
options.fields = partParameterFields({
part: part_id,
});
options.processBeforeUpload = function(data) {
// Convert data to string
data.data = data.data.toString();
return data;
}
options.method = 'POST';
options.title = '{% trans "Add Parameter" %}';
constructForm('{% url "api-part-parameter-list" %}', options);
}
/*
* Launch a modal form for editing a PartParameter object
*/
function editPartParameter(param_id, options={}) {
options.fields = partParameterFields();
options.title = '{% trans "Edit Parameter" %}';
options.processBeforeUpload = function(data) {
// Convert data to string
data.data = data.data.toString();
return data;
}
constructForm(`{% url "api-part-parameter-list" %}${param_id}/`, options);
}
function loadPartParameterTable(table, options) {
var params = options.params || {};
@ -1331,6 +1465,15 @@ function loadPartParameterTable(table, options) {
switchable: false,
sortable: true,
formatter: function(value, row) {
let template = row.template_detail;
if (template.checkbox) {
return yesNoLabel(value, {
pass: '{% trans "True" %}',
fail: '{% trans "False" %}',
});
}
if (row.data_numeric && row.template_detail.units) {
return `<span title='${row.data_numeric} ${row.template_detail.units}'>${row.data}</span>`;
} else {
@ -1368,12 +1511,8 @@ function loadPartParameterTable(table, options) {
$(table).find('.button-parameter-edit').click(function() {
var pk = $(this).attr('pk');
constructForm(`{% url "api-part-parameter-list" %}${pk}/`, {
fields: {
data: {},
},
title: '{% trans "Edit Parameter" %}',
refreshTable: table,
editPartParameter(pk, {
refreshTable: table
});
});
@ -1391,6 +1530,24 @@ function loadPartParameterTable(table, options) {
}
/*
* Return a list of fields for a part parameter template
*/
function partParameterTemplateFields() {
return {
name: {},
description: {},
units: {
icon: 'fa-ruler',
},
choices: {
icon: 'fa-th-list',
},
checkbox: {},
};
}
/*
* Construct a table showing a list of part parameter templates
*/
@ -1410,6 +1567,8 @@ function loadPartParameterTemplateTable(table, options={}) {
url: '{% url "api-part-parameter-template-list" %}',
original: params,
queryParams: filters,
sortable: true,
sidePagination: 'server',
name: 'part-parameter-templates',
formatNoMatches: function() {
return '{% trans "No part parameter templates found" %}';
@ -1438,6 +1597,21 @@ function loadPartParameterTemplateTable(table, options={}) {
sortable: false,
switchable: true,
},
{
field: 'checkbox',
title: '{% trans "Checkbox" %}',
sortable: false,
switchable: true,
formatter: function(value) {
return yesNoLabel(value);
}
},
{
field: 'choices',
title: '{% trans "Choices" %}',
sortable: false,
switchable: true,
},
{
formatter: function(value, row, index, field) {
@ -1459,11 +1633,7 @@ function loadPartParameterTemplateTable(table, options={}) {
constructForm(
`/api/part/parameter/template/${pk}/`,
{
fields: {
name: {},
units: {},
description: {},
},
fields: partParameterTemplateFields(),
title: '{% trans "Edit Part Parameter Template" %}',
refreshTable: table,
}

View File

@ -709,9 +709,28 @@ function getCompanyFilters() {
}
// Return a dictionary of filters for the "PartParameter" table
function getPartParameterFilters() {
return {};
}
// Return a dictionary of filters for the "part parameter template" table
function getPartParameterTemplateFilters() {
return {};
return {
checkbox: {
type: 'bool',
title: '{% trans "Checkbox" %}',
},
has_choices: {
type: 'bool',
title: '{% trans "Has Choices" %}',
},
has_units: {
type: 'bool',
title: '{% trans "Has Units" %}',
}
};
}
@ -747,6 +766,8 @@ function getAvailableTableFilters(tableKey) {
return getStockLocationFilters();
case 'parameters':
return getParametricPartTableFilters();
case 'part-parameters':
return getPartParameterFilters();
case 'part-parameter-templates':
return getPartParameterTemplateFilters();
case 'parts':

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -10,25 +10,41 @@ Part parameters are located in the "Parameters" tab, on each part detail page.
There is no limit for the number of part parameters and they are fully customizable through the use of [parameters templates](#parameter-templates).
Here is an example of parameters for a capacitor:
{% with id="part_parameters_example", url="part/part_parameters_example.png", description="Part Parameters Example List" %}
{% include 'img.html' %}
{% endwith %}
## Parameter Templates
Parameter templates are used to define the different types of parameters which are available for use. These are edited via the [settings interface](../settings/global.md).
Parameter templates are used to define the different types of parameters which are available for use. The following attributes are defined for a parameter template:
| Attribute | Description |
| --- | --- |
| Name | The name of the parameter template (*must be unique*) |
| Description | Optional description for the template |
| Units | Optional units field (*must be a valid [physical unit](#parameter-units)*) |
| Choices | A comma-separated list of valid choices for parameter values linked to this template. |
| Checkbox | If set, parameters linked to this template can only be assigned values *true* or *false* |
### Create Template
Parameter templates are created and edited via the [settings interface](../settings/global.md).
To create a template:
- Navigate to the "Settings" page
- Click on the "Parts" tab
- Scroll down to the "Part Parameter Templates" section
- Click on the "Part Parameters" tab
- Click on the "New Parameter" button
- Fill out the `Create Part Parameter Template` form: `Name` (required) and `Units` (optional) fields
- Click on the "Submit" button.
An existing template can be edited by clicking on the "Edit" button associated with that template:
{% with id="part_parameter_template", url="part/parameter_template_edit.png", description="Edit Parameter Template" %}
{% include 'img.html' %}
{% endwith %}
### Create Parameter
After [creating a template](#create-template) or using the existing templates, you can add parameters to any part.
@ -51,12 +67,6 @@ To access a category's parametric table, click on the "Parameters" tab within th
{% include 'img.html' %}
{% endwith %}
Below is an example of capacitor parametric table filtered with `Package Type = 0402`:
{% with id="parametric_table_example", url="part/parametric_table_example.png", description="Parametric Table Example" %}
{% include 'img.html' %}
{% endwith %}
### Sorting by Parameter Value
The parametric parts table allows the returned parts to be sorted by particular parameter values. Click on the header of a particular parameter column to sort results by that parameter: