mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-15 16:58:14 +00:00
Merge branch 'generic-parameters' of https://github.com/schrodingersgat/inventree into pr/SchrodingersGat/10699
This commit is contained in:
@@ -235,6 +235,20 @@ The [Caddy](./docker.md#ssl-certificates) container will automatically generate
|
||||
|
||||
Any persistent files generated by the Caddy container (such as certificates, etc) will be stored in the `caddy` directory within the external volume.
|
||||
|
||||
### Web Server Bind Address
|
||||
|
||||
By default, the Dockerized InvenTree web server binds to all available network interfaces and listens for IPv4 traffic on port 8000.
|
||||
This can be adjusted using the following environment variables:
|
||||
|
||||
| Environment Variable | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| INVENTREE_WEB_ADDR | 0.0.0.0 |
|
||||
| INVENTREE_WEB_PORT | 8000 |
|
||||
|
||||
These variables are combined in the [Dockerfile](../../../contrib/container/Dockerfile) to build the bind string passed to the InvenTree server on startup.
|
||||
|
||||
To enable IPv6/Dual Stack support, set `INVENTREE_WEB_ADDR` to `[::]` when you create/start the container.
|
||||
|
||||
### Demo Dataset
|
||||
|
||||
To quickly get started with a [demo dataset](../demo.md), you can run the following command:
|
||||
|
||||
@@ -451,7 +451,17 @@ class ReferenceIndexingMixin(models.Model):
|
||||
reference_int = models.BigIntegerField(default=0)
|
||||
|
||||
|
||||
class InvenTreeModel(PluginValidationMixin, models.Model):
|
||||
class ContentTypeMixin:
|
||||
"""Mixin class which supports retrieval of the ContentType for a model instance."""
|
||||
|
||||
def get_content_type(self):
|
||||
"""Return the ContentType object associated with this model."""
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
return ContentType.objects.get_for_model(self.__class__)
|
||||
|
||||
|
||||
class InvenTreeModel(ContentTypeMixin, PluginValidationMixin, models.Model):
|
||||
"""Base class for InvenTree models, which provides some common functionality.
|
||||
|
||||
Includes the following mixins by default:
|
||||
@@ -658,7 +668,7 @@ class InvenTreeAttachmentMixin(InvenTreePermissionCheckMixin):
|
||||
Attachment.objects.create(**kwargs)
|
||||
|
||||
|
||||
class InvenTreeTree(MPTTModel):
|
||||
class InvenTreeTree(ContentTypeMixin, MPTTModel):
|
||||
"""Provides an abstracted self-referencing tree model, based on the MPTTModel class.
|
||||
|
||||
Our implementation provides the following key improvements:
|
||||
|
||||
@@ -37,7 +37,7 @@ from common.icons import get_icon_packs
|
||||
from common.settings import get_global_setting
|
||||
from data_exporter.mixins import DataExportViewMixin
|
||||
from generic.states.api import urlpattern as generic_states_api_urls
|
||||
from InvenTree.api import BulkDeleteMixin, MetadataView
|
||||
from InvenTree.api import BulkCreateMixin, BulkDeleteMixin, MetadataView
|
||||
from InvenTree.config import CONFIG_LOOKUPS
|
||||
from InvenTree.filters import (
|
||||
ORDER_FILTER,
|
||||
@@ -898,6 +898,7 @@ class ParameterMixin:
|
||||
class ParameterList(
|
||||
OutputOptionsMixin,
|
||||
ParameterMixin,
|
||||
BulkCreateMixin,
|
||||
BulkDeleteMixin,
|
||||
DataExportViewMixin,
|
||||
ListCreateAPI,
|
||||
|
||||
@@ -2631,7 +2631,7 @@ class Parameter(
|
||||
from plugin import PluginMixinEnum, registry
|
||||
|
||||
for plugin in registry.with_mixin(PluginMixinEnum.VALIDATION):
|
||||
# Note: The validate_part_parameter function may raise a ValidationError
|
||||
# Note: The validate_parameter function may raise a ValidationError
|
||||
try:
|
||||
if hasattr(plugin, 'validate_parameter'):
|
||||
result = plugin.validate_parameter(self, self.data)
|
||||
|
||||
@@ -204,8 +204,8 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.LIST_URL, data={'limit': limit}, expected_code=200
|
||||
)
|
||||
|
||||
# Total database queries must be below 20, independent of the number of results
|
||||
self.assertLess(len(ctx), 20)
|
||||
# Total database queries must be below 25, independent of the number of results
|
||||
self.assertLess(len(ctx), 25)
|
||||
|
||||
for result in response.data['results']:
|
||||
self.assertIn('total_price', result)
|
||||
@@ -1428,8 +1428,8 @@ class SalesOrderTest(OrderTest):
|
||||
self.LIST_URL, data={'limit': limit}, expected_code=200
|
||||
)
|
||||
|
||||
# Total database queries must be less than 20
|
||||
self.assertLess(len(ctx), 20)
|
||||
# Total database queries must be less than 25
|
||||
self.assertLess(len(ctx), 25)
|
||||
|
||||
n = len(response.data['results'])
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ class PartCategory(
|
||||
"""Prefectch parts parameters."""
|
||||
return (
|
||||
self.get_parts(cascade=cascade)
|
||||
.prefetch_related('parameters', 'parameters__template')
|
||||
.prefetch_related('parameters_list', 'parameters_list__template')
|
||||
.all()
|
||||
)
|
||||
|
||||
@@ -237,7 +237,7 @@ class PartCategory(
|
||||
parts = prefetch or self.prefetch_parts_parameters(cascade=cascade)
|
||||
|
||||
for part in parts:
|
||||
for parameter in part.parameters.all():
|
||||
for parameter in part.parameters_list.all():
|
||||
parameter_name = parameter.template.name
|
||||
if parameter_name not in unique_parameters_names:
|
||||
unique_parameters_names.append(parameter_name)
|
||||
@@ -260,7 +260,7 @@ class PartCategory(
|
||||
if part.IPN:
|
||||
part_parameters['IPN'] = part.IPN
|
||||
|
||||
for parameter in part.parameters.all():
|
||||
for parameter in part.parameters_list.all():
|
||||
parameter_name = parameter.template.name
|
||||
parameter_value = parameter.data
|
||||
part_parameters[parameter_name] = parameter_value
|
||||
@@ -3761,7 +3761,7 @@ class PartCategoryParameterTemplate(InvenTree.models.InvenTreeMetadataModel):
|
||||
if (
|
||||
self.default_value
|
||||
and get_global_setting(
|
||||
'PART_PARAMETER_ENFORCE_UNITS', True, cache=False, create=False
|
||||
'PARAMETER_ENFORCE_UNITS', True, cache=False, create=False
|
||||
)
|
||||
and self.template.units
|
||||
):
|
||||
|
||||
@@ -236,9 +236,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
|
||||
n = ParameterTemplate.objects.count()
|
||||
|
||||
# Ensure validation of parameter values is disabled for these checks
|
||||
InvenTreeSetting.set_setting(
|
||||
'PART_PARAMETER_ENFORCE_UNITS', False, change_user=None
|
||||
)
|
||||
InvenTreeSetting.set_setting('PARAMETER_ENFORCE_UNITS', False, change_user=None)
|
||||
|
||||
for template in ParameterTemplate.objects.all():
|
||||
response = self.post(
|
||||
|
||||
@@ -163,7 +163,7 @@ class CategoryTest(TestCase):
|
||||
# Iterate through all parts and parameters
|
||||
for fastener in fasteners:
|
||||
self.assertIsInstance(fastener, Part)
|
||||
for parameter in fastener.parameters.all():
|
||||
for parameter in fastener.parameters_list.all():
|
||||
self.assertIsInstance(parameter, Parameter)
|
||||
self.assertIsInstance(parameter.template, ParameterTemplate)
|
||||
|
||||
|
||||
@@ -273,3 +273,81 @@ class TestPartTestParameterMigration(MigratorTestCase):
|
||||
for key, value in self.test_keys.items():
|
||||
template = PartTestTemplate.objects.get(test_name=value)
|
||||
self.assertEqual(template.key, key)
|
||||
|
||||
|
||||
class TestPartParameterDeletion(MigratorTestCase):
|
||||
"""Test for PartParameter deletion migration.
|
||||
|
||||
Ref: https://github.com/inventree/InvenTree/pull/10699
|
||||
|
||||
In the linked PR:
|
||||
|
||||
1. The Parameter and ParameterTemplate models are added
|
||||
2. Data is migrated from PartParameter to Parameter and PartParameterTemplate to ParameterTemplate
|
||||
3. The PartParameter and PartParameterTemplate models are deleted
|
||||
"""
|
||||
|
||||
UNITS = ['mm', 'Ampere', 'kg']
|
||||
|
||||
migrate_from = ('part', '0143_alter_part_image')
|
||||
migrate_to = ('part', '0145_remove_partparametertemplate_selectionlist_and_more')
|
||||
|
||||
def prepare(self):
|
||||
"""Prepare some parts and parameters."""
|
||||
Part = self.old_state.apps.get_model('part', 'part')
|
||||
PartParameter = self.old_state.apps.get_model('part', 'partparameter')
|
||||
PartParameterTemplate = self.old_state.apps.get_model(
|
||||
'part', 'partparametertemplate'
|
||||
)
|
||||
|
||||
# Create some parts
|
||||
for i in range(3):
|
||||
Part.objects.create(
|
||||
name=f'Part {i + 1}',
|
||||
description=f'My part {i + 1}',
|
||||
level=0,
|
||||
lft=0,
|
||||
rght=0,
|
||||
tree_id=0,
|
||||
)
|
||||
|
||||
# Create some parameter templates
|
||||
for idx, units in enumerate(self.UNITS):
|
||||
PartParameterTemplate.objects.create(
|
||||
name=f'Template {idx + 1}',
|
||||
description=f'Description for template {idx + 1}',
|
||||
units=units,
|
||||
)
|
||||
|
||||
# Create some parameters
|
||||
for ii, part in enumerate(Part.objects.all()):
|
||||
for jj, template in enumerate(PartParameterTemplate.objects.all()):
|
||||
PartParameter.objects.create(
|
||||
part=part, template=template, data=str(ii * jj)
|
||||
)
|
||||
|
||||
self.assertEqual(Part.objects.count(), 3)
|
||||
self.assertEqual(PartParameterTemplate.objects.count(), 3)
|
||||
self.assertEqual(PartParameter.objects.count(), 9)
|
||||
|
||||
def test_parameter_deletion(self):
|
||||
"""Test that PartParameter objects have been deleted."""
|
||||
# Test that the PartParameter objects have been deleted
|
||||
with self.assertRaises(ModuleNotFoundError):
|
||||
self.new_state.apps.get_model('part', 'partparameter')
|
||||
|
||||
# Load the new PartParameter model
|
||||
ParameterTemplate = self.new_state.apps.get_model('common', 'parametertemplate')
|
||||
Parameter = self.new_state.apps.get_model('common', 'parameter')
|
||||
Part = self.new_state.apps.get_model('part', 'part')
|
||||
|
||||
self.assertEqual(ParameterTemplate.objects.count(), 3)
|
||||
self.assertEqual(Parameter.objects.count(), 9)
|
||||
self.assertEqual(Part.objects.count(), 3)
|
||||
|
||||
for p in Part.objects.all():
|
||||
params = p.parameters_list.all()
|
||||
self.assertEqual(len(params), 3)
|
||||
|
||||
for unit in self.UNITS:
|
||||
self.assertTrue(params.filter(units=unit).exists())
|
||||
|
||||
@@ -249,12 +249,8 @@ class ParameterTests(TestCase):
|
||||
|
||||
bad_values = ['3 Amps', '-3 zogs', '3.14F']
|
||||
|
||||
raise ValueError('This test must be refactored...')
|
||||
|
||||
# Disable enforcing of part parameter units
|
||||
InvenTreeSetting.set_setting(
|
||||
'PART_PARAMETER_ENFORCE_UNITS', False, change_user=None
|
||||
)
|
||||
InvenTreeSetting.set_setting('PARAMETER_ENFORCE_UNITS', False, change_user=None)
|
||||
|
||||
# Invalid units also pass, but will be converted to the template units
|
||||
for value in bad_values:
|
||||
@@ -262,9 +258,7 @@ class ParameterTests(TestCase):
|
||||
param.full_clean()
|
||||
|
||||
# Enable enforcing of part parameter units
|
||||
InvenTreeSetting.set_setting(
|
||||
'PART_PARAMETER_ENFORCE_UNITS', True, change_user=None
|
||||
)
|
||||
InvenTreeSetting.set_setting('PARAMETER_ENFORCE_UNITS', True, change_user=None)
|
||||
|
||||
for value in bad_values:
|
||||
param = Parameter(content_object=prt, template=template, data=value)
|
||||
@@ -375,13 +369,21 @@ class PartParameterTest(InvenTreeAPITestCase):
|
||||
|
||||
# test that having non unique part/template combinations fails
|
||||
res = self.post(url, data, expected_code=400)
|
||||
|
||||
self.assertEqual(len(res.data), 3)
|
||||
self.assertEqual(len(res.data[1]), 0)
|
||||
for err in [res.data[0], res.data[2]]:
|
||||
self.assertEqual(len(err), 2)
|
||||
self.assertEqual(len(err), 3)
|
||||
self.assertEqual(str(err['model_id'][0]), 'This field must be unique.')
|
||||
self.assertEqual(str(err['model_type'][0]), 'This field must be unique.')
|
||||
self.assertEqual(str(err['template'][0]), 'This field must be unique.')
|
||||
self.assertEqual(Parameter.objects.filter(content_object=part4).count(), 0)
|
||||
|
||||
self.assertEqual(
|
||||
Parameter.objects.filter(
|
||||
model_type=part4.get_content_type(), model_id=part4.pk
|
||||
).count(),
|
||||
0,
|
||||
)
|
||||
|
||||
# Now, create a valid set of parameters
|
||||
data = [
|
||||
@@ -390,7 +392,13 @@ class PartParameterTest(InvenTreeAPITestCase):
|
||||
]
|
||||
res = self.post(url, data, expected_code=201)
|
||||
self.assertEqual(len(res.data), 2)
|
||||
self.assertEqual(Parameter.objects.filter(content_object=part4).count(), 2)
|
||||
|
||||
self.assertEqual(
|
||||
Parameter.objects.filter(
|
||||
model_type=part4.get_content_type(), model_id=part4.pk
|
||||
).count(),
|
||||
2,
|
||||
)
|
||||
|
||||
def test_param_detail(self):
|
||||
"""Tests for the Parameter detail endpoint."""
|
||||
|
||||
@@ -343,7 +343,10 @@ def parameter(
|
||||
Returns:
|
||||
A Parameter object, or None if not found
|
||||
"""
|
||||
if not hasattr(instance, 'parameters'):
|
||||
if instance is None:
|
||||
raise ValueError('parameter tag requires a valid Model instance')
|
||||
|
||||
if not isinstance(instance, Model) or not hasattr(instance, 'parameters'):
|
||||
raise TypeError("parameter tag requires a Model with 'parameters' attribute")
|
||||
|
||||
return (
|
||||
@@ -353,6 +356,15 @@ def parameter(
|
||||
)
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def part_parameter(instance, parameter_name):
|
||||
"""Included for backwards compatibility - use 'parameter' tag instead.
|
||||
|
||||
Ref: https://github.com/inventree/InvenTree/pull/10699
|
||||
"""
|
||||
return parameter(instance, parameter_name)
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def company_image(
|
||||
company: Company, preview: bool = False, thumbnail: bool = False, **kwargs
|
||||
|
||||
@@ -4,6 +4,7 @@ from decimal import Decimal
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import timezone
|
||||
@@ -12,7 +13,7 @@ from django.utils.safestring import SafeString
|
||||
from djmoney.money import Money
|
||||
from PIL import Image
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
from common.models import InvenTreeSetting, Parameter, ParameterTemplate
|
||||
from InvenTree.config import get_testfolder_dir
|
||||
from InvenTree.unit_test import InvenTreeTestCase
|
||||
from part.models import Part # TODO fix import: PartParameter, PartParameterTemplate
|
||||
@@ -406,16 +407,26 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
|
||||
|
||||
def test_part_parameter(self):
|
||||
"""Test the part_parameter template tag."""
|
||||
# TODO fix import: PartParameter, PartParameterTemplate
|
||||
# # Test with a valid part
|
||||
# part = Part.objects.create(name='test', description='test')
|
||||
# t1 = PartParameterTemplate.objects.create(name='Template 1', units='mm')
|
||||
# parameter = PartParameter.objects.create(part=part, template=t1, data='test')
|
||||
# Test with a valid part
|
||||
part = Part.objects.create(name='test', description='test')
|
||||
t1 = ParameterTemplate.objects.create(name='Template 1', units='mm')
|
||||
|
||||
# self.assertEqual(report_tags.part_parameter(part, 'name'), None)
|
||||
# self.assertEqual(report_tags.part_parameter(part, 'Template 1'), parameter)
|
||||
# # Test with an invalid part
|
||||
# self.assertEqual(report_tags.part_parameter(None, 'name'), None)
|
||||
content_type = ContentType.objects.get_for_model(Part)
|
||||
parameter = Parameter.objects.create(
|
||||
model_type=content_type, model_id=part.pk, template=t1, data='test'
|
||||
)
|
||||
|
||||
# Note, use the 'parameter' and 'part_parameter' tags interchangeably here
|
||||
self.assertEqual(report_tags.part_parameter(part, 'name'), None)
|
||||
self.assertEqual(report_tags.parameter(part, 'Template 1'), parameter)
|
||||
|
||||
# Test with a null part
|
||||
with self.assertRaises(ValueError):
|
||||
report_tags.parameter(None, 'name')
|
||||
|
||||
# Test with an invalid model type
|
||||
with self.assertRaises(TypeError):
|
||||
report_tags.parameter(parameter, 'name')
|
||||
|
||||
def test_render_currency(self):
|
||||
"""Test the render_currency template tag."""
|
||||
|
||||
Reference in New Issue
Block a user