Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Test result table #6430

Merged
merged 71 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
dc106e8
Add basic table for stock item test results
SchrodingersGat Feb 6, 2024
ab68d43
Improve custom data formatter callback
SchrodingersGat Feb 6, 2024
4461ffe
Custom data formatter for returned results
SchrodingersGat Feb 6, 2024
a428938
Update YesNoButton functionality
SchrodingersGat Feb 6, 2024
7211a03
Enhancements for stock item test result table
SchrodingersGat Feb 6, 2024
e3270e6
Add placeholder row actions
SchrodingersGat Feb 6, 2024
6536e8b
Merge branch 'master' into test-result-table
SchrodingersGat Feb 7, 2024
ad6890e
Fix table link
SchrodingersGat Feb 7, 2024
807a875
Add option to filter parttesttemplate table by "inherited"
SchrodingersGat Feb 7, 2024
a72d02c
Navigate through to parent part
SchrodingersGat Feb 7, 2024
dc86142
Update PartTestTemplate model
SchrodingersGat Feb 7, 2024
10a7ba1
Custom migration step in tasks.py
SchrodingersGat Feb 7, 2024
bb2d90e
Improve uniqueness validation for PartTestTemplate
SchrodingersGat Feb 7, 2024
f254d28
Add 'template' field to StockItemTestResult
SchrodingersGat Feb 7, 2024
045656c
Add "results" count to PartTestTemplate API
SchrodingersGat Feb 7, 2024
efef526
Add 'results' column to test result table
SchrodingersGat Feb 7, 2024
1109fb3
Update serializer for StockItemTestResult
SchrodingersGat Feb 7, 2024
da2a3cb
Control template_detail field with query params
SchrodingersGat Feb 7, 2024
b9f8862
Update ref in api_version.py
SchrodingersGat Feb 7, 2024
cf46f59
Update data migration
SchrodingersGat Feb 7, 2024
33a3ce7
Fix admin integration
SchrodingersGat Feb 7, 2024
5b43ac1
Update StockItemTestResult table
SchrodingersGat Feb 7, 2024
e300534
Implement "legacy" API support
SchrodingersGat Feb 7, 2024
cc61a04
PUI: Cleanup table
SchrodingersGat Feb 7, 2024
758d346
Update tasks.py
SchrodingersGat Feb 7, 2024
a67806d
Fix unique validation check
SchrodingersGat Feb 7, 2024
5668844
Remove duplicate code
SchrodingersGat Feb 7, 2024
85d1888
CUI: Fix data rendering
SchrodingersGat Feb 7, 2024
82e8374
More refactoring of PUI table
SchrodingersGat Feb 7, 2024
f2e363d
More fixes for PUI table
SchrodingersGat Feb 7, 2024
87b613c
Get row expansion working (kinda)
SchrodingersGat Feb 7, 2024
877b093
Improve rendering of subtable
SchrodingersGat Feb 7, 2024
b5676f0
More PUI updates:
SchrodingersGat Feb 7, 2024
9c978bd
allow delete of test result
SchrodingersGat Feb 7, 2024
33d08c9
Merge branch 'master' into test-result-table
SchrodingersGat Feb 8, 2024
1c6984d
Fix typo
SchrodingersGat Feb 8, 2024
a4cf0bf
Merge remote-tracking branch 'origin/master' into test-result-table
SchrodingersGat Feb 14, 2024
38823ba
Updates for admin integration
SchrodingersGat Feb 14, 2024
1cdbc8b
Unit tests for stock migrations
SchrodingersGat Feb 14, 2024
ea121f5
Added migration test for PartTestTemplate
SchrodingersGat Feb 14, 2024
adfe634
Fix for AttachmentTable
SchrodingersGat Feb 14, 2024
c884ca5
Update test fixtures
SchrodingersGat Feb 14, 2024
ed42eca
Add ModelType information
SchrodingersGat Feb 14, 2024
fe515db
Fix TableState
SchrodingersGat Feb 14, 2024
7815627
Fix dataFormatter type def
SchrodingersGat Feb 14, 2024
3a3c9ea
Merge remote-tracking branch 'origin/master' into test-result-table
SchrodingersGat Feb 14, 2024
eefedf9
Improve table rendering
SchrodingersGat Feb 14, 2024
abc7edc
Correctly filter "edit" and "delete" buttons
SchrodingersGat Feb 14, 2024
c188561
Loosen requirements for dataFormatter
SchrodingersGat Feb 14, 2024
a61aa31
Merge remote-tracking branch 'origin/master' into test-result-table
SchrodingersGat Feb 14, 2024
3f923ca
Fixtures for report tests
SchrodingersGat Feb 15, 2024
8c47d0a
Better API filtering for StocokItemTestResult list
SchrodingersGat Feb 15, 2024
5437aa1
Cleanup API filter
SchrodingersGat Feb 15, 2024
c1c2023
Fix unit tests
SchrodingersGat Feb 15, 2024
12493b0
Further unit test fixes
SchrodingersGat Feb 15, 2024
d396469
Include test results for installed stock items
SchrodingersGat Feb 15, 2024
06591f4
Improve rendering of test result table
SchrodingersGat Feb 15, 2024
c384271
Fix filtering for getTestResults
SchrodingersGat Feb 15, 2024
1046ed3
More unit test fixes
SchrodingersGat Feb 15, 2024
36a8c6a
Fix more unit tests
SchrodingersGat Feb 15, 2024
2df7e12
FIx part unit test
SchrodingersGat Feb 15, 2024
de79814
More fixes
SchrodingersGat Feb 16, 2024
31b4c45
More unit test fixes
SchrodingersGat Feb 16, 2024
112a87c
Rebuild stock item trees when merging
SchrodingersGat Feb 17, 2024
71794ff
Helper function for adding a test result to a stock item
SchrodingersGat Feb 17, 2024
4a26d77
Set init fix
SchrodingersGat Feb 17, 2024
d7c1b58
Code cleanup
SchrodingersGat Feb 18, 2024
381b2d6
Cleanup unused variables
SchrodingersGat Feb 18, 2024
b317323
Add docs and more unit tests
SchrodingersGat Feb 18, 2024
b131d54
Merge branch 'master' into test-result-table
SchrodingersGat Feb 18, 2024
fef628a
Update build unit test
SchrodingersGat Feb 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 168
INVENTREE_API_VERSION = 169
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""

INVENTREE_API_TEXT = """

v168 -> 2024-02-07 : https://github.com/inventree/InvenTree/pull/4824
v169 -> 2024-02-14 : https://github.com/inventree/InvenTree/pull/6430
- Adds 'key' field to PartTestTemplate API endpoint
- Adds annotated 'results' field to PartTestTemplate API endpoint
- Adds 'template' field to StockItemTestResult API endpoint

v168 -> 2024-02-14 : https://github.com/inventree/InvenTree/pull/4824
- Adds machine CRUD API endpoints
- Adds machine settings API endpoints
- Adds machine restart API endpoint
Expand Down
11 changes: 9 additions & 2 deletions InvenTree/InvenTree/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,23 @@ def extract_int(reference, clip=0x7FFFFFFF, allow_negative=False):
return ref_int


def generateTestKey(test_name):
def generateTestKey(test_name: str) -> str:
"""Generate a test 'key' for a given test name. This must not have illegal chars as it will be used for dict lookup in a template.

Tests must be named such that they will have unique keys.
"""
if test_name is None:
test_name = ''

key = test_name.strip().lower()
key = key.replace(' ', '')

# Remove any characters that cannot be used to represent a variable
key = re.sub(r'[^a-zA-Z0-9]', '', key)
key = re.sub(r'[^a-zA-Z0-9_]', '', key)

# If the key starts with a digit, prefix with an underscore
if key[0].isdigit():
key = '_' + key

return key

Expand Down
19 changes: 19 additions & 0 deletions InvenTree/InvenTree/management/commands/check_migrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Check if there are any pending database migrations, and run them."""

import logging

from django.core.management.base import BaseCommand

from InvenTree.tasks import check_for_migrations

logger = logging.getLogger('inventree')


class Command(BaseCommand):
"""Check if there are any pending database migrations, and run them."""

def handle(self, *args, **kwargs):
"""Check for any pending database migrations."""
logger.info('Checking for pending database migrations')
check_for_migrations(force=True, reload_registry=False)
logger.info('Database migrations complete')
14 changes: 14 additions & 0 deletions InvenTree/InvenTree/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,20 @@ def test_model_mixin(self):
self.assertNotIn(PartCategory, models)
self.assertNotIn(InvenTreeSetting, models)

def test_test_key(self):
"""Test for the generateTestKey function."""
tests = {
' Hello World ': 'helloworld',
' MY NEW TEST KEY ': 'mynewtestkey',
' 1234 5678': '_12345678',
' 100 percenT': '_100percent',
' MY_NEW_TEST': 'my_new_test',
' 100_new_tests': '_100_new_tests',
}

for name, key in tests.items():
self.assertEqual(helpers.generateTestKey(name), key)


class TestQuoteWrap(TestCase):
"""Tests for string wrapping."""
Expand Down
3 changes: 2 additions & 1 deletion InvenTree/build/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,9 +655,10 @@ def test_complete_with_required_tests(self):
# let's complete the required test and see if it could be saved
StockItemTestResult.objects.create(
stock_item=self.stockitem_with_required_test,
test=self.test_template_required.test_name,
template=self.test_template_required,
result=True
)

self.build_w_tests_trackable.complete_build_output(self.stockitem_with_required_test, None)

# let's see if a non required test could be saved
Expand Down
1 change: 1 addition & 0 deletions InvenTree/part/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ class PartTestTemplateAdmin(admin.ModelAdmin):
"""Admin class for the PartTestTemplate model."""

list_display = ('part', 'test_name', 'required')
readonly_fields = ['key']

autocomplete_fields = ('part',)

Expand Down
40 changes: 32 additions & 8 deletions InvenTree/part/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,29 +389,53 @@ def filter_part(self, queryset, name, part):

Note that for the 'part' field, we also include any parts "above" the specified part.
"""
variants = part.get_ancestors(include_self=True)
return queryset.filter(part__in=variants)
include_inherited = str2bool(
self.request.query_params.get('include_inherited', True)
)

if include_inherited:
return queryset.filter(part__in=part.get_ancestors(include_self=True))
else:
return queryset.filter(part=part)

class PartTestTemplateDetail(RetrieveUpdateDestroyAPI):
"""Detail endpoint for PartTestTemplate model."""

class PartTestTemplateMixin:
"""Mixin class for the PartTestTemplate API endpoints."""

queryset = PartTestTemplate.objects.all()
serializer_class = part_serializers.PartTestTemplateSerializer

def get_queryset(self, *args, **kwargs):
"""Return an annotated queryset for the PartTestTemplateDetail endpoints."""
queryset = super().get_queryset(*args, **kwargs)
queryset = part_serializers.PartTestTemplateSerializer.annotate_queryset(
queryset
)
return queryset


class PartTestTemplateList(ListCreateAPI):
class PartTestTemplateDetail(PartTestTemplateMixin, RetrieveUpdateDestroyAPI):
"""Detail endpoint for PartTestTemplate model."""

pass


class PartTestTemplateList(PartTestTemplateMixin, ListCreateAPI):
"""API endpoint for listing (and creating) a PartTestTemplate."""

queryset = PartTestTemplate.objects.all()
serializer_class = part_serializers.PartTestTemplateSerializer
filterset_class = PartTestTemplateFilter

filter_backends = SEARCH_ORDER_FILTER

search_fields = ['test_name', 'description']

ordering_fields = ['test_name', 'required', 'requires_value', 'requires_attachment']
ordering_fields = [
'test_name',
'required',
'requires_value',
'requires_attachment',
'results',
]

ordering = 'test_name'

Expand Down
31 changes: 27 additions & 4 deletions InvenTree/part/fixtures/test_templates.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,66 @@
fields:
part: 10000
test_name: Test strength of chair
key: 'teststrengthofchair'

- model: part.parttesttemplate
pk: 2
fields:
part: 10000
test_name: Apply paint
key: 'applypaint'

- model: part.parttesttemplate
pk: 3
fields:
part: 10000
test_name: Sew cushion
key: 'sewcushion'

- model: part.parttesttemplate
pk: 4
fields:
part: 10000
test_name: Attach legs
key: 'attachlegs'

- model: part.parttesttemplate
pk: 5
fields:
part: 10000
test_name: Record weight
key: 'recordweight'
required: false

# Add some tests for one of the variants
- model: part.parttesttemplate
pk: 6
fields:
part: 10003
test_name: Check that chair is green
test_name: Check chair is green
key: 'checkchairisgreen'
required: true

- model: part.parttesttemplate
pk: 7
pk: 8
fields:
part: 10004
test_name: Check that chair is especially green
part: 25
test_name: 'Temperature Test'
key: 'temperaturetest'
required: False

- model: part.parttesttemplate
pk: 9
fields:
part: 25
test_name: 'Settings Checksum'
key: 'settingschecksum'
required: False

- model: part.parttesttemplate
pk: 10
fields:
part: 25
test_name: 'Firmware Version'
key: 'firmwareversion'
required: False
18 changes: 18 additions & 0 deletions InvenTree/part/migrations/0120_parttesttemplate_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.9 on 2024-02-07 03:43

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('part', '0119_auto_20231120_0457'),
]

operations = [
migrations.AddField(
model_name='parttesttemplate',
name='key',
field=models.CharField(blank=True, help_text='Simplified key for the test', max_length=100, verbose_name='Test Key'),
),
]
29 changes: 29 additions & 0 deletions InvenTree/part/migrations/0121_auto_20240207_0344.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.2.9 on 2024-02-07 03:44

from django.db import migrations


def set_key(apps, schema_editor):
"""Create a 'key' value for existing PartTestTemplate objects."""

import InvenTree.helpers

PartTestTemplate = apps.get_model('part', 'PartTestTemplate')

for template in PartTestTemplate.objects.all():
template.key = InvenTree.helpers.generateTestKey(str(template.test_name).strip())
template.save()

if PartTestTemplate.objects.count() > 0:
print(f"\nUpdated 'key' value for {PartTestTemplate.objects.count()} PartTestTemplate objects")


class Migration(migrations.Migration):

dependencies = [
('part', '0120_parttesttemplate_key'),
]

operations = [
migrations.RunPython(set_key, reverse_code=migrations.RunPython.noop)
]
39 changes: 20 additions & 19 deletions InvenTree/part/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3393,6 +3393,10 @@ class PartTestTemplate(InvenTree.models.InvenTreeMetadataModel):
run on the model (refer to the validate_unique function).
"""

def __str__(self):
"""Format a string representation of this PartTestTemplate."""
return ' | '.join([self.part.name, self.test_name])

@staticmethod
def get_api_url():
"""Return the list API endpoint URL associated with the PartTestTemplate model."""
Expand All @@ -3408,6 +3412,8 @@ def clean(self):
"""Clean fields for the PartTestTemplate model."""
self.test_name = self.test_name.strip()

self.key = helpers.generateTestKey(self.test_name)

self.validate_unique()
super().clean()

Expand All @@ -3418,30 +3424,18 @@ def validate_unique(self, exclude=None):
'part': _('Test templates can only be created for trackable parts')
})

# Get a list of all tests "above" this one
# Check that this test is unique within the part tree
tests = PartTestTemplate.objects.filter(
part__in=self.part.get_ancestors(include_self=True)
)
key=self.key, part__tree_id=self.part.tree_id
).exclude(pk=self.pk)

# If this item is already in the database, exclude it from comparison!
if self.pk is not None:
tests = tests.exclude(pk=self.pk)

key = self.key

for test in tests:
if test.key == key:
raise ValidationError({
'test_name': _('Test with this name already exists for this part')
})
if tests.exists():
raise ValidationError({
'test_name': _('Test with this name already exists for this part')
})

super().validate_unique(exclude)

@property
def key(self):
"""Generate a key for this test."""
return helpers.generateTestKey(self.test_name)

part = models.ForeignKey(
Part,
on_delete=models.CASCADE,
Expand All @@ -3457,6 +3451,13 @@ def key(self):
help_text=_('Enter a name for the test'),
)

key = models.CharField(
blank=True,
max_length=100,
verbose_name=_('Test Key'),
help_text=_('Simplified key for the test'),
)

description = models.CharField(
blank=False,
null=True,
Expand Down
11 changes: 11 additions & 0 deletions InvenTree/part/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,20 @@ class Meta:
'required',
'requires_value',
'requires_attachment',
'results',
]

key = serializers.CharField(read_only=True)
results = serializers.IntegerField(
label=_('Results'),
help_text=_('Number of results recorded against this template'),
read_only=True,
)

@staticmethod
def annotate_queryset(queryset):
"""Custom query annotations for the PartTestTemplate serializer."""
return queryset.annotate(results=SubqueryCount('test_results'))


class PartSalePriceSerializer(InvenTree.serializers.InvenTreeModelSerializer):
Expand Down
4 changes: 2 additions & 2 deletions InvenTree/part/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,14 +806,14 @@ def test_test_templates(self):
response = self.get(url)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 7)
self.assertEqual(len(response.data), 9)

# Request for a particular part
response = self.get(url, data={'part': 10000})
self.assertEqual(len(response.data), 5)

response = self.get(url, data={'part': 10004})
self.assertEqual(len(response.data), 7)
self.assertEqual(len(response.data), 6)

# Try to post a new object (missing description)
response = self.post(
Expand Down
Loading
Loading