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
« 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
6from collections import Counter
7from typing import TYPE_CHECKING, Any
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
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)
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
86 from weblate.auth.models import AuthenticatedHttpRequest, User
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)
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 )
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))
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))
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 )
166 stats = get_non_glossary_stats(obj.stats)
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 )
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)
219def show_project_language(request: AuthenticatedHttpRequest, obj: ProjectLanguage):
220 language_object = obj.language
221 project_object = obj.project
222 user = request.user
224 last_changes = Change.objects.last_changes(
225 user, project=project_object, language=language_object
226 ).recent()
228 last_announcements = (
229 Change.objects.last_changes(
230 user, project=project_object, language=language_object
231 )
232 .filter_announcements()
233 .recent()
234 )
236 translations = translation_prefetch_tasks(
237 get_paginator(request, obj.translation_set, stats=True)
238 )
239 extra_translations = []
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 ]
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 )
307def show_category_language(request: AuthenticatedHttpRequest, obj):
308 language_object = obj.language
309 category_object = obj.category
310 user = request.user
312 last_changes = (
313 Change.objects.last_changes(user, language=language_object)
314 .for_category(category_object)
315 .recent()
316 )
318 translations = get_paginator(request, obj.translation_set, stats=True)
319 extra_translations = []
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 ]
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 )
377def show_project(request: AuthenticatedHttpRequest, obj):
378 user = request.user
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()
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
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 )
402 language_stats = sort_unicode(
403 language_stats, user.profile.get_translation_orderer(request)
404 )
406 components = prefetch_tasks(all_components)
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 )
461def show_category(request: AuthenticatedHttpRequest, obj):
462 user = request.user
464 all_changes = (
465 Change.objects.for_category(obj).filter_components(request.user).prefetch()
466 )
468 last_changes = all_changes.recent()
469 last_announcements = all_changes.filter_announcements().recent()
471 all_components = obj.get_child_components_access(user)
472 all_components = get_paginator(request, all_components, stats=True)
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 )
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 )
491 components = prefetch_tasks(all_components)
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 )
545def show_component(request: AuthenticatedHttpRequest, obj: Component):
546 user = request.user
548 obj.project.project_languages.preload_workflow_settings()
550 last_changes = obj.change_set.prefetch().recent(skip_preload="component")
552 translations = prefetch_stats(list(obj.translation_set.prefetch_meta()))
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 )
565 translations = sort_unicode(
566 translations, user.profile.get_translation_orderer(request)
567 )
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 )
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
627 # Get form
628 form = get_upload_form(user, obj)
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 )
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)
664 # Figure out missing components
665 available_components = [
666 c for c in project.child_components if not c.is_glossary
667 ]
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 )
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 )
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 )
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)
756def new_component_language(
757 request: AuthenticatedHttpRequest, obj: Component
758) -> HttpResponse:
759 user = request.user
761 form_class = get_new_component_language_form(request, obj)
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)
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 )
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)
805 form_class = get_new_project_language_form(request, obj)
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}
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 )
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]
828 for lang_code, lang in language_map.items():
829 counter = lang_counters[lang_code]
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 )
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 )
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 )
873 return redirect(obj)
874 messages.error(request, gettext("Please fix errors in the form."))
875 else:
876 form = form_class(user, obj)
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 )
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
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
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
946 lang_counts[f"errors_{lang_code}"] += 1
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
969 if user.has_perm("component.edit", component):
970 reset_rate_limit("language", request)
972 return result, lang_counts
975@never_cache
976def healthz(request: AuthenticatedHttpRequest) -> HttpResponse:
977 """Make simple health check endpoint."""
978 return HttpResponse("ok")
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 )
992 return render(
993 request,
994 "component-list.html",
995 {
996 "object": obj,
997 "components": components,
998 "bootstrap_5": True,
999 },
1000 )
1003@never_cache
1004def guide(request: AuthenticatedHttpRequest, path):
1005 obj = parse_path(request, path, (Component,))
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 )
1020class ProjectLanguageRedirectView(RedirectView):
1021 permanent = True
1022 query_string = True
1023 pattern_name = "show"
1025 def get_redirect_url(self, project: str | None, lang: str):
1026 return super().get_redirect_url(path=[project or "-", "-", lang])