Test template disable (#6526)

* Add new field to PartTestTemplate model

- 'enabled' field
- default = True (backwards compatibility)
- Allows tests to be disabled
- Retains test results but disables test

* Update PartTestTemplate API

- Expose new field
- Enable filtering by field

* CUI updates

- Add to PartTestTemplate table

* PUI: Update PartTestTemplateTable

* Update getRequiredTests

- By default, filter out tests which are "disabled"

* Update StockItemTestResult table

- Only display "enabled" tests
- Update CUI
- UPdate PUI

* Update existing build output table

* Bump API version

* Docs updates

* Updated unit tests
This commit is contained in:
Oliver 2024-02-20 21:01:59 +11:00 committed by GitHub
parent 3eb1914f1e
commit 567c7edbaf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 108 additions and 12 deletions

View File

@ -1,11 +1,16 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 171 INVENTREE_API_VERSION = 172
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" """Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """ INVENTREE_API_TEXT = """
v172 - 2024-02-20 : https://github.com/inventree/InvenTree/pull/6526
- Adds "enabled" field to the PartTestTemplate API endpoint
- Adds "enabled" filter to the PartTestTemplate list
- Adds "enabled" filter to the StockItemTestResult list
v171 - 2024-02-19 : https://github.com/inventree/InvenTree/pull/6516 v171 - 2024-02-19 : https://github.com/inventree/InvenTree/pull/6516
- Adds "key" as a filterable parameter to PartTestTemplate list endpoint - Adds "key" as a filterable parameter to PartTestTemplate list endpoint

View File

@ -375,7 +375,7 @@ class PartTestTemplateFilter(rest_filters.FilterSet):
"""Metaclass options for this filterset.""" """Metaclass options for this filterset."""
model = PartTestTemplate model = PartTestTemplate
fields = ['required', 'requires_value', 'requires_attachment', 'key'] fields = ['enabled', 'key', 'required', 'requires_attachment', 'requires_value']
part = rest_filters.ModelChoiceFilter( part = rest_filters.ModelChoiceFilter(
queryset=Part.objects.filter(trackable=True), queryset=Part.objects.filter(trackable=True),
@ -440,11 +440,12 @@ class PartTestTemplateList(PartTestTemplateMixin, ListCreateAPI):
search_fields = ['test_name', 'description'] search_fields = ['test_name', 'description']
ordering_fields = [ ordering_fields = [
'test_name', 'enabled',
'required', 'required',
'requires_value', 'requires_value',
'requires_attachment', 'requires_attachment',
'results', 'results',
'test_name',
] ]
ordering = 'test_name' ordering = 'test_name'

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.10 on 2024-02-20 01:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0121_auto_20240207_0344'),
]
operations = [
migrations.AddField(
model_name='parttesttemplate',
name='enabled',
field=models.BooleanField(default=True, help_text='Is this test enabled?', verbose_name='Enabled'),
),
]

View File

@ -2124,7 +2124,7 @@ class Part(
parameter.save() parameter.save()
def getTestTemplates(self, required=None, include_parent=True): def getTestTemplates(self, required=None, include_parent=True, enabled=None):
"""Return a list of all test templates associated with this Part. """Return a list of all test templates associated with this Part.
These are used for validation of a StockItem. These are used for validation of a StockItem.
@ -2143,6 +2143,9 @@ class Part(
if required is not None: if required is not None:
tests = tests.filter(required=required) tests = tests.filter(required=required)
if enabled is not None:
tests = tests.filter(enabled=enabled)
return tests return tests
def getTestTemplateMap(self, **kwargs): def getTestTemplateMap(self, **kwargs):
@ -2154,9 +2157,16 @@ class Part(
return templates return templates
def getRequiredTests(self): def getRequiredTests(self, include_parent=True, enabled=True):
"""Return the tests which are required by this part.""" """Return the tests which are required by this part.
return self.getTestTemplates(required=True)
Arguments:
include_parent: If True, include tests which are defined for parent parts
enabled: If set (either True or False), filter by template "enabled" status
"""
return self.getTestTemplates(
required=True, enabled=enabled, include_parent=include_parent
)
@property @property
def attachment_count(self): def attachment_count(self):
@ -3466,6 +3476,10 @@ class PartTestTemplate(InvenTree.models.InvenTreeMetadataModel):
help_text=_('Enter description for this test'), help_text=_('Enter description for this test'),
) )
enabled = models.BooleanField(
default=True, verbose_name=_('Enabled'), help_text=_('Is this test enabled?')
)
required = models.BooleanField( required = models.BooleanField(
default=True, default=True,
verbose_name=_('Required'), verbose_name=_('Required'),

View File

@ -153,6 +153,7 @@ class PartTestTemplateSerializer(InvenTree.serializers.InvenTreeModelSerializer)
'part', 'part',
'test_name', 'test_name',
'description', 'description',
'enabled',
'required', 'required',
'requires_value', 'requires_value',
'requires_attachment', 'requires_attachment',

View File

@ -382,6 +382,17 @@ class TestTemplateTest(TestCase):
self.assertEqual(variant.getTestTemplates(include_parent=False).count(), 0) self.assertEqual(variant.getTestTemplates(include_parent=False).count(), 0)
self.assertEqual(variant.getTestTemplates(required=True).count(), 5) self.assertEqual(variant.getTestTemplates(required=True).count(), 5)
# Test the 'enabled' status check
self.assertEqual(variant.getTestTemplates(enabled=True).count(), 6)
self.assertEqual(variant.getTestTemplates(enabled=False).count(), 0)
template = variant.getTestTemplates().first()
template.enabled = False
template.save()
self.assertEqual(variant.getTestTemplates(enabled=True).count(), 5)
self.assertEqual(variant.getTestTemplates(enabled=False).count(), 1)
def test_uniqueness(self): def test_uniqueness(self):
"""Test names must be unique for this part and also parts above.""" """Test names must be unique for this part and also parts above."""
variant = Part.objects.get(pk=10004) variant = Part.objects.get(pk=10004)

View File

@ -1247,6 +1247,10 @@ class StockItemTestResultFilter(rest_filters.FilterSet):
label='Required', field_name='template__required' label='Required', field_name='template__required'
) )
enabled = rest_filters.BooleanFilter(
label='Enabled', field_name='template__enabled'
)
test = rest_filters.CharFilter( test = rest_filters.CharFilter(
label='Test name (case insensitive)', method='filter_test_name' label='Test name (case insensitive)', method='filter_test_name'
) )

View File

@ -1127,6 +1127,8 @@ function loadBuildOutputTable(build_info, options={}) {
'{% url "api-part-test-template-list" %}', '{% url "api-part-test-template-list" %}',
{ {
part: build_info.part, part: build_info.part,
required: true,
enabled: true,
}, },
{ {
async: false, async: false,

View File

@ -2818,6 +2818,7 @@ function partTestTemplateFields(options={}) {
required: {}, required: {},
requires_value: {}, requires_value: {},
requires_attachment: {}, requires_attachment: {},
enabled: {},
part: { part: {
hidden: true, hidden: true,
} }
@ -2862,6 +2863,7 @@ function loadPartTestTemplateTable(table, options) {
field: 'pk', field: 'pk',
title: 'ID', title: 'ID',
visible: false, visible: false,
switchable: false,
}, },
{ {
field: 'test_name', field: 'test_name',
@ -2884,6 +2886,14 @@ function loadPartTestTemplateTable(table, options) {
field: 'description', field: 'description',
title: '{% trans "Description" %}', title: '{% trans "Description" %}',
}, },
{
field: 'enabled',
title: '{% trans "Enabled" %}',
sortable: true,
formatter: function(value) {
return yesNoLabel(value);
}
},
{ {
field: 'required', field: 'required',
title: '{% trans "Required" %}', title: '{% trans "Required" %}',

View File

@ -1421,6 +1421,7 @@ function loadStockTestResultsTable(table, options) {
let params = { let params = {
part: options.part, part: options.part,
include_inherited: true, include_inherited: true,
enabled: true,
}; };
var filters = loadTableFilters(filterKey, params); var filters = loadTableFilters(filterKey, params);
@ -1558,6 +1559,7 @@ function loadStockTestResultsTable(table, options) {
var query_params = { var query_params = {
stock_item: options.stock_item, stock_item: options.stock_item,
user_detail: true, user_detail: true,
enabled: true,
attachment_detail: true, attachment_detail: true,
template_detail: false, template_detail: false,
ordering: '-date', ordering: '-date',

View File

@ -476,6 +476,10 @@ function getPartTestTemplateFilters() {
type: 'bool', type: 'bool',
title: '{% trans "Required" %}', title: '{% trans "Required" %}',
}, },
enabled: {
type: 'bool',
title: '{% trans "Enabled" %}',
},
}; };
} }

View File

@ -53,6 +53,10 @@ If this flag is set, then a corresponding test result against a stock item must
If this flag is set, then a corresponding test result against a stock item must provide a file attachment uploaded. If this flag is set, then a corresponding test result against a stock item must provide a file attachment uploaded.
#### Enabled
Tests can be *disabled* by setting the *enabled* flag to `False`. This can be useful if a test is no longer required, but the test template should be retained for historical purposes. Note that *deleting* a test template will also delete any associated test results. So, if a test template is no longer required, it is better to disable it rather than delete it.
### Test Results ### Test Results
Individual stock item objects can have test results associated with them which correspond to test templates. Refer to the [stock test result](../stock/test.md) documentation for further information. Individual stock item objects can have test results associated with them which correspond to test templates. Refer to the [stock test result](../stock/test.md) documentation for further information.

View File

@ -36,7 +36,12 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
sortable: true, sortable: true,
render: (record: any) => { render: (record: any) => {
return ( return (
<Text weight={record.required && 700}>{record.test_name}</Text> <Text
weight={record.required && 700}
color={record.enabled ? undefined : 'red'}
>
{record.test_name}
</Text>
); );
} }
}, },
@ -52,6 +57,9 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
DescriptionColumn({ DescriptionColumn({
switchable: false switchable: false
}), }),
BooleanColumn({
accessor: 'enabled'
}),
BooleanColumn({ BooleanColumn({
accessor: 'required' accessor: 'required'
}), }),
@ -70,6 +78,10 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
name: 'required', name: 'required',
description: t`Show required tests` description: t`Show required tests`
}, },
{
name: 'enabled',
description: t`Show enabled tests`
},
{ {
name: 'requires_value', name: 'requires_value',
description: t`Show tests that require a value` description: t`Show tests that require a value`
@ -100,7 +112,8 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
description: {}, description: {},
required: {}, required: {},
requires_value: {}, requires_value: {},
requires_attachment: {} requires_attachment: {},
enabled: {}
}; };
}, [user]); }, [user]);

View File

@ -55,7 +55,8 @@ export default function StockItemTestResultTable({
.get(apiUrl(ApiEndpoints.part_test_template_list), { .get(apiUrl(ApiEndpoints.part_test_template_list), {
params: { params: {
part: partId, part: partId,
include_inherited: true include_inherited: true,
enabled: true
} }
}) })
.then((response) => response.data) .then((response) => response.data)
@ -126,12 +127,17 @@ export default function StockItemTestResultTable({
sortable: true, sortable: true,
render: (record: any) => { render: (record: any) => {
let required = record.required ?? record.template_detail?.required; let required = record.required ?? record.template_detail?.required;
let enabled = record.enabled ?? record.template_detail?.enabled;
let installed = let installed =
record.stock_item != undefined && record.stock_item != itemId; record.stock_item != undefined && record.stock_item != itemId;
return ( return (
<Group position="apart"> <Group position="apart">
<Text italic={installed} fw={required && 700}> <Text
italic={installed}
fw={required && 700}
color={enabled ? undefined : 'red'}
>
{!record.templateId && '- '} {!record.templateId && '- '}
{record.test_name ?? record.template_detail?.test_name} {record.test_name ?? record.template_detail?.test_name}
</Text> </Text>
@ -419,7 +425,8 @@ export default function StockItemTestResultTable({
stock_item: itemId, stock_item: itemId,
user_detail: true, user_detail: true,
attachment_detail: true, attachment_detail: true,
template_detail: true template_detail: true,
enabled: true
} }
}} }}
/> />