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

1"""AppConfig for InvenTree app.""" 

2 

3import sys 

4from importlib import import_module 

5from pathlib import Path 

6 

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 

13 

14import structlog 

15from allauth.socialaccount.signals import social_account_updated 

16 

17import InvenTree.conversion 

18import InvenTree.ready 

19import InvenTree.tasks 

20from InvenTree.config import get_setting 

21from InvenTree.ready import ignore_ready_warning 

22 

23logger = structlog.get_logger('inventree') 

24MIGRATIONS_CHECK_DONE = False 

25 

26 

27class InvenTreeConfig(AppConfig): 

28 """AppConfig for inventree app.""" 

29 

30 name = 'InvenTree' 

31 

32 def ready(self): 

33 """Run system wide setup init steps. 

34 

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 

44 

45 if not InvenTree.ready.isPluginRegistryLoaded(): 

46 return 

47 

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 

54 

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 

58 

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() 

62 

63 self.remove_obsolete_tasks() 

64 self.collect_tasks() 

65 self.start_background_tasks() 

66 

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 

73 

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 ) 

80 

81 # Update exchange rates 

82 InvenTree.tasks.offload_task( 

83 InvenTree.tasks.update_exchange_rates, force_async=True 

84 ) 

85 

86 self.update_site_url() 

87 self.load_unit_registry() 

88 

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() 

92 

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 

96 

97 social_account_updated.connect(sso.ensure_sso_groups) 

98 

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 ] 

109 

110 try: 

111 from django_q.models import Schedule 

112 except AppRegistryNotReady: # pragma: no cover 

113 return 

114 

115 # Remove any existing obsolete tasks 

116 try: 

117 obsolete_tasks = Schedule.objects.filter(func__in=obsolete) 

118 

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() 

124 

125 except Exception: 

126 logger.exception('Failed to remove obsolete tasks - database not ready') 

127 

128 @ignore_ready_warning 

129 def start_background_tasks(self): 

130 """Start all background tests for InvenTree.""" 

131 logger.info('Starting background tasks...') 

132 

133 from django_q.models import Schedule 

134 

135 # List of existing scheduled tasks (in the database) 

136 existing_tasks = {} 

137 

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 

140 

141 tasks_to_create = [] 

142 tasks_to_update = [] 

143 

144 # List of collected tasks found with the @scheduled_task decorator 

145 tasks = InvenTree.tasks.tasks.task_list 

146 

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 

150 

151 ref_name = f'{task.func.__module__}.{task.func.__name__}' 

152 

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] 

156 

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) 

164 

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 ) 

175 

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)) 

179 

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)) 

183 

184 self.add_heartbeat() 

185 

186 logger.info('Started %s scheduled background tasks...', len(tasks)) 

187 

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 

199 

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 

206 

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) 

212 

213 @ignore_ready_warning 

214 def update_site_url(self): 

215 """Update the site URL setting. 

216 

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 

221 

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 

224 

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 

229 

230 site = Site.objects.get_current() 

231 

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) 

236 

237 except Exception: 

238 pass 

239 

240 @ignore_ready_warning 

241 def load_unit_registry(self): 

242 """Ensure the unit registry is loaded.""" 

243 InvenTree.conversion.get_unit_registry() 

244 

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 

251 

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 ) 

259 

260 # check if all values are present 

261 set_variables = 0 

262 

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 

266 

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 

271 

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 

275 

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 

283 

284 # good to go -> create user 

285 self._create_admin_user(add_user, add_email, add_password) 

286 

287 # do not try again 

288 settings.USER_ADDED = True 

289 

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) 

303 

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 

310 

311 # get values 

312 add_password_file = get_setting( 

313 'INVENTREE_ADMIN_PASSWORD_FILE', 'admin_password_file', None 

314 ) 

315 

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 

320 

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 

327 

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 ) 

334 

335 # do not try again 

336 settings.USER_ADDED_FILE = True 

337 

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 

343 

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 

348 

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