Coverage for app/venv/lib/python3.14/site-packages/weblate/trans/tasks.py: 37%

386 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 07:15 +0000

1# Copyright © Michal Čihař <michal@weblate.org> 

2# 

3# SPDX-License-Identifier: GPL-3.0-or-later 

4 

5from __future__ import annotations 

6 

7import os 

8import time 

9from datetime import datetime, timedelta 

10from glob import glob 

11from operator import itemgetter 

12from pathlib import Path 

13from typing import TYPE_CHECKING, Literal 

14 

15from celery.schedules import crontab 

16from django.conf import settings 

17from django.contrib.staticfiles.storage import staticfiles_storage 

18from django.core.cache import cache 

19from django.db import IntegrityError, transaction 

20from django.db.models import ( 

21 Count, 

22 DateTimeField, 

23 ExpressionWrapper, 

24 F, 

25 Value, 

26) 

27from django.db.models.functions import Now 

28from django.http import Http404 

29from django.utils import timezone 

30from django.utils.timezone import make_aware 

31from django.utils.translation import override 

32 

33from weblate.addons.models import Addon 

34from weblate.auth.models import AuthenticatedHttpRequest, User, get_anonymous 

35from weblate.lang.models import Language 

36from weblate.logger import LOGGER 

37from weblate.trans.actions import ActionEvents 

38from weblate.trans.autotranslate import AutoTranslate 

39from weblate.trans.exceptions import FileParseError 

40from weblate.trans.functions import MySQLTimestampAdd 

41from weblate.trans.models import ( 

42 Category, 

43 Change, 

44 Comment, 

45 Component, 

46 ComponentList, 

47 Project, 

48 Suggestion, 

49 Translation, 

50) 

51from weblate.utils.celery import app 

52from weblate.utils.data import data_dir 

53from weblate.utils.db import using_postgresql 

54from weblate.utils.errors import report_error 

55from weblate.utils.files import remove_tree 

56from weblate.utils.lock import WeblateLockTimeoutError 

57from weblate.utils.stats import prefetch_stats 

58from weblate.utils.views import parse_path 

59from weblate.vcs.base import RepositoryError 

60 

61if TYPE_CHECKING: 61 ↛ 62line 61 didn't jump to line 62 because the condition on line 61 was never true

62 from collections.abc import Callable 

63 

64 from django.db.models import ( 

65 Expression, 

66 ) 

67 

68 

69@app.task( 

70 trail=False, 

71 autoretry_for=(WeblateLockTimeoutError,), 

72 retry_backoff=600, 

73 retry_backoff_max=3600, 

74) 

75def perform_update( 

76 cls: Literal["Project", "Component"], 

77 pk: int, 

78 auto: bool = False, 

79 obj=None, 

80 user_id: int = 0, 

81) -> None: 

82 request: AuthenticatedHttpRequest | None = None 

83 if user_id: 

84 request = AuthenticatedHttpRequest() 

85 request.user = User.objects.get(pk=user_id) 

86 try: 

87 if obj is None: 

88 if cls == "Project": 

89 obj = Project.objects.get(pk=pk) 

90 else: 

91 obj = Component.objects.get(pk=pk) 

92 if settings.AUTO_UPDATE in {"full", True} or not auto: 

93 obj.do_update(request) 

94 else: 

95 obj.update_remote_branch() 

96 except (FileParseError, RepositoryError, FileNotFoundError): 

97 # This is stored as alert, so we can silently ignore here 

98 return 

99 

100 

101@app.task( 

102 trail=False, 

103 autoretry_for=(WeblateLockTimeoutError,), 

104 retry_backoff=600, 

105 retry_backoff_max=3600, 

106) 

107def perform_load( 

108 pk: int, 

109 *, 

110 force: bool = False, 

111 force_scan: bool = False, 

112 langs: list[str] | None = None, 

113 changed_template: bool = False, 

114 from_link: bool = False, 

115 change: int | None = None, 

116 user_id: int | None = None, 

117) -> None: 

118 request: AuthenticatedHttpRequest | None = None 

119 if user_id: 

120 request = AuthenticatedHttpRequest() 

121 request.user = User.objects.get(pk=user_id) 

122 component = Component.objects.get(pk=pk) 

123 component.create_translations_immediate( 

124 force=force, 

125 force_scan=force_scan, 

126 langs=langs, 

127 changed_template=changed_template, 

128 from_link=from_link, 

129 change=change, 

130 request=request, 

131 ) 

132 

133 

134@app.task( 

135 trail=False, 

136 autoretry_for=(WeblateLockTimeoutError,), 

137 retry_backoff=600, 

138 retry_backoff_max=3600, 

139) 

140def perform_commit( 

141 pk, 

142 reason: str, 

143 *, 

144 user_id: int | None = None, 

145 force_scan: bool = False, 

146 previous_head: str | None = None, 

147) -> None: 

148 user = User.objects.get(pk=user_id) if user_id else None 

149 component = Component.objects.get(pk=pk) 

150 with component.repository.lock: 

151 component.commit_pending(reason, user=user) 

152 if force_scan: 

153 component.trigger_post_update( 

154 previous_head=previous_head, 

155 skip_push=False, 

156 ) 

157 component.create_translations(force=True) 

158 

159 

160@app.task( 

161 trail=False, 

162 autoretry_for=(WeblateLockTimeoutError,), 

163 retry_backoff=600, 

164 retry_backoff_max=3600, 

165) 

166def perform_push(pk, *args, **kwargs) -> None: 

167 component = Component.objects.get(pk=pk) 

168 component.do_push(*args, **kwargs) 

169 

170 

171@app.task(trail=False) 

172def commit_pending( 

173 hours: int | None = None, 

174 pks: set[int] | None = None, 

175 logger: Callable[[str], None] | None = None, 

176) -> None: 

177 if pks is None: 177 ↛ 180line 177 didn't jump to line 180 because the condition on line 177 was always true

178 components = Component.objects.all() 

179 else: 

180 components = Component.objects.filter(translation__pk__in=pks) 

181 

182 hours_expr = F("commit_pending_age") if hours is None else Value(hours) 

183 age_cutoff: Expression 

184 if using_postgresql(): 184 ↛ 189line 184 didn't jump to line 189 because the condition on line 184 was always true

185 age_cutoff = ExpressionWrapper( 

186 Now() - hours_expr * timedelta(hours=1), output_field=DateTimeField() 

187 ) 

188 else: 

189 age_cutoff = MySQLTimestampAdd("HOUR", -hours_expr, Now()) 

190 

191 components = components.filter( 

192 translation__unit__pending_changes__timestamp__lt=age_cutoff, 

193 ).distinct() 

194 

195 for component in prefetch_stats(components.prefetch()): 195 ↛ 196line 195 didn't jump to line 196 because the loop on line 195 never started

196 if logger: 

197 logger(f"Committing {component}") 

198 

199 perform_commit.delay(component.pk, "commit_pending") 

200 

201 

202@app.task(trail=False) 

203def cleanup_component(pk) -> None: 

204 """ 

205 Perform cleanup of component models. 

206 

207 - Remove stale source Unit objects. 

208 - Update variants. 

209 """ 

210 try: 

211 component = Component.objects.get(pk=pk) 

212 except Component.DoesNotExist: 

213 return 

214 

215 # Skip monolingual components, these handle cleanups based on the template 

216 if component.template: 216 ↛ 217line 216 didn't jump to line 217 because the condition on line 216 was never true

217 return 

218 

219 # Remove stale variants 

220 with transaction.atomic(): 

221 component.update_variants() 

222 

223 translation = component.source_translation 

224 # Skip translations with a filename (eg. when POT file is present) 

225 if translation.filename: 225 ↛ 229line 225 didn't jump to line 229 because the condition on line 225 was always true

226 return 

227 

228 # Remove all units where there is just one referenced unit (self) 

229 with transaction.atomic(): 

230 deleted, details = ( 

231 translation.unit_set.annotate(Count("unit")) 

232 .filter(unit__count__lte=1) 

233 .delete() 

234 ) 

235 if deleted: 

236 translation.log_info("removed leaf units: %s", details) 

237 

238 

239@app.task(trail=False) 

240def cleanup_suggestions() -> None: 

241 # Process suggestions 

242 anonymous_user = get_anonymous() 

243 suggestions = Suggestion.objects.prefetch_related("unit") 

244 for suggestion in suggestions: 

245 with transaction.atomic(): 

246 # Remove suggestions with same text as real translation 

247 if ( 

248 suggestion.unit.target == suggestion.target 

249 and suggestion.unit.translated 

250 ): 

251 suggestion.delete_log( 

252 anonymous_user, change=ActionEvents.SUGGESTION_CLEANUP 

253 ) 

254 continue 

255 

256 # Remove duplicate suggestions 

257 sugs = Suggestion.objects.filter( 

258 unit=suggestion.unit, target=suggestion.target 

259 ).exclude(id=suggestion.id) 

260 # Do not rely on the SQL as MySQL compares strings case insensitive 

261 for other in sugs: 

262 if other.target == suggestion.target: 

263 suggestion.delete_log( 

264 anonymous_user, change=ActionEvents.SUGGESTION_CLEANUP 

265 ) 

266 break 

267 

268 

269@app.task(trail=False) 

270def update_remotes() -> None: 

271 """Update all remote branches (without attempt to merge).""" 

272 if settings.AUTO_UPDATE not in {"full", "remote", True, False}: 272 ↛ 273line 272 didn't jump to line 273 because the condition on line 272 was never true

273 return 

274 

275 now = timezone.now() 

276 components = ( 

277 Component.objects.with_repo() 

278 .annotate(hourmod=F("id") % 24) 

279 .filter(hourmod=now.hour) 

280 ) 

281 for component in components.prefetch().iterator(chunk_size=100): 281 ↛ 282line 281 didn't jump to line 282 because the loop on line 281 never started

282 perform_update("Component", -1, auto=True, obj=component) 

283 

284 

285@app.task(trail=False) 

286def cleanup_stale_repos(root: Path | None = None) -> bool: 

287 vcs_root = Path(data_dir("vcs")) 

288 if root is None: 

289 root = vcs_root 

290 

291 yesterday = time.time() - 86400 

292 

293 empty_dir = True 

294 for path in root.glob("*"): 

295 if not path.is_dir(): 

296 empty_dir = False 

297 # Possibly a lock file 

298 continue 

299 git_dir = path / ".git" 

300 mercurial_dir = path / ".hg" 

301 if not git_dir.exists() and not mercurial_dir.exists(): 

302 # Category dir 

303 if not cleanup_stale_repos(path): 

304 empty_dir = False 

305 continue 

306 

307 # Skip recently modified paths 

308 if path.stat().st_mtime > yesterday: 

309 empty_dir = False 

310 continue 

311 

312 try: 

313 # Find matching components 

314 component: Component = parse_path( 

315 None, path.relative_to(vcs_root).parts, (Component,) 

316 ) 

317 except Http404: 

318 # Remove stale dir 

319 LOGGER.info("removing stale VCS path (not found): %s", path) 

320 remove_tree(path) 

321 else: 

322 if component.is_repo_link: 

323 LOGGER.info("removing stale VCS path (uses link): %s", root) 

324 remove_tree(path) 

325 else: 

326 empty_dir = False 

327 

328 if empty_dir and root != vcs_root: 

329 try: 

330 # Find matching components 

331 parse_path(None, root.relative_to(vcs_root).parts, (Category, Project)) 

332 except Http404: 

333 LOGGER.info("removing stale VCS path (not found): %s", root) 

334 root.rmdir() 

335 else: 

336 empty_dir = False 

337 return empty_dir 

338 

339 

340@app.task(trail=False) 

341def cleanup_old_suggestions() -> None: 

342 if not settings.SUGGESTION_CLEANUP_DAYS: 

343 return 

344 cutoff = timezone.now() - timedelta(days=settings.SUGGESTION_CLEANUP_DAYS) 

345 Suggestion.objects.filter(timestamp__lt=cutoff).delete() 

346 

347 

348@app.task(trail=False) 

349def cleanup_old_comments() -> None: 

350 if not settings.COMMENT_CLEANUP_DAYS: 

351 return 

352 cutoff = timezone.now() - timedelta(days=settings.COMMENT_CLEANUP_DAYS) 

353 Comment.objects.filter(timestamp__lt=cutoff).delete() 

354 

355 

356@app.task(trail=False) 

357def repository_alerts(threshold=settings.REPOSITORY_ALERT_THRESHOLD) -> None: 

358 non_linked = Component.objects.with_repo() 

359 for component in non_linked.iterator(): 

360 try: 

361 if component.repository.count_missing() > threshold: 

362 component.add_alert("RepositoryOutdated") 

363 else: 

364 component.delete_alert("RepositoryOutdated") 

365 if component.repository.count_outgoing() > threshold: 

366 component.add_alert("RepositoryChanges") 

367 else: 

368 component.delete_alert("RepositoryChanges") 

369 except RepositoryError as error: 

370 report_error("Could not check repository status", project=component.project) 

371 component.add_alert("MergeFailure", error=component.error_text(error)) 

372 

373 

374@app.task(trail=False) 

375def component_alerts(component_ids=None) -> None: 

376 if component_ids: 

377 components = Component.objects.filter(pk__in=component_ids) 

378 else: 

379 now = timezone.now() 

380 components = Component.objects.annotate(hourmod=F("id") % 24).filter( 

381 hourmod=now.hour 

382 ) 

383 for component in components.prefetch().iterator(chunk_size=100): 

384 with transaction.atomic(): 

385 component.update_alerts() 

386 

387 

388@app.task( 

389 trail=False, 

390 autoretry_for=(Component.DoesNotExist, WeblateLockTimeoutError), 

391 retry_backoff=60, 

392) 

393@transaction.atomic 

394def component_after_save( 

395 pk: int, 

396 changed_git: bool, 

397 changed_setup: bool, 

398 changed_template: bool, 

399 changed_variant: bool, 

400 changed_enforced_checks: bool, 

401 skip_push: bool, 

402 create: bool, 

403) -> dict[Literal["component"], int]: 

404 component = Component.objects.get(pk=pk) 

405 component.after_save( 

406 changed_git=changed_git, 

407 changed_setup=changed_setup, 

408 changed_template=changed_template, 

409 changed_variant=changed_variant, 

410 changed_enforced_checks=changed_enforced_checks, 

411 skip_push=skip_push, 

412 create=create, 

413 ) 

414 return {"component": pk} 

415 

416 

417@app.task( 

418 trail=False, 

419 autoretry_for=(Component.DoesNotExist, WeblateLockTimeoutError), 

420 retry_backoff=60, 

421) 

422@transaction.atomic 

423def update_enforced_checks(component: int | Component) -> None: 

424 if isinstance(component, int): 

425 component = Component.objects.get(pk=component) 

426 component.update_enforced_checks() 

427 

428 

429@app.task(trail=False) 

430@transaction.atomic 

431def component_removal(pk: int, uid: int) -> None: 

432 user = User.objects.get(pk=uid) 

433 try: 

434 component = Component.objects.get(pk=pk) 

435 except Component.DoesNotExist: 

436 return 

437 component.acting_user = user 

438 component.project.change_set.create( 

439 action=ActionEvents.REMOVE_COMPONENT, 

440 target=component.slug, 

441 user=user, 

442 author=user, 

443 ) 

444 component.delete() 

445 if component.allow_translation_propagation: 

446 components = component.project.component_set.filter( 

447 allow_translation_propagation=True 

448 ).exclude(pk=component.pk) 

449 for component in components.iterator(): 

450 component.schedule_update_checks() 

451 

452 

453@app.task(trail=False) 

454@transaction.atomic 

455def category_removal(pk: int, uid: int) -> None: 

456 user = User.objects.get(pk=uid) 

457 try: 

458 category = Category.objects.get(pk=pk) 

459 except Category.DoesNotExist: 

460 return 

461 for child in category.category_set.all(): 461 ↛ 462line 461 didn't jump to line 462 because the loop on line 461 never started

462 category_removal(child.pk, uid) 

463 for component_id in category.component_set.values_list("id", flat=True): 463 ↛ 464line 463 didn't jump to line 464 because the loop on line 463 never started

464 component_removal(component_id, uid) 

465 category.project.change_set.create( 

466 action=ActionEvents.REMOVE_CATEGORY, 

467 target=category.slug, 

468 user=user, 

469 author=user, 

470 ) 

471 category.delete() 

472 

473 

474@app.task( 

475 trail=False, 

476 autoretry_for=(IntegrityError,), 

477 retry_backoff=600, 

478 retry_backoff_max=3600, 

479) 

480def actual_project_removal(pk: int, uid: int | None) -> None: 

481 """ 

482 Remove project. 

483 

484 This is separated from project_removal to allow retry on integrity errors. 

485 """ 

486 with transaction.atomic(): 

487 user = get_anonymous() if uid is None else User.objects.get(pk=uid) 

488 try: 

489 project = Project.objects.get(pk=pk) 

490 except Project.DoesNotExist: 

491 return 

492 Change.objects.create( 

493 action=ActionEvents.REMOVE_PROJECT, 

494 target=project.slug, 

495 user=user, 

496 author=user, 

497 ) 

498 project.delete() 

499 transaction.on_commit(project.stats.update_parents) 

500 

501 

502@app.task(trail=False) 

503def project_removal(pk: int, uid: int | None) -> None: 

504 """Backup project and schedule actual removal.""" 

505 create_project_backup(pk) 

506 actual_project_removal.delay(pk, uid) 

507 

508 

509@app.task( 

510 trail=False, 

511 autoretry_for=(WeblateLockTimeoutError,), 

512 retry_backoff=600, 

513 retry_backoff_max=3600, 

514) 

515def auto_translate( 

516 *, 

517 user_id: int | None, 

518 translation_id: int, 

519 mode: str, 

520 q: str, 

521 auto_source: Literal["mt", "others"], 

522 component: int | None, 

523 engines: list[str], 

524 threshold: int, 

525 component_wide: bool = False, 

526 unit_ids: list[int] | None = None, 

527): 

528 translation = Translation.objects.get(pk=translation_id) 

529 user = User.objects.get(pk=user_id) if user_id else None 

530 with override(user.profile.language if user else "en"): 

531 auto = AutoTranslate( 

532 user=user, 

533 translation=translation, 

534 q=q, 

535 mode=mode, 

536 component_wide=component_wide, 

537 unit_ids=unit_ids, 

538 ) 

539 message = auto.perform( 

540 auto_source=auto_source, 

541 engines=engines, 

542 threshold=threshold, 

543 source=component, 

544 ) 

545 return {"translation": translation_id, "message": message} 

546 

547 

548@app.task( 

549 trail=False, 

550 autoretry_for=(WeblateLockTimeoutError,), 

551 retry_backoff=600, 

552 retry_backoff_max=3600, 

553) 

554def auto_translate_component( 

555 component_id: int, 

556 mode: str, 

557 q: str, 

558 auto_source: Literal["mt", "others"], 

559 engines: list[str], 

560 threshold: int, 

561 component: int | None = None, 

562): 

563 component_obj = Component.objects.get(pk=component_id) 

564 

565 for translation in component_obj.translation_set.iterator(): 

566 if translation.is_source: 

567 continue 

568 

569 auto = AutoTranslate( 

570 user=None, 

571 translation=translation, 

572 q=q, 

573 mode=mode, 

574 component_wide=True, 

575 ) 

576 auto.perform( 

577 auto_source=auto_source, 

578 engines=engines, 

579 threshold=threshold, 

580 source=component, 

581 ) 

582 component_obj.update_source_checks() 

583 component_obj.run_batched_checks() 

584 return {"component": component_obj.id} 

585 

586 

587@app.task(trail=False) 

588def create_component(copy_from=None, copy_addons=False, in_task=False, **kwargs): 

589 kwargs["project"] = Project.objects.get(pk=kwargs["project"]) 

590 kwargs["source_language"] = Language.objects.get(pk=kwargs["source_language"]) 

591 component = Component(**kwargs) 

592 # Perform validation to avoid creating duplicate components via background 

593 # tasks in discovery 

594 component.full_clean() 

595 component.save(force_insert=True) 

596 component.change_set.create(action=ActionEvents.CREATE_COMPONENT) 

597 if copy_from: 

598 # Copy non-automatic component lists 

599 for clist in ComponentList.objects.filter( 

600 components__id=copy_from, autocomponentlist__isnull=True 

601 ): 

602 clist.components.add(component) 

603 # Copy add-ons 

604 if copy_addons: 

605 addons = Addon.objects.filter(component__pk=copy_from, repo_scope=False) 

606 for addon in addons: 

607 # Avoid installing duplicate addons 

608 if component.addon_set.filter(name=addon.name).exists(): 

609 continue 

610 if not addon.addon.can_install(component, None): 

611 continue 

612 addon.addon.create( 

613 component=component, configuration=addon.configuration 

614 ) 

615 if in_task: 

616 return {"component": component.id} 

617 return component 

618 

619 

620@app.task(trail=False) 

621@transaction.atomic 

622def update_checks(pk: int, update_token: str, update_state: bool = False) -> None: 

623 try: 

624 component = Component.objects.get(pk=pk) 

625 except Component.DoesNotExist: 

626 return 

627 

628 # Skip when further updates are scheduled 

629 latest_token = cache.get(component.update_checks_key) 

630 if latest_token and update_token != latest_token: 

631 return 

632 

633 component.batch_checks = True 

634 # Source translation as last 

635 translations = ( 

636 *component.translation_set.exclude( 

637 pk=component.source_translation.pk 

638 ).prefetch(), 

639 component.source_translation, 

640 ) 

641 for translation in translations: 

642 units = translation.unit_set.prefetch() 

643 if update_state: 

644 units = units.select_for_update() 

645 for unit in units.prefetch_all_checks(): 

646 # Reuse object to avoid fetching from the database 

647 unit.source_unit.translation = component.source_translation 

648 if update_state: 

649 unit.update_state() 

650 unit.run_checks() 

651 component.run_batched_checks() 

652 component.invalidate_cache() 

653 

654 

655@app.task(trail=False) 

656def daily_update_checks() -> None: 

657 if settings.BACKGROUND_TASKS == "never": 657 ↛ 658line 657 didn't jump to line 658 because the condition on line 657 was never true

658 return 

659 today = timezone.now() 

660 components = Component.objects.annotate(hourmod=F("id") % 24).filter( 

661 hourmod=today.hour 

662 ) 

663 if settings.BACKGROUND_TASKS == "monthly": 663 ↛ 665line 663 didn't jump to line 665 because the condition on line 663 was always true

664 components = components.annotate(idmod=F("id") % 30).filter(idmod=today.day) 

665 elif settings.BACKGROUND_TASKS == "weekly": 

666 components = components.annotate(idmod=F("id") % 7).filter( 

667 idmod=today.weekday() 

668 ) 

669 for component in components.iterator(): 669 ↛ 670line 669 didn't jump to line 670 because the loop on line 669 never started

670 component.schedule_update_checks() 

671 

672 

673@app.task(trail=False) 

674def cleanup_project_backups() -> None: 

675 from weblate.trans.backups import PROJECTBACKUP_PREFIX 

676 

677 # This intentionally does not use Project objects to remove stale backups 

678 # for removed projects as well. 

679 rootdir = data_dir(PROJECTBACKUP_PREFIX) 

680 backup_cutoff = timezone.now() - timedelta(days=settings.PROJECT_BACKUP_KEEP_DAYS) 

681 for projectdir in glob(os.path.join(rootdir, "*")): 

682 if not os.path.isdir(projectdir): 

683 continue 

684 if projectdir.endswith("import"): 

685 # Keep imports for shorter time, but more of them 

686 cutoff = timezone.now() - timedelta(days=1) 

687 max_count = 30 

688 else: 

689 cutoff = backup_cutoff 

690 max_count = settings.PROJECT_BACKUP_KEEP_COUNT 

691 backups = sorted( 

692 ( 

693 ( 

694 path, 

695 make_aware( 

696 datetime.fromtimestamp(int(path.split(".")[0])) # noqa: DTZ006 

697 ), 

698 ) 

699 for path in os.listdir(projectdir) 

700 if path.endswith((".zip", ".zip.part")) 

701 ), 

702 key=itemgetter(1), 

703 reverse=True, 

704 ) 

705 while len(backups) > max_count: 

706 remove = backups.pop() 

707 os.unlink(os.path.join(projectdir, remove[0])) 

708 

709 for backup in backups: 

710 if backup[1] < cutoff: 

711 os.unlink(os.path.join(projectdir, backup[0])) 

712 

713 

714@app.task(trail=False) 

715def create_project_backup(pk) -> None: 

716 from weblate.trans.backups import ProjectBackup 

717 

718 project = Project.objects.get(pk=pk) 

719 ProjectBackup().backup_project(project) 

720 

721 

722@app.task(trail=False) 

723def remove_project_backup_download(name: str) -> None: 

724 if staticfiles_storage.exists(name): 

725 staticfiles_storage.delete(name) 

726 

727 

728@app.task(trail=False) 

729def cleanup_project_backup_download() -> None: 

730 from weblate.trans.backups import PROJECTBACKUP_PREFIX 

731 

732 if not staticfiles_storage.exists(PROJECTBACKUP_PREFIX): 732 ↛ 734line 732 didn't jump to line 734 because the condition on line 732 was always true

733 return 

734 cutoff = timezone.now() - timedelta(hours=2) 

735 for name in staticfiles_storage.listdir(PROJECTBACKUP_PREFIX)[1]: 

736 full_name = os.path.join(PROJECTBACKUP_PREFIX, name) 

737 if staticfiles_storage.get_created_time(full_name) < cutoff: 

738 staticfiles_storage.delete(full_name) 

739 

740 

741@app.on_after_finalize.connect 

742def setup_periodic_tasks(sender, **kwargs) -> None: 

743 sender.add_periodic_task(3600, commit_pending.s(), name="commit-pending") 

744 sender.add_periodic_task(3600, update_remotes.s(), name="update-remotes") 

745 sender.add_periodic_task( 

746 crontab(minute=30), daily_update_checks.s(), name="daily-update-checks" 

747 ) 

748 sender.add_periodic_task( 

749 crontab(hour=3, minute=45), repository_alerts.s(), name="repository-alerts" 

750 ) 

751 sender.add_periodic_task(3600, component_alerts.s(), name="component-alerts") 

752 sender.add_periodic_task( 

753 crontab(hour=0, minute=40), cleanup_suggestions.s(), name="suggestions-cleanup" 

754 ) 

755 sender.add_periodic_task( 

756 crontab(hour=0, minute=40), cleanup_stale_repos.s(), name="cleanup-stale-repos" 

757 ) 

758 sender.add_periodic_task( 

759 crontab(hour=0, minute=45), 

760 cleanup_old_suggestions.s(), 

761 name="cleanup-old-suggestions", 

762 ) 

763 sender.add_periodic_task( 

764 crontab(hour=0, minute=50), 

765 cleanup_old_comments.s(), 

766 name="cleanup-old-comments", 

767 ) 

768 sender.add_periodic_task( 

769 crontab(hour=2, minute=30), 

770 cleanup_project_backups.s(), 

771 name="cleanup-project-backups", 

772 ) 

773 sender.add_periodic_task( 

774 3600, 

775 cleanup_project_backup_download.s(), 

776 name="cleanup-project-backup-download", 

777 )