Skip to content

Commit

Permalink
Merge branch 'master' into test-result-table
Browse files Browse the repository at this point in the history
  • Loading branch information
SchrodingersGat committed Feb 18, 2024
2 parents b317323 + ad1c1ae commit b131d54
Show file tree
Hide file tree
Showing 99 changed files with 30,050 additions and 19,558 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/qc_checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ jobs:
if: needs.paths-filter.outputs.api == 'false'
run: |
diff --color -u InvenTree/schema.yml api.yaml
diff -u InvenTree/schema.yml api.yaml && echo "no difference in API schema " || echo "differences in API schema" && exit 2
diff -u InvenTree/schema.yml api.yaml && echo "no difference in API schema " || exit 2
- name: Check schema - including warnings
run: invoke schema
continue-on-error: true
Expand Down
4 changes: 2 additions & 2 deletions InvenTree/InvenTree/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import os.path
import re
from decimal import Decimal, InvalidOperation
from typing import Set, Type, TypeVar
from typing import TypeVar
from wsgiref.util import FileWrapper

from django.conf import settings
Expand Down Expand Up @@ -896,7 +896,7 @@ def get_target(self, obj):
Inheritors_T = TypeVar('Inheritors_T')


def inheritors(cls: Type[Inheritors_T]) -> Set[Type[Inheritors_T]]:
def inheritors(cls: type[Inheritors_T]) -> set[type[Inheritors_T]]:
"""Return all classes that are subclasses from the supplied cls."""
subcls = set()
work = [cls]
Expand Down
4 changes: 4 additions & 0 deletions InvenTree/InvenTree/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ def has_permission(self, request, view):

# The required role may be defined for the view class
if role := getattr(view, 'role_required', None):
# If the role is specified as "role.permission", split it
if '.' in role:
role, permission = role.split('.')

return users.models.check_user_role(user, role, permission)

try:
Expand Down
120 changes: 64 additions & 56 deletions InvenTree/InvenTree/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,61 +120,6 @@
# The filesystem location for uploaded meadia files
MEDIA_ROOT = config.get_media_dir()

# List of allowed hosts (default = allow all)
# Ref: https://docs.djangoproject.com/en/4.2/ref/settings/#allowed-hosts
ALLOWED_HOSTS = get_setting(
'INVENTREE_ALLOWED_HOSTS',
config_key='allowed_hosts',
default_value=['*'],
typecast=list,
)

# List of trusted origins for unsafe requests
# Ref: https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins
CSRF_TRUSTED_ORIGINS = get_setting(
'INVENTREE_TRUSTED_ORIGINS',
config_key='trusted_origins',
default_value=[],
typecast=list,
)

USE_X_FORWARDED_HOST = get_boolean_setting(
'INVENTREE_USE_X_FORWARDED_HOST',
config_key='use_x_forwarded_host',
default_value=False,
)

USE_X_FORWARDED_PORT = get_boolean_setting(
'INVENTREE_USE_X_FORWARDED_PORT',
config_key='use_x_forwarded_port',
default_value=False,
)

# Cross Origin Resource Sharing (CORS) options
# Refer to the django-cors-headers documentation for more information
# Ref: https://github.com/adamchainz/django-cors-headers

# Extract CORS options from configuration file
CORS_ALLOW_ALL_ORIGINS = get_boolean_setting(
'INVENTREE_CORS_ORIGIN_ALLOW_ALL', config_key='cors.allow_all', default_value=DEBUG
)

CORS_ALLOW_CREDENTIALS = get_boolean_setting(
'INVENTREE_CORS_ALLOW_CREDENTIALS',
config_key='cors.allow_credentials',
default_value=True,
)

# Only allow CORS access to API and media endpoints
CORS_URLS_REGEX = r'^/(api|media|static)/.*$'

CORS_ALLOWED_ORIGINS = get_setting(
'INVENTREE_CORS_ORIGIN_WHITELIST',
config_key='cors.whitelist',
default_value=[],
typecast=list,
)

# Needed for the parts importer, directly impacts the maximum parts that can be uploaded
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000

Expand Down Expand Up @@ -847,7 +792,7 @@
get_setting('INVENTREE_BACKGROUND_WORKERS', 'background.workers', 4)
),
'timeout': _q_worker_timeout,
'retry': min(120, _q_worker_timeout + 30),
'retry': max(120, _q_worker_timeout + 30),
'max_attempts': int(
get_setting('INVENTREE_BACKGROUND_MAX_ATTEMPTS', 'background.max_attempts', 5)
),
Expand Down Expand Up @@ -1024,6 +969,69 @@
if not SITE_MULTI:
INSTALLED_APPS.remove('django.contrib.sites')

# List of allowed hosts (default = allow all)
# Ref: https://docs.djangoproject.com/en/4.2/ref/settings/#allowed-hosts
ALLOWED_HOSTS = get_setting(
'INVENTREE_ALLOWED_HOSTS',
config_key='allowed_hosts',
default_value=['*'],
typecast=list,
)

# List of trusted origins for unsafe requests
# Ref: https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins
CSRF_TRUSTED_ORIGINS = get_setting(
'INVENTREE_TRUSTED_ORIGINS',
config_key='trusted_origins',
default_value=[],
typecast=list,
)

# If a list of trusted is not specified, but a site URL has been specified, use that
if SITE_URL and len(CSRF_TRUSTED_ORIGINS) == 0:
CSRF_TRUSTED_ORIGINS.append(SITE_URL)

USE_X_FORWARDED_HOST = get_boolean_setting(
'INVENTREE_USE_X_FORWARDED_HOST',
config_key='use_x_forwarded_host',
default_value=False,
)

USE_X_FORWARDED_PORT = get_boolean_setting(
'INVENTREE_USE_X_FORWARDED_PORT',
config_key='use_x_forwarded_port',
default_value=False,
)

# Cross Origin Resource Sharing (CORS) options
# Refer to the django-cors-headers documentation for more information
# Ref: https://github.com/adamchainz/django-cors-headers

# Extract CORS options from configuration file
CORS_ALLOW_ALL_ORIGINS = get_boolean_setting(
'INVENTREE_CORS_ORIGIN_ALLOW_ALL', config_key='cors.allow_all', default_value=DEBUG
)

CORS_ALLOW_CREDENTIALS = get_boolean_setting(
'INVENTREE_CORS_ALLOW_CREDENTIALS',
config_key='cors.allow_credentials',
default_value=True,
)

# Only allow CORS access to API and media endpoints
CORS_URLS_REGEX = r'^/(api|media|static)/.*$'

CORS_ALLOWED_ORIGINS = get_setting(
'INVENTREE_CORS_ORIGIN_WHITELIST',
config_key='cors.whitelist',
default_value=[],
typecast=list,
)

# If no CORS origins are specified, but a site URL has been specified, use that
if SITE_URL and len(CORS_ALLOWED_ORIGINS) == 0:
CORS_ALLOWED_ORIGINS.append(SITE_URL)

for app in SOCIAL_BACKENDS:
# Ensure that the app starts with 'allauth.socialaccount.providers'
social_prefix = 'allauth.socialaccount.providers.'
Expand Down
4 changes: 2 additions & 2 deletions InvenTree/InvenTree/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import warnings
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Callable, List
from typing import Callable

from django.conf import settings
from django.core.exceptions import AppRegistryNotReady
Expand Down Expand Up @@ -291,7 +291,7 @@ class ScheduledTask:
class TaskRegister:
"""Registry for periodic tasks."""

task_list: List[ScheduledTask] = []
task_list: list[ScheduledTask] = []

def register(self, task, schedule, minutes: int = None):
"""Register a task with the que."""
Expand Down
5 changes: 5 additions & 0 deletions InvenTree/build/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,11 @@ def complete_build_output(self, output, user, **kwargs):
# List the allocated BuildItem objects for the given output
allocated_items = output.items_to_install.all()

if (common.settings.prevent_build_output_complete_on_incompleted_tests() and output.hasRequiredTests() and not output.passedAllRequiredTests()):
serial = output.serial
raise ValidationError(
_(f"Build output {serial} has not passed all required tests"))

for build_item in allocated_items:
# Complete the allocation of stock for that item
build_item.complete_allocation(user)
Expand Down
12 changes: 12 additions & 0 deletions InvenTree/build/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from stock.models import generate_batch_code, StockItem, StockLocation
from stock.serializers import StockItemSerializerBrief, LocationSerializer

import common.models
from common.serializers import ProjectCodeSerializer
import part.filters
from part.serializers import BomItemSerializer, PartSerializer, PartBriefSerializer
Expand Down Expand Up @@ -523,6 +524,17 @@ def validate(self, data):

outputs = data.get('outputs', [])

if common.settings.prevent_build_output_complete_on_incompleted_tests():
errors = []
for output in outputs:
stock_item = output['output']
if stock_item.hasRequiredTests() and not stock_item.passedAllRequiredTests():
serial = stock_item.serial
errors.append(_(f"Build output {serial} has not passed all required tests"))

if errors:
raise ValidationError(errors)

if len(outputs) == 0:
raise ValidationError(_("A list of build outputs must be provided"))

Expand Down
100 changes: 95 additions & 5 deletions InvenTree/build/test_build.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Unit tests for the 'build' models"""

import uuid
from datetime import datetime, timedelta

from django.test import TestCase
Expand All @@ -14,8 +14,8 @@
import common.models
import build.tasks
from build.models import Build, BuildItem, BuildLine, generate_next_build_reference
from part.models import Part, BomItem, BomItemSubstitute
from stock.models import StockItem
from part.models import Part, BomItem, BomItemSubstitute, PartTestTemplate
from stock.models import StockItem, StockItemTestResult
from users.models import Owner

import logging
Expand Down Expand Up @@ -55,6 +55,76 @@ def setUpTestData(cls):
trackable=True,
)

# create one build with one required test template
cls.tested_part_with_required_test = Part.objects.create(
name="Part having required tests",
description="Why does it matter what my description is?",
assembly=True,
trackable=True,
)

cls.test_template_required = PartTestTemplate.objects.create(
part=cls.tested_part_with_required_test,
test_name="Required test",
description="Required test template description",
required=True,
requires_value=False,
requires_attachment=False
)

ref = generate_next_build_reference()

cls.build_w_tests_trackable = Build.objects.create(
reference=ref,
title="This is a build",
part=cls.tested_part_with_required_test,
quantity=1,
issued_by=get_user_model().objects.get(pk=1),
)

cls.stockitem_with_required_test = StockItem.objects.create(
part=cls.tested_part_with_required_test,
quantity=1,
is_building=True,
serial=uuid.uuid4(),
build=cls.build_w_tests_trackable
)

# now create a part with a non-required test template
cls.tested_part_wo_required_test = Part.objects.create(
name="Part with one non.required test",
description="Why does it matter what my description is?",
assembly=True,
trackable=True,
)

cls.test_template_non_required = PartTestTemplate.objects.create(
part=cls.tested_part_wo_required_test,
test_name="Required test template",
description="Required test template description",
required=False,
requires_value=False,
requires_attachment=False
)

ref = generate_next_build_reference()

cls.build_wo_tests_trackable = Build.objects.create(
reference=ref,
title="This is a build",
part=cls.tested_part_wo_required_test,
quantity=1,
issued_by=get_user_model().objects.get(pk=1),
)

cls.stockitem_wo_required_test = StockItem.objects.create(
part=cls.tested_part_wo_required_test,
quantity=1,
is_building=True,
serial=uuid.uuid4(),
build=cls.build_wo_tests_trackable
)

cls.sub_part_1 = Part.objects.create(
name="Widget A",
description="A widget",
Expand Down Expand Up @@ -245,7 +315,7 @@ def test_next_ref(self):

def test_init(self):
"""Perform some basic tests before we start the ball rolling"""
self.assertEqual(StockItem.objects.count(), 10)
self.assertEqual(StockItem.objects.count(), 12)

# Build is PENDING
self.assertEqual(self.build.status, status.BuildStatus.PENDING)
Expand Down Expand Up @@ -558,7 +628,7 @@ def test_complete(self):
self.assertEqual(BuildItem.objects.count(), 0)

# New stock items should have been created!
self.assertEqual(StockItem.objects.count(), 13)
self.assertEqual(StockItem.objects.count(), 15)

# This stock item has been marked as "consumed"
item = StockItem.objects.get(pk=self.stock_1_1.pk)
Expand All @@ -573,6 +643,26 @@ def test_complete(self):
for output in outputs:
self.assertFalse(output.is_building)

def test_complete_with_required_tests(self):
"""Test the prevention completion when a required test is missing feature"""

# with required tests incompleted the save should fail
common.models.InvenTreeSetting.set_setting('PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS', True, change_user=None)

with self.assertRaises(ValidationError):
self.build_w_tests_trackable.complete_build_output(self.stockitem_with_required_test, None)

# 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,
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
self.build_wo_tests_trackable.complete_build_output(self.stockitem_wo_required_test, None)

def test_overdue_notification(self):
"""Test sending of notifications when a build order is overdue."""
self.build.target_date = datetime.now().date() - timedelta(days=1)
Expand Down
Loading

0 comments on commit b131d54

Please sign in to comment.