From 9ec626b650f33992d90f62aadcf5389741548594 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 16 May 2022 17:29:14 +0200 Subject: [PATCH 01/21] Also allow non string references Fixes #3005 --- InvenTree/InvenTree/tasks.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index c156d421ab..8af30264d3 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -68,6 +68,10 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): import importlib from InvenTree.status import is_worker_running + # make sure the taskname is a string + if not isinstance(taskname, str): + taskname = str(taskname) + if is_worker_running() and not force_sync: # pragma: no cover # Running as asynchronous task try: From 0f5c03e44cb8500e07f87bfd1d46ccd6cb8bc083 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 16 May 2022 17:45:51 +0200 Subject: [PATCH 02/21] use direct import instead of text for offload --- InvenTree/InvenTree/tasks.py | 3 ++- InvenTree/InvenTree/views.py | 8 ++------ InvenTree/build/models.py | 3 ++- InvenTree/common/test_tasks.py | 3 ++- InvenTree/label/api.py | 3 ++- InvenTree/part/models.py | 3 ++- InvenTree/part/tasks.py | 3 ++- InvenTree/stock/models.py | 5 +++-- 8 files changed, 17 insertions(+), 14 deletions(-) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index 8af30264d3..cb52245740 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -11,6 +11,7 @@ from django.utils import timezone from django.core.exceptions import AppRegistryNotReady from django.db.utils import OperationalError, ProgrammingError +from django.core import mail as django_mail logger = logging.getLogger("inventree") @@ -292,7 +293,7 @@ def send_email(subject, body, recipients, from_email=None, html_message=None): recipients = [recipients] offload_task( - 'django.core.mail.send_mail', + django_mail.send_mail, subject, body, from_email, diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 6291b321a8..f15452c697 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -797,13 +797,9 @@ class CurrencyRefreshView(RedirectView): On a POST request we will attempt to refresh the exchange rates """ - from InvenTree.tasks import offload_task + from InvenTree.tasks import offload_task, update_exchange_rates - # Define associated task from InvenTree.tasks list of methods - taskname = 'InvenTree.tasks.update_exchange_rates' - - # Run it - offload_task(taskname, force_sync=True) + offload_task(update_exchange_rates, force_sync=True) return redirect(reverse_lazy('settings')) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index a1517d73dd..e7a1bbd1b3 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -41,6 +41,7 @@ from plugin.events import trigger_event from part import models as PartModels from stock import models as StockModels from users import models as UserModels +from . import tasks as build_tasks def get_next_build_number(): @@ -1146,7 +1147,7 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs): # A new Build has just been created # Run checks on required parts - InvenTree.tasks.offload_task('build.tasks.check_build_stock', instance) + InvenTree.tasks.offload_task(build_tasks.check_build_stock, instance) class BuildOrderAttachment(InvenTreeAttachment): diff --git a/InvenTree/common/test_tasks.py b/InvenTree/common/test_tasks.py index 3f85316c41..c28a1d19fe 100644 --- a/InvenTree/common/test_tasks.py +++ b/InvenTree/common/test_tasks.py @@ -2,6 +2,7 @@ from django.test import TestCase from common.models import NotificationEntry +from . import tasks as common_tasks from InvenTree.tasks import offload_task @@ -14,4 +15,4 @@ class TaskTest(TestCase): # check empty run self.assertEqual(NotificationEntry.objects.all().count(), 0) - offload_task('common.tasks.delete_old_notifications',) + offload_task(common_tasks.delete_old_notifications,) diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index cb4b939157..34ced6a3cd 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -24,6 +24,7 @@ from plugin.registry import registry from stock.models import StockItem, StockLocation from part.models import Part +from plugin.base.label import label as plugin_label from .models import StockItemLabel, StockLocationLabel, PartLabel from .serializers import StockItemLabelSerializer, StockLocationLabelSerializer, PartLabelSerializer @@ -156,7 +157,7 @@ class LabelPrintMixin: # Offload a background task to print the provided label offload_task( - 'plugin.base.label.label.print_label', + plugin_label.print_label, plugin.plugin_slug(), image, label_instance=label_instance, diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 96ffa581f4..229b25bf16 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -63,6 +63,7 @@ from stock import models as StockModels import common.models import part.settings as part_settings +from part import tasks as part_tasks logger = logging.getLogger("inventree") @@ -2298,7 +2299,7 @@ def after_save_part(sender, instance: Part, created, **kwargs): # Check part stock only if we are *updating* the part (not creating it) # Run this check in the background - InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance) + InvenTree.tasks.offload_task(part_tasks.notify_low_stock_if_required, instance) class PartAttachment(InvenTreeAttachment): diff --git a/InvenTree/part/tasks.py b/InvenTree/part/tasks.py index b158d26ad3..c8615bd4a2 100644 --- a/InvenTree/part/tasks.py +++ b/InvenTree/part/tasks.py @@ -10,6 +10,7 @@ import InvenTree.tasks import common.notifications import part.models +from part import tasks as part_tasks logger = logging.getLogger("inventree") @@ -49,6 +50,6 @@ def notify_low_stock_if_required(part: part.models.Part): for p in parts: if p.is_part_low_on_stock(): InvenTree.tasks.offload_task( - 'part.tasks.notify_low_stock', + part_tasks.notify_low_stock, p ) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index a46d43b007..69be97cf4b 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -49,6 +49,7 @@ from users.models import Owner from company import models as CompanyModels from part import models as PartModels +from part import tasks as part_tasks class StockLocation(InvenTreeTree): @@ -2026,7 +2027,7 @@ def after_delete_stock_item(sender, instance: StockItem, **kwargs): if not InvenTree.ready.isImportingData(): # Run this check in the background - InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance.part) + InvenTree.tasks.offload_task(part_tasks.notify_low_stock_if_required, instance.part) @receiver(post_save, sender=StockItem, dispatch_uid='stock_item_post_save_log') @@ -2037,7 +2038,7 @@ def after_save_stock_item(sender, instance: StockItem, created, **kwargs): if not InvenTree.ready.isImportingData(): # Run this check in the background - InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance.part) + InvenTree.tasks.offload_task(part_tasks.notify_low_stock_if_required, instance.part) class StockItemAttachment(InvenTreeAttachment): From a9cfdf8fdb6853f175cdc31abc2dec91ec6dcf3a Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 16 May 2022 17:52:36 +0200 Subject: [PATCH 03/21] fix import --- InvenTree/part/tasks.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/InvenTree/part/tasks.py b/InvenTree/part/tasks.py index c8615bd4a2..d0c91ac79d 100644 --- a/InvenTree/part/tasks.py +++ b/InvenTree/part/tasks.py @@ -10,7 +10,6 @@ import InvenTree.tasks import common.notifications import part.models -from part import tasks as part_tasks logger = logging.getLogger("inventree") @@ -50,6 +49,6 @@ def notify_low_stock_if_required(part: part.models.Part): for p in parts: if p.is_part_low_on_stock(): InvenTree.tasks.offload_task( - part_tasks.notify_low_stock, + notify_low_stock, p ) From 18a263ff75e9e26fe4385d09eec3c9b58027776e Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 16 May 2022 17:55:45 +0200 Subject: [PATCH 04/21] do a local import --- InvenTree/part/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 67afef4923..fa0f1816f8 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -59,7 +59,6 @@ from order import models as OrderModels from company.models import SupplierPart import part.settings as part_settings from stock import models as StockModels -from part import tasks as part_tasks from plugin.models import MetadataMixin @@ -2291,6 +2290,7 @@ def after_save_part(sender, instance: Part, created, **kwargs): """ Function to be executed after a Part is saved """ + from part import tasks as part_tasks if not created and not InvenTree.ready.isImportingData(): # Check part stock only if we are *updating* the part (not creating it) From cce3d3a35deaae30adda964d2a3f8581e4ad0362 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 16 May 2022 18:01:16 +0200 Subject: [PATCH 05/21] make imports on function level --- InvenTree/build/models.py | 2 +- InvenTree/stock/models.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index fa81111f18..0f539dc158 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -39,7 +39,6 @@ from plugin.events import trigger_event from part import models as PartModels from stock import models as StockModels from users import models as UserModels -from . import tasks as build_tasks def get_next_build_number(): @@ -1140,6 +1139,7 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs): """ Callback function to be executed after a Build instance is saved """ + from . import tasks as build_tasks if created: # A new Build has just been created diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index b3eed54c5f..6cd0469b75 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -46,7 +46,6 @@ from users.models import Owner from company import models as CompanyModels from part import models as PartModels -from part import tasks as part_tasks class StockLocation(MetadataMixin, InvenTreeTree): @@ -2021,6 +2020,7 @@ def after_delete_stock_item(sender, instance: StockItem, **kwargs): """ Function to be executed after a StockItem object is deleted """ + from part import tasks as part_tasks if not InvenTree.ready.isImportingData(): # Run this check in the background @@ -2032,6 +2032,7 @@ def after_save_stock_item(sender, instance: StockItem, created, **kwargs): """ Hook function to be executed after StockItem object is saved/updated """ + from part import tasks as part_tasks if not InvenTree.ready.isImportingData(): # Run this check in the background From 30dbfa9a4fb5399d6915eafd8a5da1c710ffaa49 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 16 May 2022 18:21:58 +0200 Subject: [PATCH 06/21] add tests for InvenTree.tasks --- InvenTree/InvenTree/test_tasks.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/InvenTree/InvenTree/test_tasks.py b/InvenTree/InvenTree/test_tasks.py index e9c9d9f01c..c00931b237 100644 --- a/InvenTree/InvenTree/test_tasks.py +++ b/InvenTree/InvenTree/test_tasks.py @@ -41,3 +41,22 @@ class ScheduledTaskTests(TestCase): # But the 'minutes' should have been updated t = Schedule.objects.get(func=task) self.assertEqual(t.minutes, 5) + +class InvenTreeTaskTests(TestCase): + """Unit tests for tasks""" + + def test_task_hearbeat(self, name): + """Test the task heartbeat""" + InvenTree.tasks.offload_task(InvenTree.tasks.heartbeat) + + def test_task_delete_successful_tasks(self, name): + """Test the task delete_successful_tasks""" + InvenTree.tasks.offload_task(InvenTree.tasks.delete_successful_tasks) + + def test_task_delete_old_error_logs(self, name): + """Test the task delete_old_error_logs""" + InvenTree.tasks.offload_task(InvenTree.tasks.delete_old_error_logs) + + def test_task_check_for_updates(self, name): + """Test the task check_for_updates""" + InvenTree.tasks.offload_task(InvenTree.tasks.check_for_updates) From 7fc408cf607c1bbcd81403bcd9d25742bdb8e305 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 16 May 2022 18:29:41 +0200 Subject: [PATCH 07/21] move exceptions up --- InvenTree/InvenTree/tasks.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index e2869c6a46..bdcf2fe413 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -68,6 +68,11 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): import importlib from InvenTree.status import is_worker_running + except AppRegistryNotReady: # pragma: no cover + logger.warning(f"Could not offload task '{taskname}' - app registry not ready") + return + except (OperationalError, ProgrammingError): # pragma: no cover + logger.warning(f"Could not offload task '{taskname}' - database not ready") # make sure the taskname is a string if not isinstance(taskname, str): @@ -113,11 +118,6 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): # Workers are not running: run it as synchronous task _func(*args, **kwargs) - except AppRegistryNotReady: # pragma: no cover - logger.warning(f"Could not offload task '{taskname}' - app registry not ready") - return - except (OperationalError, ProgrammingError): # pragma: no cover - logger.warning(f"Could not offload task '{taskname}' - database not ready") def heartbeat(): From 763cd13b7ccefbf8b1da758e0b2a497f9046db57 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 16 May 2022 18:30:37 +0200 Subject: [PATCH 08/21] use a function if it was passed --- InvenTree/InvenTree/tasks.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index bdcf2fe413..cd06acfe3d 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -74,17 +74,18 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): except (OperationalError, ProgrammingError): # pragma: no cover logger.warning(f"Could not offload task '{taskname}' - database not ready") - # make sure the taskname is a string - if not isinstance(taskname, str): - taskname = str(taskname) + if is_worker_running() and not force_sync: # pragma: no cover + # 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: - if is_worker_running() and not force_sync: # pragma: no cover - # Running as asynchronous task - try: - task = AsyncTask(taskname, *args, **kwargs) - task.run() - except ImportError: - logger.warning(f"WARNING: '{taskname}' not started - Function not found") + if isinstance(taskname, function): + # function was passed - use that + _func = taskname else: # Split path try: @@ -115,8 +116,8 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'") return - # Workers are not running: run it as synchronous task - _func(*args, **kwargs) + # Workers are not running: run it as synchronous task + _func(*args, **kwargs) From 79b4b23a07ce4a7103afbcfc8d82d0a443326a0a Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 16 May 2022 18:33:49 +0200 Subject: [PATCH 09/21] pep fix --- InvenTree/InvenTree/tasks.py | 1 - InvenTree/InvenTree/test_tasks.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index cd06acfe3d..97b2393df5 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -120,7 +120,6 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): _func(*args, **kwargs) - def heartbeat(): """ Simple task which runs at 5 minute intervals, diff --git a/InvenTree/InvenTree/test_tasks.py b/InvenTree/InvenTree/test_tasks.py index c00931b237..f571eee8cd 100644 --- a/InvenTree/InvenTree/test_tasks.py +++ b/InvenTree/InvenTree/test_tasks.py @@ -42,6 +42,7 @@ class ScheduledTaskTests(TestCase): t = Schedule.objects.get(func=task) self.assertEqual(t.minutes, 5) + class InvenTreeTaskTests(TestCase): """Unit tests for tasks""" From 07711b8e747110541386dbbe383d1e37b9f475ce Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 16 May 2022 18:34:02 +0200 Subject: [PATCH 10/21] fix check --- InvenTree/InvenTree/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index 97b2393df5..40e63108e9 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -83,7 +83,7 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): logger.warning(f"WARNING: '{taskname}' not started - Function not found") else: - if isinstance(taskname, function): + if callable(taskname): # function was passed - use that _func = taskname else: From fc8a63325e803f4a3843ba7f07ad88d75d246fd9 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 16 May 2022 18:57:27 +0200 Subject: [PATCH 11/21] that was a stupid misstake --- InvenTree/InvenTree/test_tasks.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/test_tasks.py b/InvenTree/InvenTree/test_tasks.py index f571eee8cd..0d043bd9f8 100644 --- a/InvenTree/InvenTree/test_tasks.py +++ b/InvenTree/InvenTree/test_tasks.py @@ -46,18 +46,18 @@ class ScheduledTaskTests(TestCase): class InvenTreeTaskTests(TestCase): """Unit tests for tasks""" - def test_task_hearbeat(self, name): + def test_task_hearbeat(self): """Test the task heartbeat""" InvenTree.tasks.offload_task(InvenTree.tasks.heartbeat) - def test_task_delete_successful_tasks(self, name): + def test_task_delete_successful_tasks(self): """Test the task delete_successful_tasks""" InvenTree.tasks.offload_task(InvenTree.tasks.delete_successful_tasks) - def test_task_delete_old_error_logs(self, name): + def test_task_delete_old_error_logs(self): """Test the task delete_old_error_logs""" InvenTree.tasks.offload_task(InvenTree.tasks.delete_old_error_logs) - def test_task_check_for_updates(self, name): + def test_task_check_for_updates(self): """Test the task check_for_updates""" InvenTree.tasks.offload_task(InvenTree.tasks.check_for_updates) From eb24bf78b8ee0ed12d7e69ea474e5711527c0cd1 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 16 May 2022 19:39:43 +0200 Subject: [PATCH 12/21] external api failures are not covered --- InvenTree/InvenTree/tasks.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index 40e63108e9..6dc8b7d297 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -210,25 +210,25 @@ def check_for_updates(): response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest') if response.status_code != 200: - raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}') + raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}') # pragma: no cover data = json.loads(response.text) tag = data.get('tag_name', None) if not tag: - raise ValueError("'tag_name' missing from GitHub response") + raise ValueError("'tag_name' missing from GitHub response") # pragma: no cover match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag) - if len(match.groups()) != 3: + if len(match.groups()) != 3: # pragma: no cover logger.warning(f"Version '{tag}' did not match expected pattern") return latest_version = [int(x) for x in match.groups()] if len(latest_version) != 3: - raise ValueError(f"Version '{tag}' is not correct format") + raise ValueError(f"Version '{tag}' is not correct format") # pragma: no cover logger.info(f"Latest InvenTree version: '{tag}'") From 2ec59a6ad22200906ab2b520f042f7e0fe64fd1b Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 16 May 2022 19:45:00 +0200 Subject: [PATCH 13/21] extend tests for task_delete_succ --- InvenTree/InvenTree/test_tasks.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/InvenTree/InvenTree/test_tasks.py b/InvenTree/InvenTree/test_tasks.py index 0d043bd9f8..5a5e32d08d 100644 --- a/InvenTree/InvenTree/test_tasks.py +++ b/InvenTree/InvenTree/test_tasks.py @@ -2,6 +2,9 @@ Unit tests for task management """ +from datetime import timedelta + +from django.utils import timezone from django.test import TestCase from django_q.models import Schedule @@ -52,7 +55,19 @@ class InvenTreeTaskTests(TestCase): def test_task_delete_successful_tasks(self): """Test the task delete_successful_tasks""" + from django_q.models import Success + + Success.objects.create( + name='abc', + func='abc', + started=timezone.now() - timedelta(days=31) + ) InvenTree.tasks.offload_task(InvenTree.tasks.delete_successful_tasks) + threshold = timezone.now() - timedelta(days=30) + results = Success.objects.filter( + started__lte=threshold + ) + self.assertEqual(len(results, 0)) def test_task_delete_old_error_logs(self): """Test the task delete_old_error_logs""" From f3c4720f5b316e4d4778bf2df5b8285bae6b0fb9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 May 2022 23:41:33 +0200 Subject: [PATCH 14/21] extend update check --- InvenTree/InvenTree/test_tasks.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/InvenTree/InvenTree/test_tasks.py b/InvenTree/InvenTree/test_tasks.py index 5a5e32d08d..74eee7c73d 100644 --- a/InvenTree/InvenTree/test_tasks.py +++ b/InvenTree/InvenTree/test_tasks.py @@ -9,6 +9,7 @@ from django.test import TestCase from django_q.models import Schedule import InvenTree.tasks +from common.models import InvenTreeSetting class ScheduledTaskTests(TestCase): @@ -75,4 +76,13 @@ class InvenTreeTaskTests(TestCase): def test_task_check_for_updates(self): """Test the task check_for_updates""" + # Check that setting should be empty + self.assertEqual(InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION'), '') + + # Get new version InvenTree.tasks.offload_task(InvenTree.tasks.check_for_updates) + + # Check that setting is not empty + response = InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION') + self.assertNotEqual(response, '') + self.assertTrue(bool(response)) From 09bda7e516b247cbc69541fa9e3339d7663488d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 May 2022 23:42:09 +0200 Subject: [PATCH 15/21] add checks for old_error_log --- InvenTree/InvenTree/test_tasks.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/InvenTree/InvenTree/test_tasks.py b/InvenTree/InvenTree/test_tasks.py index 74eee7c73d..9952eccff4 100644 --- a/InvenTree/InvenTree/test_tasks.py +++ b/InvenTree/InvenTree/test_tasks.py @@ -8,10 +8,16 @@ from django.utils import timezone from django.test import TestCase from django_q.models import Schedule +from error_report.models import Error + import InvenTree.tasks from common.models import InvenTreeSetting +threshold = timezone.now() - timedelta(days=30) +threshold_low = threshold - timedelta(days=1) + + class ScheduledTaskTests(TestCase): """ Unit tests for scheduled tasks @@ -72,8 +78,23 @@ class InvenTreeTaskTests(TestCase): def test_task_delete_old_error_logs(self): """Test the task delete_old_error_logs""" + + # Create error + error_obj = Error.objects.create() + error_obj.when = threshold_low + error_obj.save() + + # Check that it is not empty + errors = Error.objects.filter(when__lte=threshold,) + self.assertNotEqual(len(errors), 0) + + # Run action InvenTree.tasks.offload_task(InvenTree.tasks.delete_old_error_logs) + # Check that it is empty again + errors = Error.objects.filter(when__lte=threshold,) + self.assertEqual(len(errors), 0) + def test_task_check_for_updates(self): """Test the task check_for_updates""" # Check that setting should be empty From 9f3fdcb59074d81235e5fb7f086743f7cc5d7f78 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 May 2022 23:43:07 +0200 Subject: [PATCH 16/21] make test simpler --- InvenTree/InvenTree/test_tasks.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/InvenTree/InvenTree/test_tasks.py b/InvenTree/InvenTree/test_tasks.py index 9952eccff4..bbcf7a4799 100644 --- a/InvenTree/InvenTree/test_tasks.py +++ b/InvenTree/InvenTree/test_tasks.py @@ -64,16 +64,9 @@ class InvenTreeTaskTests(TestCase): """Test the task delete_successful_tasks""" from django_q.models import Success - Success.objects.create( - name='abc', - func='abc', - started=timezone.now() - timedelta(days=31) - ) + Success.objects.create(name='abc', func='abc', stopped=threshold, started=threshold_low) InvenTree.tasks.offload_task(InvenTree.tasks.delete_successful_tasks) - threshold = timezone.now() - timedelta(days=30) - results = Success.objects.filter( - started__lte=threshold - ) + results = Success.objects.filter(started__lte=threshold) self.assertEqual(len(results, 0)) def test_task_delete_old_error_logs(self): From 3e859f7abbcc0ba0b8bbc2815b087ad999706043 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 May 2022 23:51:17 +0200 Subject: [PATCH 17/21] fix assertation --- InvenTree/InvenTree/test_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/test_tasks.py b/InvenTree/InvenTree/test_tasks.py index bbcf7a4799..0b6cd08ae1 100644 --- a/InvenTree/InvenTree/test_tasks.py +++ b/InvenTree/InvenTree/test_tasks.py @@ -67,7 +67,7 @@ class InvenTreeTaskTests(TestCase): Success.objects.create(name='abc', func='abc', stopped=threshold, started=threshold_low) InvenTree.tasks.offload_task(InvenTree.tasks.delete_successful_tasks) results = Success.objects.filter(started__lte=threshold) - self.assertEqual(len(results, 0)) + self.assertEqual(len(results), 0) def test_task_delete_old_error_logs(self): """Test the task delete_old_error_logs""" From b3e42f5fb04b992187cc1e01ea0b6b8d80953ced Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 May 2022 00:13:39 +0200 Subject: [PATCH 18/21] Add tests for offloading --- InvenTree/InvenTree/test_tasks.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/InvenTree/InvenTree/test_tasks.py b/InvenTree/InvenTree/test_tasks.py index 0b6cd08ae1..1502037dba 100644 --- a/InvenTree/InvenTree/test_tasks.py +++ b/InvenTree/InvenTree/test_tasks.py @@ -53,9 +53,31 @@ class ScheduledTaskTests(TestCase): self.assertEqual(t.minutes, 5) +def get_result(): + """Demo function for test_offloading""" + return 'abc' + + class InvenTreeTaskTests(TestCase): """Unit tests for tasks""" + def test_offloading(self): + """Test task offloading""" + + # Run with function ref + InvenTree.tasks.offload_task(get_result) + + # Run with string ref + InvenTree.tasks.offload_task('InvenTree.test_tasks.get_result') + + # Error runs + # Malformed taskname + InvenTree.tasks.offload_task('InvenTree') + # Non exsistent app + InvenTree.tasks.offload_task('InvenTreeABC.test_tasks.doesnotmatter') + # Non exsistent function + InvenTree.tasks.offload_task('InvenTree.test_tasks.doesnotexsist') + def test_task_hearbeat(self): """Test the task heartbeat""" InvenTree.tasks.offload_task(InvenTree.tasks.heartbeat) From 09af7d964dec7f107a6924a2a542dfb91aae6a3d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 May 2022 00:24:02 +0200 Subject: [PATCH 19/21] raise a warning for assertations --- InvenTree/InvenTree/tasks.py | 21 ++++++++++++++++----- InvenTree/InvenTree/test_tasks.py | 11 ++++++++--- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index 6dc8b7d297..a750c393e6 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import re import json +import warnings import requests import logging @@ -12,6 +13,7 @@ from django.utils import timezone from django.core.exceptions import AppRegistryNotReady from django.db.utils import OperationalError, ProgrammingError from django.core import mail as django_mail +from django.conf import settings logger = logging.getLogger("inventree") @@ -53,6 +55,15 @@ def schedule_task(taskname, **kwargs): pass +def raise_warning(msg): + """Log and raise a warning""" + logger.warning(msg) + + # If testing is running raise a warning that can be asserted + if settings.TESTING: + warnings.warn(msg) + + def offload_task(taskname, *args, force_sync=False, **kwargs): """ Create an AsyncTask if workers are running. @@ -72,7 +83,7 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): logger.warning(f"Could not offload task '{taskname}' - app registry not ready") return except (OperationalError, ProgrammingError): # pragma: no cover - logger.warning(f"Could not offload task '{taskname}' - database not ready") + raise_warning(f"Could not offload task '{taskname}' - database not ready") if is_worker_running() and not force_sync: # pragma: no cover # Running as asynchronous task @@ -80,7 +91,7 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): task = AsyncTask(taskname, *args, **kwargs) task.run() except ImportError: - logger.warning(f"WARNING: '{taskname}' not started - Function not found") + raise_warning(f"WARNING: '{taskname}' not started - Function not found") else: if callable(taskname): @@ -92,14 +103,14 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): app, mod, func = taskname.split('.') app_mod = app + '.' + mod except ValueError: - logger.warning(f"WARNING: '{taskname}' not started - Malformed function path") + raise_warning(f"WARNING: '{taskname}' not started - Malformed function path") return # 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}'") + raise_warning(f"WARNING: '{taskname}' not started - No module named '{app_mod}'") return # Retrieve function @@ -113,7 +124,7 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): if not _func: _func = eval(func) # pragma: no cover except NameError: - logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'") + raise_warning(f"WARNING: '{taskname}' not started - No function named '{func}'") return # Workers are not running: run it as synchronous task diff --git a/InvenTree/InvenTree/test_tasks.py b/InvenTree/InvenTree/test_tasks.py index 1502037dba..eb307f2292 100644 --- a/InvenTree/InvenTree/test_tasks.py +++ b/InvenTree/InvenTree/test_tasks.py @@ -72,11 +72,16 @@ class InvenTreeTaskTests(TestCase): # Error runs # Malformed taskname - InvenTree.tasks.offload_task('InvenTree') + with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree' not started - Malformed function path"): + InvenTree.tasks.offload_task('InvenTree') + # Non exsistent app - InvenTree.tasks.offload_task('InvenTreeABC.test_tasks.doesnotmatter') + with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTreeABC.test_tasks.doesnotmatter' not started - No module named 'InvenTreeABC'"): + InvenTree.tasks.offload_task('InvenTreeABC.test_tasks.doesnotmatter') + # Non exsistent function - InvenTree.tasks.offload_task('InvenTree.test_tasks.doesnotexsist') + with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree.test_tasks.doesnotexsist' not started - No function named 'doesnotexsist'"): + InvenTree.tasks.offload_task('InvenTree.test_tasks.doesnotexsist') def test_task_hearbeat(self): """Test the task heartbeat""" From 728cccc469db3c583a83ebbbb60d7f9594156f65 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 May 2022 00:53:51 +0200 Subject: [PATCH 20/21] fix assertation --- InvenTree/InvenTree/test_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/test_tasks.py b/InvenTree/InvenTree/test_tasks.py index eb307f2292..c8497e2241 100644 --- a/InvenTree/InvenTree/test_tasks.py +++ b/InvenTree/InvenTree/test_tasks.py @@ -76,7 +76,7 @@ class InvenTreeTaskTests(TestCase): InvenTree.tasks.offload_task('InvenTree') # Non exsistent app - with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTreeABC.test_tasks.doesnotmatter' not started - No module named 'InvenTreeABC'"): + with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTreeABC.test_tasks.doesnotmatter' not started - No module named 'InvenTreeABC.test_tasks'"): InvenTree.tasks.offload_task('InvenTreeABC.test_tasks.doesnotmatter') # Non exsistent function From fb55f5d2a2e0a243547a7a7d72002bd8fac24e90 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 May 2022 01:32:08 +0200 Subject: [PATCH 21/21] convert remaining function --- InvenTree/plugin/base/event/events.py | 4 ++-- InvenTree/plugin/base/locate/api.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/plugin/base/event/events.py b/InvenTree/plugin/base/event/events.py index 4b9c44521d..4dea7525b3 100644 --- a/InvenTree/plugin/base/event/events.py +++ b/InvenTree/plugin/base/event/events.py @@ -37,7 +37,7 @@ def trigger_event(event, *args, **kwargs): logger.debug(f"Event triggered: '{event}'") offload_task( - 'plugin.events.register_event', + register_event, event, *args, **kwargs @@ -72,7 +72,7 @@ def register_event(event, *args, **kwargs): # Offload a separate task for each plugin offload_task( - 'plugin.events.process_event', + process_event, slug, event, *args, diff --git a/InvenTree/plugin/base/locate/api.py b/InvenTree/plugin/base/locate/api.py index 30c6d749a9..020951b283 100644 --- a/InvenTree/plugin/base/locate/api.py +++ b/InvenTree/plugin/base/locate/api.py @@ -56,7 +56,7 @@ class LocatePluginView(APIView): try: StockItem.objects.get(pk=item_pk) - offload_task('plugin.registry.call_function', plugin, 'locate_stock_item', item_pk) + offload_task(registry.call_function, plugin, 'locate_stock_item', item_pk) data['item'] = item_pk @@ -69,7 +69,7 @@ class LocatePluginView(APIView): try: StockLocation.objects.get(pk=location_pk) - offload_task('plugin.registry.call_function', plugin, 'locate_stock_location', location_pk) + offload_task(registry.call_function, plugin, 'locate_stock_location', location_pk) data['location'] = location_pk