diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py index b905e86795..3cd5aa74f7 100644 --- a/InvenTree/InvenTree/middleware.py +++ b/InvenTree/InvenTree/middleware.py @@ -21,28 +21,15 @@ class AuthRequiredMiddleware(object): assert hasattr(request, 'user') - response = self.get_response(request) + # API requests are handled by the DRF library + if request.path_info.startswith('/api/'): + return self.get_response(request) if not request.user.is_authenticated: """ Normally, a web-based session would use csrftoken based authentication. - However when running an external application (e.g. the InvenTree app), - we wish to use token-based auth to grab media files. - - So, we will allow token-based authentication but ONLY for the /media/ directory. - - What problem is this solving? - - The InvenTree mobile app does not use csrf token auth - - Token auth is used by the Django REST framework, but that is under the /api/ endpoint - - Media files (e.g. Part images) are required to be served to the app - - We do not want to make /media/ files accessible without login! - - There is PROBABLY a better way of going about this? - a) Allow token-based authentication against a user? - b) Serve /media/ files in a duplicate location e.g. /api/media/ ? - c) Is there a "standard" way of solving this problem? - - My [google|stackoverflow]-fu has failed me. So this hack has been created. + However when running an external application (e.g. the InvenTree app or Python library), + we must validate the user token manually. """ authorized = False @@ -56,20 +43,23 @@ class AuthRequiredMiddleware(object): elif request.path_info.startswith('/accounts/'): authorized = True - elif 'Authorization' in request.headers.keys(): - auth = request.headers['Authorization'].strip() + elif 'Authorization' in request.headers.keys() or 'authorization' in request.headers.keys(): + auth = request.headers.get('Authorization', request.headers.get('authorization')).strip() - if auth.startswith('Token') and len(auth.split()) == 2: - token = auth.split()[1] + if auth.lower().startswith('token') and len(auth.split()) == 2: + token_key = auth.split()[1] # Does the provided token match a valid user? - if Token.objects.filter(key=token).exists(): + try: + token = Token.objects.get(key=token_key) - allowed = ['/api/', '/media/'] + # Provide the user information to the request + request.user = token.user + authorized = True - # Only allow token-auth for /media/ or /static/ dirs! - if any([request.path_info.startswith(a) for a in allowed]): - authorized = True + except Token.DoesNotExist: + logger.warning(f"Access denied for unknown token {token_key}") + pass # No authorization was found for the request if not authorized: @@ -92,8 +82,7 @@ class AuthRequiredMiddleware(object): return redirect('%s?next=%s' % (reverse_lazy('login'), request.path)) - # Code to be executed for each request/response after - # the view is called. + response = self.get_response(request) return response diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index cab87ee64b..24631dc9e5 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -6,7 +6,8 @@ import json import requests import logging -from datetime import datetime, timedelta +from datetime import timedelta +from django.utils import timezone from django.core.exceptions import AppRegistryNotReady from django.db.utils import OperationalError, ProgrammingError @@ -51,11 +52,14 @@ def schedule_task(taskname, **kwargs): pass -def offload_task(taskname, *args, **kwargs): +def offload_task(taskname, force_sync=False, *args, **kwargs): """ - Create an AsyncTask. - This is different to a 'scheduled' task, - in that it only runs once! + Create an AsyncTask if workers are running. + This is different to a 'scheduled' task, + in that it only runs once! + + If workers are not running or force_sync flag + is set then the task is ran synchronously. """ try: @@ -63,10 +67,48 @@ def offload_task(taskname, *args, **kwargs): except (AppRegistryNotReady): logger.warning("Could not offload task - app registry not ready") return + import importlib + from InvenTree.status import is_worker_running - task = AsyncTask(taskname, *args, **kwargs) + if is_worker_running() and not force_sync: + # Running as asynchronous task + try: + task = AsyncTask(taskname, *args, **kwargs) + task.run() + except ImportError: + logger.warning(f"WARNING: '{taskname}' not started - Function not found") + else: + # Split path + try: + app, mod, func = taskname.split('.') + app_mod = app + '.' + mod + except ValueError: + logger.warning(f"WARNING: '{taskname}' not started - Malformed function path") + return - task.run() + # Import module from app + try: + _mod = importlib.import_module(app_mod) + except ModuleNotFoundError: + logger.warning(f"WARNING: '{taskname}' not started - No module named '{app_mod}'") + return + + # Retrieve function + try: + _func = getattr(_mod, func) + except AttributeError: + # getattr does not work for local import + _func = None + + try: + if not _func: + _func = eval(func) + except NameError: + logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'") + return + + # Workers are not running: run it as synchronous task + _func() def heartbeat(): @@ -84,7 +126,7 @@ def heartbeat(): except AppRegistryNotReady: return - threshold = datetime.now() - timedelta(minutes=30) + threshold = timezone.now() - timedelta(minutes=30) # Delete heartbeat results more than half an hour old, # otherwise they just create extra noise @@ -108,7 +150,7 @@ def delete_successful_tasks(): logger.info("Could not perform 'delete_successful_tasks' - App registry not ready") return - threshold = datetime.now() - timedelta(days=30) + threshold = timezone.now() - timedelta(days=30) results = Success.objects.filter( started__lte=threshold diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 69deb8fd97..0528c6c694 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -31,8 +31,6 @@ from stock.models import StockLocation, StockItem from common.models import InvenTreeSetting, ColorTheme from users.models import check_user_role, RuleSet -import InvenTree.tasks - from .forms import DeleteForm, EditUserForm, SetPasswordForm from .forms import SettingCategorySelectForm from .helpers import str2bool @@ -827,8 +825,13 @@ class CurrencyRefreshView(RedirectView): On a POST request we will attempt to refresh the exchange rates """ - # Will block for a little bit - InvenTree.tasks.update_exchange_rates() + from InvenTree.tasks import offload_task + + # Define associated task from InvenTree.tasks list of methods + taskname = 'InvenTree.tasks.update_exchange_rates' + + # Run it + offload_task(taskname, force_sync=True) return redirect(reverse_lazy('settings')) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 3b731381b8..2531781631 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -9,6 +9,8 @@ import os from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator +from django.core.exceptions import ValidationError + from django.db import models from django.db.models import Sum, Q, UniqueConstraint @@ -473,12 +475,32 @@ class SupplierPart(models.Model): def get_absolute_url(self): return reverse('supplier-part-detail', kwargs={'pk': self.id}) + def api_instance_filters(self): + + return { + 'manufacturer_part': { + 'part': self.part.pk + } + } + class Meta: unique_together = ('part', 'supplier', 'SKU') # This model was moved from the 'Part' app db_table = 'part_supplierpart' + def clean(self): + + super().clean() + + # Ensure that the linked manufacturer_part points to the same part! + if self.manufacturer_part and self.part: + + if not self.manufacturer_part.part == self.part: + raise ValidationError({ + 'manufacturer_part': _("Linked manufacturer part must reference the same base part"), + }) + part = models.ForeignKey('part.Part', on_delete=models.CASCADE, related_name='supplier_parts', verbose_name=_('Base Part'), diff --git a/InvenTree/part/templates/part/bom_upload/match_fields.html b/InvenTree/part/templates/part/bom_upload/match_fields.html index d1f325aaee..3bd51c1855 100644 --- a/InvenTree/part/templates/part/bom_upload/match_fields.html +++ b/InvenTree/part/templates/part/bom_upload/match_fields.html @@ -5,7 +5,7 @@ {% block form_alert %} {% if missing_columns and missing_columns|length > 0 %} -