mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Fix task register (#3805)
* fix schedule registration * add collection step for tasks * make tasks register configurable * extend docs * Also run InvenTree setup in testing * fix import loading method * fix wrong task registration * do not test * do only distinct testing * ignore import error for coverage
This commit is contained in:
		| @@ -1,8 +1,10 @@ | |||||||
| """AppConfig for inventree app.""" | """AppConfig for inventree app.""" | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
|  | from importlib import import_module | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from django.apps import AppConfig, apps | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.contrib.auth import get_user_model | from django.contrib.auth import get_user_model | ||||||
| from django.core.exceptions import AppRegistryNotReady | from django.core.exceptions import AppRegistryNotReady | ||||||
| @@ -23,10 +25,11 @@ class InvenTreeConfig(AppConfig): | |||||||
|  |  | ||||||
|     def ready(self): |     def ready(self): | ||||||
|         """Setup background tasks and update exchange rates.""" |         """Setup background tasks and update exchange rates.""" | ||||||
|         if canAppAccessDatabase(): |         if canAppAccessDatabase() or settings.TESTING_ENV: | ||||||
|  |  | ||||||
|             self.remove_obsolete_tasks() |             self.remove_obsolete_tasks() | ||||||
|  |  | ||||||
|  |             self.collect_tasks() | ||||||
|             self.start_background_tasks() |             self.start_background_tasks() | ||||||
|  |  | ||||||
|             if not isInTestMode():  # pragma: no cover |             if not isInTestMode():  # pragma: no cover | ||||||
| @@ -54,68 +57,31 @@ class InvenTreeConfig(AppConfig): | |||||||
|  |  | ||||||
|     def start_background_tasks(self): |     def start_background_tasks(self): | ||||||
|         """Start all background tests for InvenTree.""" |         """Start all background tests for InvenTree.""" | ||||||
|         try: |  | ||||||
|             from django_q.models import Schedule |  | ||||||
|         except AppRegistryNotReady:  # pragma: no cover |  | ||||||
|             logger.warning("Cannot start background tasks - app registry not ready") |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         logger.info("Starting background tasks...") |         logger.info("Starting background tasks...") | ||||||
|  |  | ||||||
|         # Remove successful task results from the database |         for task in InvenTree.tasks.tasks.task_list: | ||||||
|  |             ref_name = f'{task.func.__module__}.{task.func.__name__}' | ||||||
|             InvenTree.tasks.schedule_task( |             InvenTree.tasks.schedule_task( | ||||||
|             'InvenTree.tasks.delete_successful_tasks', |                 ref_name, | ||||||
|             schedule_type=Schedule.DAILY, |                 schedule_type=task.interval, | ||||||
|  |                 minutes=task.minutes, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         # Check for InvenTree updates |         logger.info("Started background tasks...") | ||||||
|         InvenTree.tasks.schedule_task( |  | ||||||
|             'InvenTree.tasks.check_for_updates', |  | ||||||
|             schedule_type=Schedule.DAILY |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Heartbeat to let the server know the background worker is running |     def collect_tasks(self): | ||||||
|         InvenTree.tasks.schedule_task( |         """Collect all background tasks.""" | ||||||
|             'InvenTree.tasks.heartbeat', |  | ||||||
|             schedule_type=Schedule.MINUTES, |  | ||||||
|             minutes=15 |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Keep exchange rates up to date |         for app_name, app in apps.app_configs.items(): | ||||||
|         InvenTree.tasks.schedule_task( |             if app_name == 'InvenTree': | ||||||
|             'InvenTree.tasks.update_exchange_rates', |                 continue | ||||||
|             schedule_type=Schedule.DAILY, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Delete old error messages |             if Path(app.path).joinpath('tasks.py').exists(): | ||||||
|         InvenTree.tasks.schedule_task( |                 try: | ||||||
|             'InvenTree.tasks.delete_old_error_logs', |                     import_module(f'{app.module.__package__}.tasks') | ||||||
|             schedule_type=Schedule.DAILY, |                 except Exception as e:  # pragma: no cover | ||||||
|         ) |                     logger.error(f"Error loading tasks for {app_name}: {e}") | ||||||
|  |  | ||||||
|         # Delete old notification records |  | ||||||
|         InvenTree.tasks.schedule_task( |  | ||||||
|             'common.tasks.delete_old_notifications', |  | ||||||
|             schedule_type=Schedule.DAILY, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Check for overdue purchase orders |  | ||||||
|         InvenTree.tasks.schedule_task( |  | ||||||
|             'order.tasks.check_overdue_purchase_orders', |  | ||||||
|             schedule_type=Schedule.DAILY |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Check for overdue sales orders |  | ||||||
|         InvenTree.tasks.schedule_task( |  | ||||||
|             'order.tasks.check_overdue_sales_orders', |  | ||||||
|             schedule_type=Schedule.DAILY, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Check for overdue build orders |  | ||||||
|         InvenTree.tasks.schedule_task( |  | ||||||
|             'build.tasks.check_overdue_build_orders', |  | ||||||
|             schedule_type=Schedule.DAILY |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     def update_exchange_rates(self):  # pragma: no cover |     def update_exchange_rates(self):  # pragma: no cover | ||||||
|         """Update exchange rates each time the server is started. |         """Update exchange rates each time the server is started. | ||||||
|   | |||||||
| @@ -4,7 +4,9 @@ import json | |||||||
| import logging | import logging | ||||||
| import re | import re | ||||||
| import warnings | import warnings | ||||||
|  | from dataclasses import dataclass | ||||||
| from datetime import timedelta | from datetime import timedelta | ||||||
|  | from typing import Callable | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.core import mail as django_mail | from django.core import mail as django_mail | ||||||
| @@ -126,6 +128,79 @@ def offload_task(taskname, *args, force_async=False, force_sync=False, **kwargs) | |||||||
|         _func(*args, **kwargs) |         _func(*args, **kwargs) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass() | ||||||
|  | class ScheduledTask: | ||||||
|  |     """A scheduled task. | ||||||
|  |  | ||||||
|  |     - interval: The interval at which the task should be run | ||||||
|  |     - minutes: The number of minutes between task runs | ||||||
|  |     - func: The function to be run | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     func: Callable | ||||||
|  |     interval: str | ||||||
|  |     minutes: int = None | ||||||
|  |  | ||||||
|  |     MINUTES = "I" | ||||||
|  |     HOURLY = "H" | ||||||
|  |     DAILY = "D" | ||||||
|  |     WEEKLY = "W" | ||||||
|  |     MONTHLY = "M" | ||||||
|  |     QUARTERLY = "Q" | ||||||
|  |     YEARLY = "Y" | ||||||
|  |     TYPE = [MINUTES, HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TaskRegister: | ||||||
|  |     """Registery for periodicall tasks.""" | ||||||
|  |     task_list: list[ScheduledTask] = [] | ||||||
|  |  | ||||||
|  |     def register(self, task, schedule, minutes: int = None): | ||||||
|  |         """Register a task with the que.""" | ||||||
|  |         self.task_list.append(ScheduledTask(task, schedule, minutes)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | tasks = TaskRegister() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def scheduled_task(interval: str, minutes: int = None, tasklist: TaskRegister = None): | ||||||
|  |     """Register the given task as a scheduled task. | ||||||
|  |  | ||||||
|  |     Example: | ||||||
|  |     ```python | ||||||
|  |     @register(ScheduledTask.DAILY) | ||||||
|  |     def my_custom_funciton(): | ||||||
|  |         ... | ||||||
|  |     ``` | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         interval (str): The interval at which the task should be run | ||||||
|  |         minutes (int, optional): The number of minutes between task runs. Defaults to None. | ||||||
|  |         tasklist (TaskRegister, optional): The list the tasks should be registered to. Defaults to None. | ||||||
|  |  | ||||||
|  |     Raises: | ||||||
|  |         ValueError: If decorated object is not callable | ||||||
|  |         ValueError: If interval is not valid | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         _type_: _description_ | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def _task_wrapper(admin_class): | ||||||
|  |         if not isinstance(admin_class, Callable): | ||||||
|  |             raise ValueError('Wrapped object must be a function') | ||||||
|  |  | ||||||
|  |         if interval not in ScheduledTask.TYPE: | ||||||
|  |             raise ValueError(f'Invalid interval. Must be one of {ScheduledTask.TYPE}') | ||||||
|  |  | ||||||
|  |         _tasks = tasklist if tasklist else tasks | ||||||
|  |         _tasks.register(admin_class, interval, minutes=minutes) | ||||||
|  |  | ||||||
|  |         return admin_class | ||||||
|  |     return _task_wrapper | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @scheduled_task(ScheduledTask.MINUTES, 15) | ||||||
| def heartbeat(): | def heartbeat(): | ||||||
|     """Simple task which runs at 5 minute intervals, so we can determine that the background worker is actually running. |     """Simple task which runs at 5 minute intervals, so we can determine that the background worker is actually running. | ||||||
|  |  | ||||||
| @@ -149,6 +224,7 @@ def heartbeat(): | |||||||
|     heartbeats.delete() |     heartbeats.delete() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @scheduled_task(ScheduledTask.DAILY) | ||||||
| def delete_successful_tasks(): | def delete_successful_tasks(): | ||||||
|     """Delete successful task logs which are more than a month old.""" |     """Delete successful task logs which are more than a month old.""" | ||||||
|     try: |     try: | ||||||
| @@ -168,6 +244,7 @@ def delete_successful_tasks(): | |||||||
|         results.delete() |         results.delete() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @scheduled_task(ScheduledTask.DAILY) | ||||||
| def delete_old_error_logs(): | def delete_old_error_logs(): | ||||||
|     """Delete old error logs from the server.""" |     """Delete old error logs from the server.""" | ||||||
|     try: |     try: | ||||||
| @@ -190,6 +267,7 @@ def delete_old_error_logs(): | |||||||
|         return |         return | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @scheduled_task(ScheduledTask.DAILY) | ||||||
| def check_for_updates(): | def check_for_updates(): | ||||||
|     """Check if there is an update for InvenTree.""" |     """Check if there is an update for InvenTree.""" | ||||||
|     try: |     try: | ||||||
| @@ -232,6 +310,7 @@ def check_for_updates(): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @scheduled_task(ScheduledTask.DAILY) | ||||||
| def update_exchange_rates(): | def update_exchange_rates(): | ||||||
|     """Update currency exchange rates.""" |     """Update currency exchange rates.""" | ||||||
|     try: |     try: | ||||||
| @@ -273,6 +352,7 @@ def update_exchange_rates(): | |||||||
|         logger.error(f"Error updating exchange rates: {e}") |         logger.error(f"Error updating exchange rates: {e}") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @scheduled_task(ScheduledTask.DAILY) | ||||||
| def run_backup(): | def run_backup(): | ||||||
|     """Run the backup command.""" |     """Run the backup command.""" | ||||||
|     from common.models import InvenTreeSetting |     from common.models import InvenTreeSetting | ||||||
|   | |||||||
| @@ -144,6 +144,7 @@ def notify_overdue_build_order(bo: build.models.Build): | |||||||
|     trigger_event(event_name, build_order=bo.pk) |     trigger_event(event_name, build_order=bo.pk) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @InvenTree.tasks.scheduled_task(InvenTree.tasks.ScheduledTask.DAILY) | ||||||
| def check_overdue_build_orders(): | def check_overdue_build_orders(): | ||||||
|     """Check if any outstanding BuildOrders have just become overdue |     """Check if any outstanding BuildOrders have just become overdue | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,9 +5,12 @@ from datetime import datetime, timedelta | |||||||
|  |  | ||||||
| from django.core.exceptions import AppRegistryNotReady | from django.core.exceptions import AppRegistryNotReady | ||||||
|  |  | ||||||
|  | from InvenTree.tasks import ScheduledTask, scheduled_task | ||||||
|  |  | ||||||
| logger = logging.getLogger('inventree') | logger = logging.getLogger('inventree') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @scheduled_task(ScheduledTask.DAILY) | ||||||
| def delete_old_notifications(): | def delete_old_notifications(): | ||||||
|     """Remove old notifications from the database. |     """Remove old notifications from the database. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,9 +6,9 @@ from django.utils.translation import gettext_lazy as _ | |||||||
|  |  | ||||||
| import common.notifications | import common.notifications | ||||||
| import InvenTree.helpers | import InvenTree.helpers | ||||||
| import InvenTree.tasks |  | ||||||
| import order.models | import order.models | ||||||
| from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus | from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus | ||||||
|  | from InvenTree.tasks import ScheduledTask, scheduled_task | ||||||
| from plugin.events import trigger_event | from plugin.events import trigger_event | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -55,6 +55,7 @@ def notify_overdue_purchase_order(po: order.models.PurchaseOrder): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @scheduled_task(ScheduledTask.DAILY) | ||||||
| def check_overdue_purchase_orders(): | def check_overdue_purchase_orders(): | ||||||
|     """Check if any outstanding PurchaseOrders have just become overdue: |     """Check if any outstanding PurchaseOrders have just become overdue: | ||||||
|  |  | ||||||
| @@ -117,6 +118,7 @@ def notify_overdue_sales_order(so: order.models.SalesOrder): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @scheduled_task(ScheduledTask.DAILY) | ||||||
| def check_overdue_sales_orders(): | def check_overdue_sales_orders(): | ||||||
|     """Check if any outstanding SalesOrders have just become overdue |     """Check if any outstanding SalesOrders have just become overdue | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user