Skip to content

Commit

Permalink
Extend functionality of custom validation plugins (#4391)
Browse files Browse the repository at this point in the history
* Pass "Part" instance to plugins when calling validate_serial_number

* Pass part instance through when validating IPN

* Improve custom part name validation

- Pass the Part instance through to the plugins
- Validation is performed at the model instance level
- Updates to sample plugin code

* Pass StockItem through when validating batch code

* Pass Part instance through when calling validate_serial_number

* Bug fix

* Update unit tests

* Unit test fixes

* Fixes for unit tests

* More unit test fixes

* More unit tests

* Furrther unit test fixes

* Simplify custom batch code validation

* Further improvements to unit tests

* Further unit test
  • Loading branch information
SchrodingersGat authored Mar 7, 2023
1 parent edae82c commit abeb85c
Show file tree
Hide file tree
Showing 22 changed files with 193 additions and 137 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ _tmp.csv
inventree/label.pdf
inventree/label.png
inventree/my_special*
_tests*.txt

# Sphinx files
docs/_build
Expand Down
10 changes: 1 addition & 9 deletions InvenTree/InvenTree/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,12 @@

from . import config, helpers, ready, status, version
from .tasks import offload_task
from .validators import validate_overage, validate_part_name
from .validators import validate_overage


class ValidatorTest(TestCase):
"""Simple tests for custom field validators."""

def test_part_name(self):
"""Test part name validator."""
validate_part_name('hello world')

# Validate with some strange chars
with self.assertRaises(django_exceptions.ValidationError):
validate_part_name('### <> This | name is not } valid')

def test_overage(self):
"""Test overage validator."""
validate_overage("100%")
Expand Down
46 changes: 0 additions & 46 deletions InvenTree/InvenTree/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
from jinja2 import Template
from moneyed import CURRENCIES

import common.models


def validate_currency_code(code):
"""Check that a given code is a valid currency code."""
Expand Down Expand Up @@ -47,50 +45,6 @@ def __call__(self, value):
super().__call__(value)


def validate_part_name(value):
"""Validate the name field for a Part instance
This function is exposed to any Validation plugins, and thus can be customized.
"""

from plugin.registry import registry

for plugin in registry.with_mixin('validation'):
# Run the name through each custom validator
# If the plugin returns 'True' we will skip any subsequent validation
if plugin.validate_part_name(value):
return


def validate_part_ipn(value):
"""Validate the IPN field for a Part instance.
This function is exposed to any Validation plugins, and thus can be customized.
If no validation errors are raised, the IPN is also validated against a configurable regex pattern.
"""

from plugin.registry import registry

plugins = registry.with_mixin('validation')

for plugin in plugins:
# Run the IPN through each custom validator
# If the plugin returns 'True' we will skip any subsequent validation
if plugin.validate_part_ipn(value):
return

# If we get to here, none of the plugins have raised an error

pattern = common.models.InvenTreeSetting.get_setting('PART_IPN_REGEX')

if pattern:
match = re.search(pattern, value)

if match is None:
raise ValidationError(_('IPN must match regex pattern {pat}').format(pat=pattern))


def validate_purchase_order_reference(value):
"""Validate the 'reference' field of a PurchaseOrder."""

Expand Down
20 changes: 10 additions & 10 deletions InvenTree/order/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -950,12 +950,12 @@ def test_batch_code(self):
{
'line_item': 1,
'quantity': 10,
'batch_code': 'abc-123',
'batch_code': 'B-abc-123',
},
{
'line_item': 2,
'quantity': 10,
'batch_code': 'xyz-789',
'batch_code': 'B-xyz-789',
}
],
'location': 1,
Expand All @@ -975,8 +975,8 @@ def test_batch_code(self):
item_1 = StockItem.objects.filter(supplier_part=line_1.part).first()
item_2 = StockItem.objects.filter(supplier_part=line_2.part).first()

self.assertEqual(item_1.batch, 'abc-123')
self.assertEqual(item_2.batch, 'xyz-789')
self.assertEqual(item_1.batch, 'B-abc-123')
self.assertEqual(item_2.batch, 'B-xyz-789')

def test_serial_numbers(self):
"""Test that we can supply a 'serial number' when receiving items."""
Expand All @@ -991,13 +991,13 @@ def test_serial_numbers(self):
{
'line_item': 1,
'quantity': 10,
'batch_code': 'abc-123',
'batch_code': 'B-abc-123',
'serial_numbers': '100+',
},
{
'line_item': 2,
'quantity': 10,
'batch_code': 'xyz-789',
'batch_code': 'B-xyz-789',
}
],
'location': 1,
Expand All @@ -1022,7 +1022,7 @@ def test_serial_numbers(self):
item = StockItem.objects.get(serial_int=i)
self.assertEqual(item.serial, str(i))
self.assertEqual(item.quantity, 1)
self.assertEqual(item.batch, 'abc-123')
self.assertEqual(item.batch, 'B-abc-123')

# A single stock item (quantity 10) created for the second line item
items = StockItem.objects.filter(supplier_part=line_2.part)
Expand All @@ -1031,7 +1031,7 @@ def test_serial_numbers(self):
item = items.first()

self.assertEqual(item.quantity, 10)
self.assertEqual(item.batch, 'xyz-789')
self.assertEqual(item.batch, 'B-xyz-789')


class SalesOrderTest(OrderTest):
Expand Down Expand Up @@ -1437,7 +1437,7 @@ def test_so_line_list(self):
n_parts = Part.objects.filter(salable=True).count()

# List by part
for part in Part.objects.filter(salable=True):
for part in Part.objects.filter(salable=True)[:3]:
response = self.get(
self.url,
{
Expand All @@ -1449,7 +1449,7 @@ def test_so_line_list(self):
self.assertEqual(response.data['count'], n_orders)

# List by order
for order in models.SalesOrder.objects.all():
for order in models.SalesOrder.objects.all()[:3]:
response = self.get(
self.url,
{
Expand Down
10 changes: 7 additions & 3 deletions InvenTree/part/fixtures/part.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
pk: 100
fields:
name: 'Bob'
description: 'Can we build it?'
description: 'Can we build it? Yes we can!'
assembly: true
salable: true
purchaseable: false
Expand All @@ -112,7 +112,7 @@
pk: 101
fields:
name: 'Assembly'
description: 'A high level assembly'
description: 'A high level assembly part'
salable: true
active: True
tree_id: 0
Expand All @@ -125,7 +125,7 @@
pk: 10000
fields:
name: 'Chair Template'
description: 'A chair'
description: 'A chair, which is actually just a template part'
is_template: True
trackable: true
salable: true
Expand All @@ -139,6 +139,7 @@
pk: 10001
fields:
name: 'Blue Chair'
description: 'A variant chair part which is blue'
variant_of: 10000
trackable: true
category: 7
Expand All @@ -151,6 +152,7 @@
pk: 10002
fields:
name: 'Red chair'
description: 'A variant chair part which is red'
variant_of: 10000
IPN: "R.CH"
trackable: true
Expand All @@ -164,6 +166,7 @@
pk: 10003
fields:
name: 'Green chair'
description: 'A template chair part which is green'
variant_of: 10000
category: 7
trackable: true
Expand All @@ -176,6 +179,7 @@
pk: 10004
fields:
name: 'Green chair variant'
description: 'A green chair, which is a variant of the chair template'
variant_of: 10003
is_template: true
category: 7
Expand Down
2 changes: 1 addition & 1 deletion InvenTree/part/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class Migration(migrations.Migration):
name='Part',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Part name', max_length=100, validators=[InvenTree.validators.validate_part_name])),
('name', models.CharField(help_text='Part name', max_length=100)),
('variant', models.CharField(blank=True, help_text='Part variant or revision code', max_length=32)),
('description', models.CharField(help_text='Part description', max_length=250)),
('keywords', models.CharField(blank=True, help_text='Part keywords to improve visibility in search results', max_length=250)),
Expand Down
3 changes: 1 addition & 2 deletions InvenTree/part/migrations/0006_auto_20190526_1215.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Generated by Django 2.2 on 2019-05-26 02:15

import InvenTree.validators
from django.db import migrations, models


Expand All @@ -14,7 +13,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='part',
name='name',
field=models.CharField(help_text='Part name (must be unique)', max_length=100, unique=True, validators=[InvenTree.validators.validate_part_name]),
field=models.CharField(help_text='Part name (must be unique)', max_length=100, unique=True),
),
migrations.AlterUniqueTogether(
name='part',
Expand Down
3 changes: 1 addition & 2 deletions InvenTree/part/migrations/0010_auto_20190620_2135.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Generated by Django 2.2.2 on 2019-06-20 11:35

import InvenTree.validators
from django.db import migrations, models


Expand All @@ -14,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='part',
name='name',
field=models.CharField(help_text='Part name', max_length=100, validators=[InvenTree.validators.validate_part_name]),
field=models.CharField(help_text='Part name', max_length=100),
),
]
3 changes: 1 addition & 2 deletions InvenTree/part/migrations/0028_auto_20200203_1007.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Generated by Django 2.2.9 on 2020-02-03 10:07

import InvenTree.validators
from django.db import migrations, models


Expand All @@ -14,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='part',
name='IPN',
field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100, validators=[InvenTree.validators.validate_part_ipn]),
field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100),
),
]
3 changes: 1 addition & 2 deletions InvenTree/part/migrations/0048_auto_20200902_1404.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Generated by Django 3.0.7 on 2020-09-02 14:04

import InvenTree.fields
import InvenTree.validators

from django.db import migrations, models

Expand All @@ -16,7 +15,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='part',
name='IPN',
field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100, null=True, validators=[InvenTree.validators.validate_part_ipn]),
field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100, null=True),
),
migrations.AlterField(
model_name='part',
Expand Down
5 changes: 2 additions & 3 deletions InvenTree/part/migrations/0061_auto_20210103_2313.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Generated by Django 3.0.7 on 2021-01-03 12:13

import InvenTree.fields
import InvenTree.validators
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
Expand All @@ -19,7 +18,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='part',
name='IPN',
field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100, null=True, validators=[InvenTree.validators.validate_part_ipn], verbose_name='IPN'),
field=models.CharField(blank=True, help_text='Internal Part Number', max_length=100, null=True, verbose_name='IPN'),
),
migrations.AlterField(
model_name='part',
Expand Down Expand Up @@ -59,7 +58,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='part',
name='name',
field=models.CharField(help_text='Part name', max_length=100, validators=[InvenTree.validators.validate_part_name], verbose_name='Name'),
field=models.CharField(help_text='Part name', max_length=100, verbose_name='Name'),
),
migrations.AlterField(
model_name='part',
Expand Down
Loading

0 comments on commit abeb85c

Please sign in to comment.