Coverage for src/backend/InvenTree/InvenTree/apps.py: 65%
185 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""AppConfig for InvenTree app."""
3import sys
4from importlib import import_module
5from pathlib import Path
7from django.apps import AppConfig, apps
8from django.conf import settings
9from django.contrib.auth import get_user_model
10from django.core.exceptions import AppRegistryNotReady
11from django.db import transaction
12from django.db.utils import IntegrityError
14import structlog
15from allauth.socialaccount.signals import social_account_updated
17import InvenTree.conversion
18import InvenTree.ready
19import InvenTree.tasks
20from InvenTree.config import get_setting
21from InvenTree.ready import ignore_ready_warning
23logger = structlog.get_logger('inventree')
24MIGRATIONS_CHECK_DONE = False
27class InvenTreeConfig(AppConfig):
28 """AppConfig for inventree app."""
30 name = 'InvenTree'
32 def ready(self):
33 """Run system wide setup init steps.
35 Like:
36 - Checking if migrations should be run
37 - Cleaning up tasks
38 - Starting regular tasks
39 - Updating exchange rates
40 - Collecting state transition methods
41 - Adding users set in the current environment
42 """
43 # skip loading if plugin registry is not loaded or we run in a background thread
45 if not InvenTree.ready.isPluginRegistryLoaded():
46 return
48 # Skip if not in worker or main thread
49 if ( 49 ↛ 53line 49 didn't jump to line 53 because the condition on line 49 was never true
50 not InvenTree.ready.isInMainThread()
51 and not InvenTree.ready.isInWorkerThread()
52 ):
53 return
55 # Skip if running migrations
56 if InvenTree.ready.isRunningMigrations(): 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true
57 return
59 if InvenTree.ready.canAppAccessDatabase() or settings.TESTING_ENV: 59 ↛ 86line 59 didn't jump to line 86 because the condition on line 59 was always true
60 # Ensure there are no open migrations
61 self.ensure_migrations_done()
63 self.remove_obsolete_tasks()
64 self.collect_tasks()
65 self.start_background_tasks()
67 if ( 67 ↛ 86line 67 didn't jump to line 86 because the condition on line 67 was always true
68 not InvenTree.ready.isInTestMode()
69 and not InvenTree.ready.isInWorkerThread()
70 ): # pragma: no cover
71 # Let the background worker check for migrations
72 # Don't offload task if we are already *in* the background worker, otherwise we might end up in a deadlock situation
74 try:
75 InvenTree.tasks.offload_task(InvenTree.tasks.check_for_migrations)
76 except Exception as exc:
77 logger.exception(
78 'Failed to offload check_for_migrations task: %s', exc
79 )
81 # Update exchange rates
82 InvenTree.tasks.offload_task(
83 InvenTree.tasks.update_exchange_rates, force_async=True
84 )
86 self.update_site_url()
87 self.load_unit_registry()
89 if InvenTree.ready.canAppAccessDatabase() or settings.TESTING_ENV: 89 ↛ 95line 89 didn't jump to line 95 because the condition on line 89 was always true
90 self.add_user_on_startup()
91 self.add_user_from_file()
93 # register event receiver and connect signal for SSO group sync. The connected signal is
94 # used for account updates whereas the receiver is used for the initial account creation.
95 from InvenTree import sso
97 social_account_updated.connect(sso.ensure_sso_groups)
99 @ignore_ready_warning
100 def remove_obsolete_tasks(self):
101 """Delete any obsolete scheduled tasks in the database."""
102 obsolete = [
103 'InvenTree.tasks.delete_expired_sessions',
104 'stock.tasks.delete_old_stock_items',
105 'label.tasks.cleanup_old_label_outputs',
106 'report.tasks.cleanup_old_report_outputs',
107 'data_exporter.tasks.cleanup_old_export_outputs',
108 ]
110 try:
111 from django_q.models import Schedule
112 except AppRegistryNotReady: # pragma: no cover
113 return
115 # Remove any existing obsolete tasks
116 try:
117 obsolete_tasks = Schedule.objects.filter(func__in=obsolete)
119 if obsolete_tasks.exists(): 119 ↛ 120line 119 didn't jump to line 120 because the condition on line 119 was never true
120 logger.info(
121 'Removing %s obsolete background tasks', obsolete_tasks.count()
122 )
123 obsolete_tasks.delete()
125 except Exception:
126 logger.exception('Failed to remove obsolete tasks - database not ready')
128 @ignore_ready_warning
129 def start_background_tasks(self):
130 """Start all background tests for InvenTree."""
131 logger.info('Starting background tasks...')
133 from django_q.models import Schedule
135 # List of existing scheduled tasks (in the database)
136 existing_tasks = {}
138 for existing_task in Schedule.objects.all(): 138 ↛ 139line 138 didn't jump to line 139 because the loop on line 138 never started
139 existing_tasks[existing_task.func] = existing_task
141 tasks_to_create = []
142 tasks_to_update = []
144 # List of collected tasks found with the @scheduled_task decorator
145 tasks = InvenTree.tasks.tasks.task_list
147 for task in tasks:
148 if not task: 148 ↛ 149line 148 didn't jump to line 149 because the condition on line 148 was never true
149 continue # pragma: no cover
151 ref_name = f'{task.func.__module__}.{task.func.__name__}'
153 if ref_name in existing_tasks: 153 ↛ 155line 153 didn't jump to line 155 because the condition on line 153 was never true
154 # This task already exists - update the details if required
155 existing_task = existing_tasks[ref_name]
157 if (
158 existing_task.schedule_type != task.interval
159 or existing_task.minutes != task.minutes
160 ):
161 existing_task.schedule_type = task.interval
162 existing_task.minutes = task.minutes
163 tasks_to_update.append(existing_task)
165 else:
166 # This task does *not* already exist - create it
167 tasks_to_create.append(
168 Schedule(
169 name=ref_name,
170 func=ref_name,
171 schedule_type=task.interval,
172 minutes=task.minutes,
173 )
174 )
176 if len(tasks_to_create) > 0: 176 ↛ 180line 176 didn't jump to line 180 because the condition on line 176 was always true
177 Schedule.objects.bulk_create(tasks_to_create)
178 logger.info('Created %s new scheduled tasks', len(tasks_to_create))
180 if len(tasks_to_update) > 0: 180 ↛ 181line 180 didn't jump to line 181 because the condition on line 180 was never true
181 Schedule.objects.bulk_update(tasks_to_update, ['schedule_type', 'minutes'])
182 logger.info('Updated %s existing scheduled tasks', len(tasks_to_update))
184 self.add_heartbeat()
186 logger.info('Started %s scheduled background tasks...', len(tasks))
188 @ignore_ready_warning
189 def add_heartbeat(self):
190 """Ensure there is at least one background task in the queue."""
191 try:
192 InvenTree.tasks.offload_task(
193 InvenTree.tasks.heartbeat, force_async=True, group='heartbeat'
194 )
195 except AppRegistryNotReady: # pragma: no cover
196 pass
197 except Exception:
198 pass
200 @ignore_ready_warning
201 def collect_tasks(self):
202 """Collect all background tasks."""
203 for app_name, app in apps.app_configs.items():
204 if app_name == 'InvenTree':
205 continue
207 if Path(app.path).joinpath('tasks.py').exists():
208 try:
209 import_module(f'{app.module.__package__}.tasks')
210 except Exception as e: # pragma: no cover
211 logger.exception('Error loading tasks for %s: %s', app_name, e)
213 @ignore_ready_warning
214 def update_site_url(self):
215 """Update the site URL setting.
217 - If multi-site support is enabled, update the site URL for the current site
218 """
219 if not InvenTree.ready.canAppAccessDatabase(): 219 ↛ 220line 219 didn't jump to line 220 because the condition on line 219 was never true
220 return
222 if InvenTree.ready.isImportingData() or InvenTree.ready.isRunningMigrations(): 222 ↛ 223line 222 didn't jump to line 223 because the condition on line 222 was never true
223 return
225 if settings.SITE_URL: 225 ↛ exitline 225 didn't return from function 'update_site_url' because the condition on line 225 was always true
226 # If multi-site support is enabled, update the site URL for the current site
227 try:
228 from django.contrib.sites.models import Site
230 site = Site.objects.get_current()
232 if site and site.domain != settings.SITE_URL:
233 site.domain = settings.SITE_URL
234 site.save()
235 logger.info('Updated current site URL to %s', settings.SITE_URL)
237 except Exception:
238 pass
240 @ignore_ready_warning
241 def load_unit_registry(self):
242 """Ensure the unit registry is loaded."""
243 InvenTree.conversion.get_unit_registry()
245 @ignore_ready_warning
246 def add_user_on_startup(self):
247 """Add a user on startup."""
248 # stop if checks were already created
249 if hasattr(settings, 'USER_ADDED') and settings.USER_ADDED: 249 ↛ 250line 249 didn't jump to line 250 because the condition on line 249 was never true
250 return
252 # get values
253 add_user = get_setting('INVENTREE_ADMIN_USER', 'admin_user')
254 add_email = get_setting('INVENTREE_ADMIN_EMAIL', 'admin_email')
255 add_password = get_setting('INVENTREE_ADMIN_PASSWORD', 'admin_password')
256 add_password_file = get_setting(
257 'INVENTREE_ADMIN_PASSWORD_FILE', 'admin_password_file', None
258 )
260 # check if all values are present
261 set_variables = 0
263 for tested_var in [add_user, add_email, add_password]:
264 if tested_var: 264 ↛ 263line 264 didn't jump to line 263 because the condition on line 264 was always true
265 set_variables += 1
267 # no variable set -> do not try anything
268 if set_variables == 0: 268 ↛ 269line 268 didn't jump to line 269 because the condition on line 268 was never true
269 settings.USER_ADDED = True
270 return
272 # not all needed variables set
273 if set_variables < 3: 273 ↛ 274line 273 didn't jump to line 274 because the condition on line 273 was never true
274 settings.USER_ADDED = True
276 # if a password file is present, do not warn - will be handled later
277 if add_password_file:
278 return
279 logger.warning(
280 'Not all required settings for adding a user on startup are present:\nINVENTREE_ADMIN_USER, INVENTREE_ADMIN_EMAIL, INVENTREE_ADMIN_PASSWORD'
281 )
282 return
284 # good to go -> create user
285 self._create_admin_user(add_user, add_email, add_password)
287 # do not try again
288 settings.USER_ADDED = True
290 def _create_admin_user(self, add_user, add_email, add_password):
291 user = get_user_model()
292 try:
293 with transaction.atomic():
294 if user.objects.filter(username=add_user).exists(): 294 ↛ 295line 294 didn't jump to line 295 because the condition on line 294 was never true
295 logger.info('User %s already exists - skipping creation', add_user)
296 else:
297 new_user = user.objects.create_superuser(
298 add_user, add_email, add_password
299 )
300 logger.info('User %s was created!', new_user)
301 except IntegrityError:
302 logger.warning('The user "%s" could not be created', add_user)
304 @ignore_ready_warning
305 def add_user_from_file(self):
306 """Add the superuser from a file."""
307 # stop if checks were already created
308 if hasattr(settings, 'USER_ADDED_FILE') and settings.USER_ADDED_FILE: 308 ↛ 309line 308 didn't jump to line 309 because the condition on line 308 was never true
309 return
311 # get values
312 add_password_file = get_setting(
313 'INVENTREE_ADMIN_PASSWORD_FILE', 'admin_password_file', None
314 )
316 # no variable set -> do not try anything
317 if not add_password_file: 317 ↛ 322line 317 didn't jump to line 322 because the condition on line 317 was always true
318 settings.USER_ADDED_FILE = True
319 return
321 # check if file exists
322 add_password_file = Path(str(add_password_file))
323 if not add_password_file.exists():
324 logger.warning('The file "%s" does not exist', add_password_file)
325 settings.USER_ADDED_FILE = True
326 return
328 # good to go -> create user
329 self._create_admin_user(
330 get_setting('INVENTREE_ADMIN_USER', 'admin_user', 'admin'),
331 get_setting('INVENTREE_ADMIN_EMAIL', 'admin_email', ''),
332 add_password_file.read_text(encoding='utf-8'),
333 )
335 # do not try again
336 settings.USER_ADDED_FILE = True
338 def ensure_migrations_done(self=None):
339 """Ensures there are no open migrations, stop if inconsistent state."""
340 global MIGRATIONS_CHECK_DONE
341 if MIGRATIONS_CHECK_DONE:
342 return
344 # Exit early if we are not in a state where we can access the database,
345 # otherwise we might end up in a deadlock situation
346 if not InvenTree.ready.canAppAccessDatabase(): 346 ↛ 347line 346 didn't jump to line 347 because the condition on line 346 was never true
347 return
349 if not InvenTree.tasks.check_for_migrations(): 349 ↛ 351line 349 didn't jump to line 351 because the condition on line 349 was never true
350 # Detect if this an empty database - if so, start with a fresh migration
351 if (
352 settings.DOCKER
353 and not InvenTree.ready.isInTestMode()
354 and not InvenTree.ready.isRunningMigrations()
355 and InvenTree.tasks.get_migration_count() == 0
356 ):
357 logger.warning(
358 'INVE-W8: Empty database detected - trying to run migrations'
359 )
360 InvenTree.tasks.check_for_migrations(force_run=True)
361 else:
362 logger.error('INVE-W8: Database Migrations required')
363 sys.exit(1)
364 MIGRATIONS_CHECK_DONE = True