Coverage for app/venv/lib/python3.14/site-packages/weblate/trans/views/basic.py: 16%

300 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 

4from __future__ import annotations 

5 

6from collections import Counter 

7from typing import TYPE_CHECKING, Any 

8 

9from django.contrib.auth.decorators import login_required 

10from django.db import transaction 

11from django.db.models import Q 

12from django.http import HttpResponse 

13from django.shortcuts import get_object_or_404, redirect 

14from django.urls import reverse 

15from django.utils.html import format_html 

16from django.utils.http import urlencode 

17from django.utils.translation import gettext, ngettext 

18from django.views.decorators.cache import never_cache 

19from django.views.generic import RedirectView 

20 

21from weblate.auth.models import AuthenticatedHttpRequest 

22from weblate.formats.models import EXPORTERS 

23from weblate.lang.models import Language 

24from weblate.trans.actions import ActionEvents 

25from weblate.trans.exceptions import FileParseError 

26from weblate.trans.forms import ( 

27 AddCategoryForm, 

28 AnnouncementForm, 

29 AutoForm, 

30 BulkEditForm, 

31 CategoryDeleteForm, 

32 CategoryLanguageDeleteForm, 

33 CategoryRenameForm, 

34 ComponentDeleteForm, 

35 ComponentRenameForm, 

36 DownloadForm, 

37 ProjectDeleteForm, 

38 ProjectFilterForm, 

39 ProjectLanguageDeleteForm, 

40 ProjectRenameForm, 

41 ReplaceForm, 

42 ReportsForm, 

43 SearchForm, 

44 TranslationDeleteForm, 

45 get_new_component_language_form, 

46 get_new_project_language_form, 

47 get_new_unit_form, 

48 get_upload_form, 

49) 

50from weblate.trans.models import ( 

51 Category, 

52 Change, 

53 Component, 

54 ComponentList, 

55 Project, 

56 Translation, 

57) 

58from weblate.trans.models.component import ( 

59 prefetch_tasks, 

60 translation_prefetch_tasks, 

61) 

62from weblate.trans.models.project import prefetch_project_flags 

63from weblate.trans.models.translation import GhostTranslation 

64from weblate.trans.util import render, sort_unicode, translation_percent 

65from weblate.utils import messages 

66from weblate.utils.ratelimit import reset_rate_limit, session_ratelimit_post 

67from weblate.utils.stats import ( 

68 CategoryLanguage, 

69 GhostCategoryLanguageStats, 

70 GhostProjectLanguageStats, 

71 ProjectLanguage, 

72 get_non_glossary_stats, 

73 prefetch_stats, 

74) 

75from weblate.utils.views import ( 

76 get_paginator, 

77 optional_form, 

78 parse_path, 

79 show_form_errors, 

80 try_set_language, 

81) 

82 

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

84 from collections.abc import Callable 

85 

86 from weblate.auth.models import AuthenticatedHttpRequest, User 

87 

88 

89@never_cache 

90def list_projects(request: AuthenticatedHttpRequest): 

91 """List all projects.""" 

92 query_string = "" 

93 projects = request.user.allowed_projects 

94 form = ProjectFilterForm(request.GET) 

95 if form.is_valid(): 

96 query = {} 

97 if form.cleaned_data["owned"]: 

98 user = form.cleaned_data["owned"] 

99 query["owned"] = user.username 

100 projects = (user.owned_projects & projects.distinct()).order() 

101 elif form.cleaned_data["watched"]: 

102 user = form.cleaned_data["watched"] 

103 query["watched"] = user.username 

104 projects = (user.watched_projects & projects).order() 

105 query_string = urlencode(query) 

106 else: 

107 show_form_errors(request, form) 

108 

109 return render( 

110 request, 

111 "projects.html", 

112 { 

113 "allow_index": True, 

114 "projects": prefetch_project_flags( 

115 get_paginator(request, projects, stats=True) 

116 ), 

117 "title": gettext("Projects"), 

118 "query_string": query_string, 

119 "bootstrap_5": True, 

120 }, 

121 ) 

122 

123 

124def add_ghost_translations( 

125 obj: Project | Category, 

126 user: User, 

127 translations: list, 

128 generator: Callable, 

129 **kwargs, 

130) -> None: 

131 """Add ghost translations for user languages to the list.""" 

132 language_ids = {translation.language.id for translation in translations} 

133 for language in user.profile.all_languages: 

134 if language.id in language_ids: 

135 continue 

136 translations.append(generator(obj, language, **kwargs)) 

137 

138 

139def show_engage(request: AuthenticatedHttpRequest, path): 

140 # Legacy URL 

141 if len(path) == 2: 

142 return redirect("engage", permanent=True, path=[path[0], "-", path[1]]) 

143 # Get project object, skipping ACL 

144 obj = parse_path(None, path, (ProjectLanguage, Project)) 

145 

146 translate_object = None 

147 if isinstance(obj, ProjectLanguage): 

148 language = obj.language 

149 try_set_language(language.code) 

150 translate_object = obj 

151 project = obj.project 

152 else: 

153 project = obj 

154 language = None 

155 guessed_language = ( 

156 Language.objects.filter(translation__component__project=obj) 

157 .exclude(component__project=obj) 

158 .distinct() 

159 .get_request_language(request) 

160 ) 

161 if guessed_language: 

162 translate_object = ProjectLanguage( 

163 project=project, language=guessed_language 

164 ) 

165 

166 stats = get_non_glossary_stats(obj.stats) 

167 

168 return render( 

169 request, 

170 "engage.html", 

171 { 

172 "allow_index": True, 

173 "object": obj, 

174 "path_object": obj, 

175 "project": project, 

176 "strings_count": stats["source_strings"], 

177 "languages_count": project.stats.languages, 

178 "percent": translation_percent(stats["translated"], stats["all"]), 

179 "language": language, 

180 "translate_object": translate_object, 

181 "project_link": format_html( 

182 '<a href="{}">{}</a>', project.get_absolute_url(), project.name 

183 ), 

184 "title": gettext("Get involved in {0}!").format(project), 

185 }, 

186 ) 

187 

188 

189@never_cache 

190def show(request: AuthenticatedHttpRequest, path): 

191 obj = parse_path( 

192 request, 

193 path, 

194 ( 

195 Translation, 

196 Component, 

197 Project, 

198 ProjectLanguage, 

199 Category, 

200 CategoryLanguage, 

201 ), 

202 ) 

203 if isinstance(obj, Project): 

204 return show_project(request, obj) 

205 if isinstance(obj, Component): 

206 return show_component(request, obj) 

207 if isinstance(obj, ProjectLanguage): 

208 return show_project_language(request, obj) 

209 if isinstance(obj, Category): 

210 return show_category(request, obj) 

211 if isinstance(obj, CategoryLanguage): 

212 return show_category_language(request, obj) 

213 if isinstance(obj, Translation): 

214 return show_translation(request, obj) 

215 msg = f"Not supported show: {obj}" 

216 raise TypeError(msg) 

217 

218 

219def show_project_language(request: AuthenticatedHttpRequest, obj: ProjectLanguage): 

220 language_object = obj.language 

221 project_object = obj.project 

222 user = request.user 

223 

224 last_changes = Change.objects.last_changes( 

225 user, project=project_object, language=language_object 

226 ).recent() 

227 

228 last_announcements = ( 

229 Change.objects.last_changes( 

230 user, project=project_object, language=language_object 

231 ) 

232 .filter_announcements() 

233 .recent() 

234 ) 

235 

236 translations = translation_prefetch_tasks( 

237 get_paginator(request, obj.translation_set, stats=True) 

238 ) 

239 extra_translations = [] 

240 

241 # Add ghost translations 

242 if user.is_authenticated and translations.paginator.num_pages == 1: 

243 existing = {translation.component.id for translation in obj.translation_set} 

244 missing = project_object.get_child_components_filter( 

245 lambda qs: qs.exclude(id__in=existing) 

246 .prefetch() 

247 .prefetch_related("source_language") 

248 ) 

249 for item in missing: 

250 item.project = project_object 

251 extra_translations = [ 

252 GhostTranslation(project_object, language_object, component) 

253 for component in missing 

254 if component.can_add_new_language(user, fast=True) 

255 ] 

256 

257 return render( 

258 request, 

259 "language-project.html", 

260 { 

261 "allow_index": True, 

262 "language": language_object, 

263 "project": project_object, 

264 "object": obj, 

265 "path_object": obj, 

266 "last_changes": last_changes, 

267 "last_announcements": last_announcements, 

268 "translations": translations, 

269 "translation_objects": [*extra_translations, *translations], 

270 "categories": prefetch_stats( 

271 CategoryLanguage(category, obj.language) 

272 for category in obj.project.category_set.filter(category=None).all() 

273 ), 

274 "title": f"{project_object} - {language_object}", 

275 "search_form": SearchForm( 

276 request=request, 

277 language=language_object, 

278 initial=SearchForm.get_initial(request), 

279 obj=obj, 

280 bootstrap_5=True, 

281 ), 

282 "announcement_form": optional_form( 

283 AnnouncementForm, user, "announcement.add", project_object 

284 ), 

285 "language_stats": project_object.stats.get_single_language_stats( 

286 language_object 

287 ), 

288 "delete_form": optional_form( 

289 ProjectLanguageDeleteForm, user, "translation.delete", obj, obj=obj 

290 ), 

291 "replace_form": optional_form(ReplaceForm, user, "unit.edit", obj, obj=obj), 

292 "bulk_state_form": optional_form( 

293 BulkEditForm, 

294 user, 

295 "unit.bulk_edit", 

296 obj, 

297 user=user, 

298 obj=obj, 

299 project=obj.project, 

300 bootstrap_5=True, 

301 ), 

302 "bootstrap_5": True, 

303 }, 

304 ) 

305 

306 

307def show_category_language(request: AuthenticatedHttpRequest, obj): 

308 language_object = obj.language 

309 category_object = obj.category 

310 user = request.user 

311 

312 last_changes = ( 

313 Change.objects.last_changes(user, language=language_object) 

314 .for_category(category_object) 

315 .recent() 

316 ) 

317 

318 translations = get_paginator(request, obj.translation_set, stats=True) 

319 extra_translations = [] 

320 

321 # Add ghost translations 

322 if user.is_authenticated and translations.paginator.num_pages == 1: 

323 existing = {translation.component.id for translation in obj.translation_set} 

324 missing = category_object.component_set.exclude(id__in=existing) 

325 extra_translations = [ 

326 GhostTranslation(obj.project, language_object, component) 

327 for component in missing 

328 if component.can_add_new_language(user, fast=True) 

329 ] 

330 

331 return render( 

332 request, 

333 "category-language.html", 

334 { 

335 "allow_index": True, 

336 "language": language_object, 

337 "category": category_object, 

338 "object": obj, 

339 "path_object": obj, 

340 "last_changes": last_changes, 

341 "translations": translations, 

342 "translation_objects": [*extra_translations, *translations], 

343 "categories": prefetch_stats( 

344 CategoryLanguage(category, obj.language) 

345 for category in obj.category.category_set.all() 

346 ), 

347 "title": f"{category_object} - {language_object}", 

348 "search_form": SearchForm( 

349 request=request, 

350 language=language_object, 

351 initial=SearchForm.get_initial(request), 

352 obj=obj, 

353 bootstrap_5=True, 

354 ), 

355 "language_stats": category_object.stats.get_single_language_stats( 

356 language_object 

357 ), 

358 "delete_form": optional_form( 

359 CategoryLanguageDeleteForm, user, "translation.delete", obj, obj=obj 

360 ), 

361 "replace_form": optional_form(ReplaceForm, user, "unit.edit", obj, obj=obj), 

362 "bulk_state_form": optional_form( 

363 BulkEditForm, 

364 user, 

365 "unit.bulk_edit", 

366 obj, 

367 user=user, 

368 obj=obj, 

369 project=obj.category.project, 

370 bootstrap_5=True, 

371 ), 

372 "bootstrap_5": True, 

373 }, 

374 ) 

375 

376 

377def show_project(request: AuthenticatedHttpRequest, obj): 

378 user = request.user 

379 

380 all_changes = obj.change_set.filter_components(request.user).prefetch() 

381 last_changes = all_changes.recent() 

382 last_announcements = all_changes.filter_announcements().recent() 

383 

384 all_components = obj.get_child_components_access( 

385 user, lambda qs: qs.filter(category=None) 

386 ) 

387 all_components = get_paginator(request, all_components, stats=True) 

388 for component in all_components: 

389 component.is_shared = None if component.project == obj else component.project 

390 

391 language_stats = obj.stats.get_language_stats() 

392 can_add_language_components = obj.components_user_can_add_new_language(user) 

393 user_can_add_translation = can_add_language_components.exists() 

394 if user_can_add_translation: 

395 add_ghost_translations( 

396 obj, 

397 user, 

398 language_stats, 

399 GhostProjectLanguageStats, 

400 ) 

401 

402 language_stats = sort_unicode( 

403 language_stats, user.profile.get_translation_orderer(request) 

404 ) 

405 

406 components = prefetch_tasks(all_components) 

407 

408 return render( 

409 request, 

410 "project.html", 

411 { 

412 "allow_index": True, 

413 "object": obj, 

414 "path_object": obj, 

415 "project": obj, 

416 "last_changes": last_changes, 

417 "last_announcements": last_announcements, 

418 "reports_form": ReportsForm({"project": obj}), 

419 "language_stats": [stat.obj or stat for stat in language_stats], 

420 "search_form": SearchForm( 

421 request=request, 

422 initial=SearchForm.get_initial(request), 

423 obj=obj, 

424 bootstrap_5=True, 

425 ), 

426 "announcement_form": optional_form( 

427 AnnouncementForm, user, "announcement.add", obj 

428 ), 

429 "add_form": AddCategoryForm(request, obj) if obj.can_add_category else None, 

430 "delete_form": optional_form( 

431 ProjectDeleteForm, user, "project.edit", obj, obj=obj 

432 ), 

433 "managed_teams": obj.defined_groups.filter(admins=request.user), 

434 "rename_form": optional_form( 

435 ProjectRenameForm, 

436 user, 

437 "project.edit", 

438 obj, 

439 request=request, 

440 instance=obj, 

441 ), 

442 "replace_form": optional_form(ReplaceForm, user, "unit.edit", obj, obj=obj), 

443 "bulk_state_form": optional_form( 

444 BulkEditForm, 

445 user, 

446 "unit.bulk_edit", 

447 obj, 

448 user=user, 

449 obj=obj, 

450 project=obj, 

451 bootstrap_5=True, 

452 ), 

453 "components": components, 

454 "categories": prefetch_stats(obj.category_set.filter(category=None)), 

455 "user_can_add_translation": user_can_add_translation, 

456 "bootstrap_5": True, 

457 }, 

458 ) 

459 

460 

461def show_category(request: AuthenticatedHttpRequest, obj): 

462 user = request.user 

463 

464 all_changes = ( 

465 Change.objects.for_category(obj).filter_components(request.user).prefetch() 

466 ) 

467 

468 last_changes = all_changes.recent() 

469 last_announcements = all_changes.filter_announcements().recent() 

470 

471 all_components = obj.get_child_components_access(user) 

472 all_components = get_paginator(request, all_components, stats=True) 

473 

474 language_stats = obj.stats.get_language_stats() 

475 can_add_language_components = obj.project.components_user_can_add_new_language(user) 

476 user_can_add_translation = can_add_language_components.exists() 

477 if user_can_add_translation: 

478 add_ghost_translations( 

479 obj, 

480 user, 

481 language_stats, 

482 GhostCategoryLanguageStats, 

483 ) 

484 

485 orderer = user.profile.get_translation_orderer(request) 

486 language_stats = sort_unicode( 

487 language_stats, 

488 lambda x: f"{orderer(x)}-{x.language}", 

489 ) 

490 

491 components = prefetch_tasks(all_components) 

492 

493 return render( 

494 request, 

495 "category.html", 

496 { 

497 "allow_index": True, 

498 "object": obj, 

499 "path_object": obj, 

500 "project": obj, 

501 "add_form": AddCategoryForm(request, obj) if obj.can_add_category else None, 

502 "last_changes": last_changes, 

503 "last_announcements": last_announcements, 

504 "reports_form": ReportsForm({"category": obj}), 

505 "language_stats": [stat.obj or stat for stat in language_stats], 

506 "search_form": SearchForm( 

507 request=request, 

508 initial=SearchForm.get_initial(request), 

509 obj=obj, 

510 bootstrap_5=True, 

511 ), 

512 "announcement_form": optional_form( 

513 AnnouncementForm, user, "announcement.add", obj.project 

514 ), 

515 "delete_form": optional_form( 

516 CategoryDeleteForm, user, "project.edit", obj, obj=obj 

517 ), 

518 "rename_form": optional_form( 

519 CategoryRenameForm, 

520 user, 

521 "project.edit", 

522 obj, 

523 request=request, 

524 instance=obj, 

525 ), 

526 "replace_form": optional_form(ReplaceForm, user, "unit.edit", obj, obj=obj), 

527 "bulk_state_form": optional_form( 

528 BulkEditForm, 

529 user, 

530 "unit.bulk_edit", 

531 obj, 

532 user=user, 

533 obj=obj, 

534 project=obj.project, 

535 bootstrap_5=True, 

536 ), 

537 "components": components, 

538 "categories": prefetch_stats(obj.category_set.all()), 

539 "user_can_add_translation": user_can_add_translation, 

540 "bootstrap_5": True, 

541 }, 

542 ) 

543 

544 

545def show_component(request: AuthenticatedHttpRequest, obj: Component): 

546 user = request.user 

547 

548 obj.project.project_languages.preload_workflow_settings() 

549 

550 last_changes = obj.change_set.prefetch().recent(skip_preload="component") 

551 

552 translations = prefetch_stats(list(obj.translation_set.prefetch_meta())) 

553 

554 can_add_language_components = obj.project.components_user_can_add_new_language(user) 

555 user_can_add_translation = can_add_language_components.exists() 

556 if user_can_add_translation: 

557 add_ghost_translations( 

558 obj.project, 

559 user, 

560 translations, 

561 GhostTranslation, 

562 component=obj, 

563 ) 

564 

565 translations = sort_unicode( 

566 translations, user.profile.get_translation_orderer(request) 

567 ) 

568 

569 return render( 

570 request, 

571 "component.html", 

572 { 

573 "allow_index": True, 

574 "object": obj, 

575 "path_object": obj, 

576 "project": obj.project, 

577 "component": obj, 

578 "translations": translations, 

579 "reports_form": ReportsForm({"component": obj}), 

580 "last_changes": last_changes, 

581 "replace_form": optional_form(ReplaceForm, user, "unit.edit", obj, obj=obj), 

582 "bulk_state_form": optional_form( 

583 BulkEditForm, 

584 user, 

585 "unit.bulk_edit", 

586 obj, 

587 user=user, 

588 obj=obj, 

589 project=obj.project, 

590 bootstrap_5=True, 

591 ), 

592 "announcement_form": optional_form( 

593 AnnouncementForm, user, "announcement.add", obj 

594 ), 

595 "delete_form": optional_form( 

596 ComponentDeleteForm, user, "component.edit", obj, obj=obj 

597 ), 

598 "rename_form": optional_form( 

599 ComponentRenameForm, 

600 user, 

601 "component.edit", 

602 obj, 

603 request=request, 

604 instance=obj, 

605 ), 

606 "search_form": SearchForm( 

607 request=request, 

608 initial=SearchForm.get_initial(request), 

609 obj=obj, 

610 bootstrap_5=True, 

611 ), 

612 "alerts": obj.all_active_alerts 

613 if "alerts" not in request.GET 

614 else obj.alert_set.all(), 

615 "user_can_add_translation": user_can_add_translation, 

616 "bootstrap_5": True, 

617 }, 

618 ) 

619 

620 

621def show_translation(request: AuthenticatedHttpRequest, obj): 

622 component = obj.component 

623 project = component.project 

624 last_changes = obj.change_set.prefetch().recent(skip_preload="translation") 

625 user = request.user 

626 

627 # Get form 

628 form = get_upload_form(user, obj) 

629 

630 search_form = SearchForm( 

631 request=request, 

632 language=obj.language, 

633 initial=SearchForm.get_initial(request), 

634 obj=obj, 

635 bootstrap_5=True, 

636 ) 

637 

638 # Translations to same language from other components in this project 

639 # Show up to 10 of them, needs to be list to append ghost ones later 

640 other_translations = list( 

641 Translation.objects.prefetch() 

642 .filter( 

643 Q(component__project=project) | Q(component__links=project), 

644 language=obj.language, 

645 ) 

646 .exclude(component__is_glossary=True) 

647 .order_by("component__name") 

648 .exclude(pk=obj.pk)[:10] 

649 ) 

650 if len(other_translations) == 10: 

651 # Discard too long list as the selection is purely random and thus most 

652 # likely useless 

653 other_translations = [] 

654 else: 

655 # Prefetch stats and tasks for component listing 

656 other_translations = translation_prefetch_tasks( 

657 prefetch_stats(other_translations) 

658 ) 

659 # Include ghost translations for other components, this 

660 # adds quick way to create translations in other components 

661 existing = {translation.component.id for translation in other_translations} 

662 existing.add(component.id) 

663 

664 # Figure out missing components 

665 available_components = [ 

666 c for c in project.child_components if not c.is_glossary 

667 ] 

668 

669 all_components = {c.id for c in available_components} 

670 missing = all_components - existing 

671 if 0 < len(missing) < 5: 

672 for test_component in available_components: 

673 if test_component.id in existing: 

674 continue 

675 if test_component.can_add_new_language(user, fast=True): 

676 other_translations.append( 

677 GhostTranslation(project, obj.language, test_component) 

678 ) 

679 

680 return render( 

681 request, 

682 "translation.html", 

683 { 

684 "allow_index": True, 

685 "path_object": obj, 

686 "object": obj, 

687 "project": project, 

688 "component": obj.component, 

689 "supports_plural": component.file_format_cls.supports_plural, 

690 "form": form, 

691 "download_form": DownloadForm(obj, auto_id="id_dl_%s"), 

692 "autoform": optional_form( 

693 AutoForm, 

694 user, 

695 "translation.auto", 

696 obj, 

697 obj=component, 

698 user=user, 

699 ), 

700 "search_form": search_form, 

701 "replace_form": optional_form(ReplaceForm, user, "unit.edit", obj, obj=obj), 

702 "bulk_state_form": optional_form( 

703 BulkEditForm, 

704 user, 

705 "unit.bulk_edit", 

706 obj, 

707 user=user, 

708 obj=obj, 

709 project=project, 

710 bootstrap_5=True, 

711 ), 

712 "new_unit_form": get_new_unit_form(obj, user), 

713 "new_unit_plural_form": get_new_unit_form(obj, user, is_source_plural=True), 

714 "announcement_form": optional_form( 

715 AnnouncementForm, user, "announcement.add", component 

716 ), 

717 "delete_form": optional_form( 

718 TranslationDeleteForm, user, "translation.delete", obj, obj=obj 

719 ), 

720 "last_changes": last_changes, 

721 "other_translations": other_translations, 

722 "exporters": EXPORTERS.list_exporters(obj), 

723 "bootstrap_5": True, 

724 }, 

725 ) 

726 

727 

728@never_cache 

729def data_project(request: AuthenticatedHttpRequest, project) -> HttpResponse: 

730 obj = parse_path(request, [project], (Project,)) 

731 return render( 

732 request, 

733 "data.html", 

734 { 

735 "object": obj, 

736 "components": obj.get_child_components_access(request.user), 

737 "project": obj, 

738 }, 

739 ) 

740 

741 

742@never_cache 

743@login_required 

744@transaction.atomic 

745@session_ratelimit_post("language", logout_user=False) 

746def new_language(request: AuthenticatedHttpRequest, path) -> HttpResponse: 

747 obj = parse_path(request, path, (Component, Project)) 

748 if isinstance(obj, Component): 

749 return new_component_language(request, obj) 

750 if isinstance(obj, Project): 

751 return new_project_language(request, obj) 

752 msg = f"Not supported new language: {obj}" 

753 raise TypeError(msg) 

754 

755 

756def new_component_language( 

757 request: AuthenticatedHttpRequest, obj: Component 

758) -> HttpResponse: 

759 user = request.user 

760 

761 form_class = get_new_component_language_form(request, obj) 

762 

763 if request.method == "POST": 

764 form = form_class(user, obj, request.POST) 

765 if form.is_valid(): 

766 languages = Language.objects.filter(code__in=form.cleaned_data["lang"]) 

767 result, _ = add_languages_to_component( 

768 request, 

769 user, 

770 languages, 

771 obj, 

772 show_messages=True, 

773 ) 

774 return redirect(result) 

775 messages.error(request, gettext("Please fix errors in the form.")) 

776 else: 

777 form = form_class(user, obj) 

778 

779 return render( 

780 request, 

781 "new-language.html", 

782 { 

783 "object": obj, 

784 "path_object": obj, 

785 "project": obj.project, 

786 "component": obj, 

787 "form": form, 

788 "can_add": obj.can_add_new_language(user), 

789 }, 

790 ) 

791 

792 

793def new_project_language( 

794 request: AuthenticatedHttpRequest, obj: Project 

795) -> HttpResponse: 

796 user = request.user 

797 eligible_components = obj.components_user_can_add_new_language(user) 

798 if not eligible_components.exists(): 

799 messages.error( 

800 request, 

801 gettext("Language addition is not supported by any of the components."), 

802 ) 

803 return redirect(obj) 

804 

805 form_class = get_new_project_language_form(request, obj) 

806 

807 if request.method == "POST": 

808 form = form_class(user, obj, request.POST) 

809 if form.is_valid(): 

810 languages = Language.objects.filter(code__in=form.cleaned_data["lang"]) 

811 language_map = {lang.code: lang for lang in languages} 

812 lang_counters = {lang_code: Counter() for lang_code in language_map} 

813 

814 for component in eligible_components: 

815 _, component_counts = add_languages_to_component( 

816 request, 

817 user, 

818 languages, 

819 component, 

820 show_messages=False, 

821 ) 

822 

823 for lang_code in language_map: 

824 for action in ["added", "requested", "errors"]: 

825 key = f"{action}_{lang_code}" 

826 lang_counters[lang_code][action] += component_counts[key] 

827 

828 for lang_code, lang in language_map.items(): 

829 counter = lang_counters[lang_code] 

830 

831 if counter["added"] > 0: 

832 messages.success( 

833 request, 

834 ngettext( 

835 "Language %(language)s added to %(count)d component.", 

836 "Language %(language)s added to %(count)d components.", 

837 counter["added"], 

838 ) 

839 % { 

840 "language": lang.name, 

841 "count": counter["added"], 

842 }, 

843 ) 

844 

845 if counter["requested"] > 0: 

846 messages.success( 

847 request, 

848 ngettext( 

849 "Language %(language)s requested for %(count)d component.", 

850 "Language %(language)s requested for %(count)d components.", 

851 counter["requested"], 

852 ) 

853 % { 

854 "language": lang.name, 

855 "count": counter["requested"], 

856 }, 

857 ) 

858 

859 if counter["errors"] > 0: 

860 messages.warning( 

861 request, 

862 ngettext( 

863 "Language %(language)s could not be added to %(count)d component. Please check the component's configuration.", 

864 "Language %(language)s could not be added to %(count)d components. Please check the components' configuration.", 

865 counter["errors"], 

866 ) 

867 % { 

868 "language": lang.name, 

869 "count": counter["errors"], 

870 }, 

871 ) 

872 

873 return redirect(obj) 

874 messages.error(request, gettext("Please fix errors in the form.")) 

875 else: 

876 form = form_class(user, obj) 

877 

878 return render( 

879 request, 

880 "new-project-language.html", 

881 { 

882 "object": obj, 

883 "path_object": obj, 

884 "project": obj, 

885 "form": form, 

886 }, 

887 ) 

888 

889 

890def add_languages_to_component( 

891 request: AuthenticatedHttpRequest, 

892 user: User, 

893 languages: list[Language], 

894 component: Component, 

895 show_messages: bool, 

896) -> tuple[Any, Counter]: 

897 added = False 

898 result = component 

899 kwargs = { 

900 "user": user, 

901 "component": component, 

902 "details": {}, 

903 } 

904 lang_counts = Counter() 

905 with component.repository.lock: 

906 component.commit_pending("add language", None) 

907 for language in languages: 

908 lang_code = language.code 

909 kwargs["details"]["language"] = lang_code 

910 

911 if component.can_add_new_language(user): 

912 translation = component.add_new_language( 

913 language, 

914 request, 

915 create_translations=False, 

916 show_messages=show_messages, 

917 ) 

918 if translation: 

919 added = True 

920 kwargs["translation"] = translation 

921 if len(languages) == 1: 

922 result = translation 

923 component.change_set.create( 

924 action=ActionEvents.ADDED_LANGUAGE, **kwargs 

925 ) 

926 lang_counts[f"added_{lang_code}"] += 1 

927 continue 

928 

929 elif component.new_lang == "contact": 

930 if component.translation_set.filter(language_code=lang_code).exists(): 

931 continue 

932 component.change_set.create( 

933 action=ActionEvents.REQUESTED_LANGUAGE, **kwargs 

934 ) 

935 if show_messages: 

936 messages.success( 

937 request, 

938 gettext( 

939 "A request for a new translation has been " 

940 "sent to the project's maintainers." 

941 ), 

942 ) 

943 lang_counts[f"requested_{lang_code}"] += 1 

944 continue 

945 

946 lang_counts[f"errors_{lang_code}"] += 1 

947 

948 try: 

949 # force_scan needed, see add_new_language 

950 if added and not component.create_translations( 

951 request=request, force_scan=True 

952 ): 

953 if show_messages: 

954 messages.success( 

955 request, 

956 gettext( 

957 "All languages have been added, updates of translations are in progress." 

958 ), 

959 ) 

960 result = "{}?info=1".format( 

961 reverse( 

962 "show_progress", 

963 kwargs={"path": result.get_url_path()}, 

964 ) 

965 ) 

966 except FileParseError: 

967 pass 

968 

969 if user.has_perm("component.edit", component): 

970 reset_rate_limit("language", request) 

971 

972 return result, lang_counts 

973 

974 

975@never_cache 

976def healthz(request: AuthenticatedHttpRequest) -> HttpResponse: 

977 """Make simple health check endpoint.""" 

978 return HttpResponse("ok") 

979 

980 

981@never_cache 

982def show_component_list(request: AuthenticatedHttpRequest, name) -> HttpResponse: 

983 obj = get_object_or_404(ComponentList, slug__iexact=name) 

984 components = prefetch_tasks( 

985 get_paginator( 

986 request, 

987 obj.components.filter_access(request.user).order().prefetch(), 

988 stats=True, 

989 ) 

990 ) 

991 

992 return render( 

993 request, 

994 "component-list.html", 

995 { 

996 "object": obj, 

997 "components": components, 

998 "bootstrap_5": True, 

999 }, 

1000 ) 

1001 

1002 

1003@never_cache 

1004def guide(request: AuthenticatedHttpRequest, path): 

1005 obj = parse_path(request, path, (Component,)) 

1006 

1007 return render( 

1008 request, 

1009 "guide.html", 

1010 { 

1011 "object": obj, 

1012 "path_object": obj, 

1013 "project": obj.project, 

1014 "component": obj, 

1015 "guidelines": obj.guidelines, 

1016 }, 

1017 ) 

1018 

1019 

1020class ProjectLanguageRedirectView(RedirectView): 

1021 permanent = True 

1022 query_string = True 

1023 pattern_name = "show" 

1024 

1025 def get_redirect_url(self, project: str | None, lang: str): 

1026 return super().get_redirect_url(path=[project or "-", "-", lang])