Coverage for app/venv/lib/python3.14/site-packages/weblate/api/serializers.py: 83%
737 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
5from __future__ import annotations
7from copy import copy
8from typing import TYPE_CHECKING, TypeVar, cast
9from zipfile import BadZipfile
11from django.conf import settings
12from django.db import models
13from django.db.models import Model, TextChoices
14from django.utils.translation import gettext_lazy
15from drf_spectacular.extensions import OpenApiSerializerExtension
16from drf_spectacular.plumbing import build_basic_type, build_object_type
17from drf_spectacular.utils import (
18 OpenApiExample,
19 extend_schema_field,
20 extend_schema_serializer,
21 inline_serializer,
22)
23from drf_standardized_errors.openapi_serializers import ServerErrorEnum
24from rest_framework import serializers
26from weblate.accounts.models import Subscription
27from weblate.addons.models import ADDONS, Addon
28from weblate.auth.models import Group, Permission, Role, User
29from weblate.auth.results import PermissionResult
30from weblate.checks.models import CHECKS
31from weblate.lang.models import Language, Plural
32from weblate.memory.models import Memory
33from weblate.screenshots.models import Screenshot
34from weblate.trans.defines import BRANCH_LENGTH, LANGUAGE_NAME_LENGTH, REPO_LENGTH
35from weblate.trans.models import (
36 AutoComponentList,
37 Category,
38 Change,
39 Comment,
40 Component,
41 ComponentList,
42 Label,
43 Project,
44 Translation,
45 Unit,
46)
47from weblate.trans.models.translation import NewUnitParams
48from weblate.trans.util import check_upload_method_permissions, cleanup_repo_url
49from weblate.utils.site import get_site_url
50from weblate.utils.state import STATE_READONLY, StringState
51from weblate.utils.validators import validate_bitmap
52from weblate.utils.views import (
53 create_component_from_doc,
54 create_component_from_zip,
55 get_form_errors,
56 guess_filemask_from_doc,
57)
59if TYPE_CHECKING: 59 ↛ 60line 59 didn't jump to line 60 because the condition on line 59 was never true
60 from drf_spectacular.openapi import AutoSchema
61 from rest_framework.request import Request
63 from weblate.auth.models import AuthenticatedHttpRequest
65_MT = TypeVar("_MT", bound=Model) # Model Type
68def get_reverse_kwargs(
69 obj, lookup_field: tuple[str, ...], strip_parts: int = 0
70) -> dict[str, str] | None:
71 kwargs = {}
72 was_slug = False
73 for lookup in lookup_field:
74 value = obj
75 for key in lookup.split("__"):
76 # NULL value
77 if value is None:
78 return None
79 previous = value
80 value = getattr(value, key)
81 if key == "slug":
82 if was_slug and previous.category: 82 ↛ 83line 82 didn't jump to line 83 because the condition on line 82 was never true
83 value = "%2F".join((*previous.category.get_url_path()[1:], value))
84 was_slug = True
85 if strip_parts:
86 lookup = "__".join(lookup.split("__")[strip_parts:])
87 kwargs[lookup] = value
88 return kwargs
91class MultiFieldHyperlinkedIdentityField(serializers.HyperlinkedIdentityField):
92 lookup_field: tuple[str, ...] # type: ignore[assignment]
94 def __init__(
95 self, lookup_field: tuple[str, ...], strip_parts: int = 0, **kwargs
96 ) -> None:
97 self.strip_parts = strip_parts
98 super().__init__(**kwargs)
99 self.lookup_field = lookup_field
101 def get_url(self, obj, view_name, request: AuthenticatedHttpRequest, format): # noqa: A002
102 """
103 Given an object, return the URL that hyperlinks to the object.
105 May raise a `NoReverseMatch` if the `view_name` and `lookup_field` attributes
106 are not configured to correctly match the URL conf.
107 """
108 # Unsaved objects will not yet have a valid URL.
109 if not getattr(obj, "pk", None): 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 return None
112 kwargs = get_reverse_kwargs(obj, self.lookup_field, self.strip_parts)
113 if kwargs is None:
114 return None
115 return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
118class AbsoluteURLField(serializers.CharField):
119 def get_attribute(self, instance):
120 value = cast("str", super().get_attribute(instance))
121 if "http:/" not in value and "https:/" not in value:
122 return get_site_url(value)
123 return value
126class RemovableSerializer(serializers.ModelSerializer[_MT]):
127 def __init__(self, *args, **kwargs) -> None:
128 remove_fields = kwargs.pop("remove_fields", None)
129 super().__init__(*args, **kwargs)
131 if remove_fields:
132 # for multiple fields in a list
133 for field_name in remove_fields:
134 self.fields.pop(field_name)
137class LanguagePluralSerializer(serializers.ModelSerializer[Plural]):
138 class Meta:
139 model = Plural
140 fields = (
141 "id",
142 "source",
143 "number",
144 "formula",
145 "type",
146 )
149class LanguageSerializer(serializers.ModelSerializer[Language]):
150 name = serializers.CharField(required=False, max_length=LANGUAGE_NAME_LENGTH)
151 web_url = AbsoluteURLField(source="get_absolute_url", read_only=True)
152 plural = LanguagePluralSerializer(required=False)
153 aliases = serializers.ListField(source="get_aliases_names", read_only=True)
154 statistics_url = serializers.HyperlinkedIdentityField(
155 view_name="api:language-statistics", lookup_field="code"
156 )
158 class Meta:
159 model = Language
160 fields = (
161 "id",
162 "code",
163 "name",
164 "plural",
165 "aliases",
166 "direction",
167 "population",
168 "web_url",
169 "url",
170 "statistics_url",
171 )
172 extra_kwargs = { # noqa: RUF012
173 "url": {"view_name": "api:language-detail", "lookup_field": "code"},
174 "code": {"validators": []},
175 }
177 @property
178 def is_source_language(self):
179 return (
180 isinstance(self.parent, ComponentSerializer)
181 and self.field_name == "source_language"
182 )
184 def validate_code(self, value):
185 check_query = Language.objects.filter(code=value)
186 if not check_query.exists() and self.is_source_language:
187 msg = "Language with this language code was not found."
188 raise serializers.ValidationError(msg)
189 return value
191 def validate_plural(self, value):
192 if not value and not self.is_source_language:
193 msg = "This field is required."
194 raise serializers.ValidationError(msg)
195 return value
197 def validate_name(self, value):
198 if not value and not self.is_source_language: 198 ↛ 199line 198 didn't jump to line 199 because the condition on line 198 was never true
199 msg = "This field is required."
200 raise serializers.ValidationError(msg)
201 return value
203 def create(self, validated_data):
204 plural_validated = validated_data.pop("plural", None)
205 if not plural_validated:
206 msg = "No valid plural data was provided."
207 raise serializers.ValidationError(msg)
209 check_query = Language.objects.filter(code=validated_data.get("code"))
210 if check_query.exists():
211 msg = "Language with this Language code already exists."
212 raise serializers.ValidationError(msg)
213 language = super().create(validated_data)
214 plural = Plural(language=language, **plural_validated)
215 plural.save()
216 return language
218 def get_value(self, dictionary):
219 if self.is_source_language and "source_language" in dictionary:
220 value = dictionary["source_language"]
221 if isinstance(value, str):
222 return {"code": value}
223 return super().get_value(dictionary)
226class FullUserSerializer(serializers.ModelSerializer[User]):
227 groups = serializers.HyperlinkedIdentityField(
228 view_name="api:group-detail",
229 lookup_field="id",
230 many=True,
231 read_only=True,
232 )
233 languages = serializers.HyperlinkedIdentityField(
234 view_name="api:language-detail",
235 lookup_field="code",
236 source="profile.languages",
237 many=True,
238 read_only=True,
239 )
240 notifications = serializers.HyperlinkedIdentityField(
241 view_name="api:user-notifications",
242 lookup_field="username",
243 source="subscriptions",
244 )
245 statistics_url = serializers.HyperlinkedIdentityField(
246 view_name="api:user-statistics", lookup_field="username"
247 )
248 contributions_url = serializers.HyperlinkedIdentityField(
249 view_name="api:user-contributions", lookup_field="username"
250 )
252 class Meta:
253 model = User
254 fields = (
255 "id",
256 "email",
257 "full_name",
258 "username",
259 "groups",
260 "languages",
261 "notifications",
262 "is_superuser",
263 "is_active",
264 "is_bot",
265 "date_joined",
266 "date_expires",
267 "last_login",
268 "url",
269 "statistics_url",
270 "contributions_url",
271 )
272 read_only_fields = (
273 "id",
274 "date_joined",
275 "last_login",
276 )
277 extra_kwargs = { # noqa: RUF012
278 "url": {"view_name": "api:user-detail", "lookup_field": "username"}
279 }
282class BasicUserSerializer(serializers.ModelSerializer[User]):
283 class Meta:
284 model = User
285 fields = (
286 "id",
287 "full_name",
288 "username",
289 )
292@extend_schema_field(str)
293class PermissionSerializer(serializers.RelatedField[Permission, str, str]):
294 class Meta:
295 model = Permission
297 def to_representation(self, value):
298 return value.codename
300 def get_queryset(self):
301 return Permission.objects.all()
303 def to_internal_value(self, data):
304 check_query = Permission.objects.filter(codename=data)
305 if not check_query.exists(): 305 ↛ 308line 305 didn't jump to line 308 because the condition on line 305 was always true
306 msg = "Permission with this codename was not found."
307 raise serializers.ValidationError(msg)
308 return data
311class RoleSerializer(serializers.ModelSerializer[Role]):
312 permissions = PermissionSerializer(many=True)
314 class Meta:
315 model = Role
316 fields = (
317 "id",
318 "name",
319 "permissions",
320 "url",
321 )
322 extra_kwargs = { # noqa: RUF012
323 "url": {"view_name": "api:role-detail", "lookup_field": "id"},
324 }
326 def create(self, validated_data):
327 permissions_validated = validated_data.pop("permissions", [])
328 role = Role.objects.create(**validated_data)
329 role.permissions.add(
330 *Permission.objects.filter(codename__in=permissions_validated)
331 )
332 return role
334 def update(self, instance, validated_data):
335 permissions_validated = validated_data.pop("permissions", [])
336 instance.name = validated_data.get("name", instance.name)
337 instance.save()
338 if self.partial:
339 instance.permissions.add(
340 *Permission.objects.filter(codename__in=permissions_validated)
341 )
342 else:
343 instance.permissions.set(
344 Permission.objects.filter(codename__in=permissions_validated)
345 )
346 return instance
349class CommentSerializer(serializers.Serializer[Comment]):
350 scope = serializers.ChoiceField(
351 choices=["report", "global", "translation"],
352 label=gettext_lazy("Scope"),
353 help_text=gettext_lazy(
354 "Is your comment specific to this translation, or generic for all of them?"
355 ),
356 write_only=True,
357 )
358 comment = serializers.CharField(
359 max_length=1000,
360 label=gettext_lazy("Comment text"),
361 help_text=gettext_lazy("You can use Markdown and mention users by @username."),
362 )
363 timestamp = serializers.DateTimeField(
364 required=False,
365 label=gettext_lazy("Creation timestamp"),
366 help_text=gettext_lazy(
367 "If you’re an admin, you can set the explicit timestamp at which the comment was created."
368 ),
369 )
370 user_email = serializers.EmailField(
371 required=False,
372 label=gettext_lazy("Commenter’s email"),
373 help_text=gettext_lazy(
374 "If you’re an admin, you can attribute this comment to another user by their email."
375 ),
376 write_only=True,
377 )
379 id = serializers.IntegerField(read_only=True)
380 user = serializers.HyperlinkedRelatedField(
381 read_only=True, view_name="api:user-detail", lookup_field="username"
382 )
384 class Meta:
385 model = Comment
386 fields = ("scope", "comment", "timestamp", "user_email", "id", "user")
388 def validate_scope(self, value):
389 unit: Unit | None = self.context.get("unit", None)
390 if unit is None: 390 ↛ 391line 390 didn't jump to line 391 because the condition on line 390 was never true
391 return value
393 # Remove bug-report in case source review is not enabled
394 if value == "report" and not unit.translation.component.project.source_review:
395 msg = f'"{value}" is not a valid choice as source review is disabled.'
396 raise serializers.ValidationError(msg)
398 # Remove translation comment when commenting on source
399 if value == "translation" and unit.translation.is_source:
400 msg = f'"{value}" is not a valid choice for source units.'
401 raise serializers.ValidationError(msg)
403 return value
405 def create(self, validated_data):
406 request = self.context["request"]
407 unit = self.context["unit"]
409 text = validated_data.pop("comment")
410 scope = validated_data.pop("scope")
411 timestamp = validated_data.pop("timestamp", None)
412 user_email = validated_data.pop("user_email", None)
414 user = request.user
415 if user_email:
416 override = User.objects.filter(email=user_email).first()
417 if override: 417 ↛ 418line 417 didn't jump to line 418 because the condition on line 417 was never true
418 user = override
420 return Comment.objects.add(
421 request=request,
422 unit=unit,
423 text=text,
424 scope=scope,
425 user=user,
426 timestamp=timestamp,
427 )
430class GroupSerializer(serializers.ModelSerializer[Group]):
431 roles = serializers.HyperlinkedIdentityField(
432 view_name="api:role-detail",
433 lookup_field="id",
434 many=True,
435 read_only=True,
436 )
437 languages = serializers.HyperlinkedIdentityField(
438 view_name="api:language-detail",
439 lookup_field="code",
440 many=True,
441 read_only=True,
442 )
443 projects = serializers.HyperlinkedIdentityField(
444 view_name="api:project-detail",
445 lookup_field="slug",
446 many=True,
447 read_only=True,
448 )
449 componentlists = serializers.HyperlinkedRelatedField(
450 view_name="api:componentlist-detail",
451 lookup_field="slug",
452 many=True,
453 read_only=True,
454 )
455 components = MultiFieldHyperlinkedIdentityField(
456 view_name="api:component-detail",
457 lookup_field=("project__slug", "slug"),
458 many=True,
459 read_only=True,
460 )
461 defining_project = serializers.HyperlinkedRelatedField(
462 view_name="api:project-detail",
463 lookup_field="slug",
464 queryset=Project.objects.none(),
465 required=False,
466 )
468 class Meta:
469 model = Group
470 fields = (
471 "id",
472 "name",
473 "defining_project",
474 "project_selection",
475 "language_selection",
476 "url",
477 "roles",
478 "languages",
479 "projects",
480 "componentlists",
481 "components",
482 "enforced_2fa",
483 )
484 extra_kwargs = { # noqa: RUF012
485 "url": {"view_name": "api:group-detail", "lookup_field": "id"},
486 }
488 def __init__(self, *args, **kwargs) -> None:
489 super().__init__(*args, **kwargs)
490 user = self.context["request"].user
491 self.fields["defining_project"].queryset = user.managed_projects
494class ProjectSerializer(serializers.ModelSerializer[Project]):
495 web_url = AbsoluteURLField(source="get_absolute_url", read_only=True)
496 components_list_url = serializers.HyperlinkedIdentityField(
497 view_name="api:project-components", lookup_field="slug"
498 )
499 changes_list_url = serializers.HyperlinkedIdentityField(
500 view_name="api:project-changes", lookup_field="slug"
501 )
502 repository_url = serializers.HyperlinkedIdentityField(
503 view_name="api:project-repository", lookup_field="slug"
504 )
505 statistics_url = serializers.HyperlinkedIdentityField(
506 view_name="api:project-statistics", lookup_field="slug"
507 )
508 categories_url = serializers.HyperlinkedIdentityField(
509 view_name="api:project-categories", lookup_field="slug"
510 )
511 languages_url = serializers.HyperlinkedIdentityField(
512 view_name="api:project-languages", lookup_field="slug"
513 )
514 labels_url = serializers.HyperlinkedIdentityField(
515 view_name="api:project-labels", lookup_field="slug"
516 )
517 credits_url = serializers.HyperlinkedIdentityField(
518 view_name="api:project-credits", lookup_field="slug"
519 )
520 lock_url = serializers.HyperlinkedIdentityField(
521 view_name="api:project-lock", lookup_field="slug"
522 )
523 machinery_settings = serializers.HyperlinkedIdentityField(
524 view_name="api:project-machinery-settings", lookup_field="slug"
525 )
526 locked = serializers.BooleanField(read_only=True)
528 class Meta:
529 model = Project
530 fields = (
531 "name",
532 "slug",
533 "id",
534 "web",
535 "web_url",
536 "url",
537 "check_flags",
538 "components_list_url",
539 "repository_url",
540 "statistics_url",
541 "categories_url",
542 "changes_list_url",
543 "languages_url",
544 "labels_url",
545 "credits_url",
546 "lock_url",
547 "translation_review",
548 "source_review",
549 "commit_policy",
550 "set_language_team",
551 "instructions",
552 "enable_hooks",
553 "language_aliases",
554 "secondary_language",
555 "enforced_2fa",
556 "machinery_settings",
557 "locked",
558 )
559 extra_kwargs = { # noqa: RUF012
560 "url": {"view_name": "api:project-detail", "lookup_field": "slug"}
561 }
564class LinkedField(serializers.CharField):
565 def get_attribute(self, instance):
566 if instance.linked_component: 566 ↛ 567line 566 didn't jump to line 567 because the condition on line 566 was never true
567 instance = instance.linked_component
568 return getattr(instance, self.source)
571class RepoField(LinkedField):
572 def get_attribute(self, instance):
573 url = super().get_attribute(instance)
574 if not settings.HIDE_REPO_CREDENTIALS: 574 ↛ 575line 574 didn't jump to line 575 because the condition on line 574 was never true
575 return url
576 return cleanup_repo_url(url)
579class RelatedTaskField(serializers.HyperlinkedRelatedField):
580 def __init__(self, **kwargs) -> None:
581 super().__init__(
582 "api:task-detail",
583 read_only=True,
584 allow_null=True,
585 lookup_url_kwarg="pk",
586 **kwargs,
587 )
589 def get_attribute(self, instance):
590 return instance
592 def get_url(self, obj, view_name, request: Request, format): # noqa: A002
593 if not obj.in_progress():
594 return None
595 return super().get_url(obj, view_name, request, format)
598class ComponentSerializer(RemovableSerializer[Component]):
599 web_url = AbsoluteURLField(source="get_absolute_url", read_only=True)
600 project = ProjectSerializer(read_only=True)
601 repository_url = MultiFieldHyperlinkedIdentityField(
602 view_name="api:component-repository", lookup_field=("project__slug", "slug")
603 )
604 translations_url = MultiFieldHyperlinkedIdentityField(
605 view_name="api:component-translations", lookup_field=("project__slug", "slug")
606 )
607 statistics_url = MultiFieldHyperlinkedIdentityField(
608 view_name="api:component-statistics", lookup_field=("project__slug", "slug")
609 )
610 lock_url = MultiFieldHyperlinkedIdentityField(
611 view_name="api:component-lock", lookup_field=("project__slug", "slug")
612 )
613 links_url = MultiFieldHyperlinkedIdentityField(
614 view_name="api:component-links", lookup_field=("project__slug", "slug")
615 )
616 changes_list_url = MultiFieldHyperlinkedIdentityField(
617 view_name="api:component-changes", lookup_field=("project__slug", "slug")
618 )
619 credits_url = MultiFieldHyperlinkedIdentityField(
620 view_name="api:component-credits", lookup_field=("project__slug", "slug")
621 )
622 license_url = serializers.CharField(read_only=True)
623 source_language = LanguageSerializer(required=False)
625 repo = RepoField(max_length=REPO_LENGTH)
627 push = RepoField(required=False, allow_blank=True, max_length=REPO_LENGTH)
628 branch = LinkedField(required=False, allow_blank=True, max_length=BRANCH_LENGTH)
629 push_branch = LinkedField(
630 required=False, allow_blank=True, max_length=BRANCH_LENGTH
631 )
632 locked = serializers.BooleanField(read_only=True)
634 serializer_url_field = MultiFieldHyperlinkedIdentityField
636 zipfile = serializers.FileField(required=False)
637 docfile = serializers.FileField(required=False)
638 disable_autoshare = serializers.BooleanField(required=False)
640 enforced_checks = serializers.JSONField(required=False)
642 category = serializers.HyperlinkedRelatedField(
643 view_name="api:category-detail",
644 queryset=Category.objects.none(),
645 required=False,
646 allow_null=True,
647 )
648 linked_component = MultiFieldHyperlinkedIdentityField(
649 view_name="api:component-detail",
650 lookup_field=("linked_component__project__slug", "linked_component__slug"),
651 strip_parts=1,
652 read_only=True,
653 )
655 task_url = RelatedTaskField(lookup_field="background_task_id")
657 addons = serializers.HyperlinkedIdentityField(
658 view_name="api:addon-detail",
659 source="addon_set",
660 many=True,
661 read_only=True,
662 )
664 class Meta:
665 model = Component
666 fields = (
667 "name",
668 "slug",
669 "id",
670 "source_language",
671 "project",
672 "vcs",
673 "repo",
674 "git_export",
675 "branch",
676 "push_branch",
677 "filemask",
678 "screenshot_filemask",
679 "template",
680 "edit_template",
681 "intermediate",
682 "new_base",
683 "file_format",
684 "file_format_params",
685 "license",
686 "license_url",
687 "agreement",
688 "web_url",
689 "url",
690 "repository_url",
691 "translations_url",
692 "statistics_url",
693 "lock_url",
694 "links_url",
695 "changes_list_url",
696 "task_url",
697 "credits_url",
698 "new_lang",
699 "language_code_style",
700 "push",
701 "check_flags",
702 "priority",
703 "enforced_checks",
704 "restricted",
705 "repoweb",
706 "report_source_bugs",
707 "merge_style",
708 "commit_message",
709 "add_message",
710 "delete_message",
711 "merge_message",
712 "addon_message",
713 "pull_message",
714 "allow_translation_propagation",
715 "manage_units",
716 "enable_suggestions",
717 "suggestion_voting",
718 "suggestion_autoaccept",
719 "push_on_commit",
720 "commit_pending_age",
721 "auto_lock_error",
722 "language_regex",
723 "key_filter",
724 "secondary_language",
725 "variant_regex",
726 "zipfile",
727 "docfile",
728 "addons",
729 "is_glossary",
730 "glossary_color",
731 "disable_autoshare",
732 "category",
733 "linked_component",
734 "locked",
735 )
736 extra_kwargs = { # noqa: RUF012
737 "url": {
738 "view_name": "api:component-detail",
739 "lookup_field": ("project__slug", "slug"),
740 }
741 }
743 def __init__(self, *args, **kwargs) -> None:
744 super().__init__(*args, **kwargs)
746 project = None
747 if isinstance(self.instance, Component):
748 project = self.instance.project
749 elif "context" in kwargs and "project" in kwargs["context"]:
750 project = kwargs["context"]["project"]
752 if project is not None:
753 self.fields["category"].queryset = project.category_set.all()
755 def validate_enforced_checks(self, value):
756 if not isinstance(value, list):
757 msg = "Enforced checks has to be a list."
758 raise serializers.ValidationError(msg)
759 for item in value: 759 ↛ 763line 759 didn't jump to line 763 because the loop on line 759 didn't complete
760 if item not in CHECKS: 760 ↛ 759line 760 didn't jump to line 759 because the condition on line 760 was always true
761 msg = f"Unsupported enforced check: {item}"
762 raise serializers.ValidationError(msg)
763 return value
765 def to_representation(self, instance):
766 """Remove VCS properties if user has no permission for that."""
767 result = super().to_representation(instance)
768 user = self.context["request"].user
769 if not user.has_perm("vcs.view", instance): 769 ↛ 770line 769 didn't jump to line 770 because the condition on line 769 was never true
770 result["vcs"] = None
771 result["repo"] = None
772 result["branch"] = None
773 result["filemask"] = None
774 result["screenshot_filemask"] = None
775 result["push"] = None
776 return result
778 def to_internal_value(self, data):
779 # Preprocess to inject params based on content
780 data = data.copy()
782 # Provide a reasonable default
783 if "manage_units" not in data and data.get("template"):
784 data["manage_units"] = "1"
786 # File uploads indicate usage of a local repo
787 if "docfile" in data or "zipfile" in data:
788 data["repo"] = "local:"
789 data["vcs"] = "local"
790 data["branch"] = "main"
792 # Provide a filemask so that it is not listed as an
793 # error. The validation of docfile will fail later
794 if "docfile" in data and "filemask" not in data:
795 guess_filemask_from_doc(data)
797 # DRF processing
798 result = super().to_internal_value(data)
800 # Handle source language attribute
801 if "source_language" in result:
802 language = result["source_language"]
803 result["source_language"] = Language.objects.get(
804 code=language if isinstance(language, str) else language["code"]
805 )
807 # Add missing project context
808 if "project" in self._context:
809 result["project"] = self._context["project"]
810 elif self.instance: 810 ↛ 814line 810 didn't jump to line 814 because the condition on line 810 was always true
811 result["project"] = self.instance.project
813 # Workaround for https://github.com/encode/django-rest-framework/issues/7489
814 if "category" not in result: 814 ↛ 817line 814 didn't jump to line 817 because the condition on line 814 was always true
815 result["category"] = None
817 return result
819 def validate(self, attrs):
820 # Handle non-component args
821 disable_autoshare = attrs.pop("disable_autoshare", False)
822 docfile = attrs.pop("docfile", None)
823 zipfile = attrs.pop("zipfile", None)
825 # Restrict create fields on patching
826 if self.instance and (docfile is not None or zipfile is not None): 826 ↛ 827line 826 didn't jump to line 827 because the condition on line 826 was never true
827 field = "docfile" if docfile is not None else "zipfile"
828 raise serializers.ValidationError(
829 {field: "This field is for creation only, use /file/ instead."}
830 )
832 # Build new or patched Component instance with changed attributes
833 if self.instance:
834 instance = copy(self.instance)
835 for key, value in attrs.items():
836 setattr(instance, key, value)
837 else:
838 instance = Component(**attrs)
840 if docfile is not None or zipfile is not None:
841 # Validate name/slug uniqueness, this has to be done prior docfile/zipfile
842 # extracting
843 instance.clean_unique_together()
845 # Handle uploaded files
846 if docfile is not None: 846 ↛ 847line 846 didn't jump to line 847 because the condition on line 846 was never true
847 fake = create_component_from_doc(attrs, docfile)
848 instance.template = attrs["template"] = fake.template
849 instance.new_base = attrs["new_base"] = fake.template
850 instance.filemask = attrs["filemask"] = fake.filemask
851 if zipfile is not None: 851 ↛ 860line 851 didn't jump to line 860 because the condition on line 851 was always true
852 try:
853 create_component_from_zip(attrs, zipfile)
854 except BadZipfile as error:
855 raise serializers.ValidationError(
856 {"zipfile": "Could not parse uploaded ZIP file."}
857 ) from error
859 # Call model validation here, DRF does not do that
860 instance.clean()
862 if not self.instance and not disable_autoshare:
863 repo = instance.suggest_repo_link()
864 if repo: 864 ↛ 865line 864 didn't jump to line 865 because the condition on line 864 was never true
865 attrs["repo"] = instance.repo = repo
866 attrs["branch"] = instance.branch = ""
867 return attrs
870class NotificationSerializer(serializers.ModelSerializer[Subscription]):
871 project = ProjectSerializer(read_only=True)
872 component = ComponentSerializer(read_only=True)
874 class Meta:
875 model = Subscription
876 fields = (
877 "notification",
878 "id",
879 "scope",
880 "frequency",
881 "project",
882 "component",
883 )
886class TranslationSerializer(RemovableSerializer[Translation]):
887 web_url = AbsoluteURLField(source="get_absolute_url", read_only=True)
888 share_url = AbsoluteURLField(source="get_share_url", read_only=True)
889 translate_url = AbsoluteURLField(source="get_translate_url", read_only=True)
890 component = ComponentSerializer(read_only=True)
891 language = LanguageSerializer(read_only=True)
892 is_template = serializers.BooleanField(read_only=True)
893 is_source = serializers.BooleanField(read_only=True)
894 total = serializers.IntegerField(source="stats.all", read_only=True)
895 total_words = serializers.IntegerField(source="stats.all_words", read_only=True)
896 translated = serializers.IntegerField(source="stats.translated", read_only=True)
897 translated_words = serializers.IntegerField(
898 source="stats.translated_words", read_only=True
899 )
900 translated_percent = serializers.FloatField(
901 source="stats.translated_percent", read_only=True
902 )
903 fuzzy = serializers.IntegerField(source="stats.fuzzy", read_only=True)
904 fuzzy_words = serializers.IntegerField(source="stats.fuzzy_words", read_only=True)
905 fuzzy_percent = serializers.FloatField(source="stats.fuzzy_percent", read_only=True)
906 failing_checks = serializers.IntegerField(source="stats.allchecks", read_only=True)
907 failing_checks_words = serializers.IntegerField(
908 source="stats.allchecks_words", read_only=True
909 )
910 failing_checks_percent = serializers.FloatField(
911 source="stats.allchecks_percent", read_only=True
912 )
913 have_suggestion = serializers.IntegerField(
914 source="stats.suggestions", read_only=True
915 )
916 have_comment = serializers.IntegerField(source="stats.comments", read_only=True)
917 last_change = serializers.DateTimeField(source="stats.last_changed", read_only=True)
918 last_author = serializers.CharField(source="get_last_author", read_only=True)
919 repository_url = MultiFieldHyperlinkedIdentityField(
920 view_name="api:translation-repository",
921 lookup_field=("component__project__slug", "component__slug", "language__code"),
922 )
923 statistics_url = MultiFieldHyperlinkedIdentityField(
924 view_name="api:translation-statistics",
925 lookup_field=("component__project__slug", "component__slug", "language__code"),
926 )
927 file_url = MultiFieldHyperlinkedIdentityField(
928 view_name="api:translation-file",
929 lookup_field=("component__project__slug", "component__slug", "language__code"),
930 )
931 changes_list_url = MultiFieldHyperlinkedIdentityField(
932 view_name="api:translation-changes",
933 lookup_field=("component__project__slug", "component__slug", "language__code"),
934 )
935 units_list_url = MultiFieldHyperlinkedIdentityField(
936 view_name="api:translation-units",
937 lookup_field=("component__project__slug", "component__slug", "language__code"),
938 )
940 serializer_url_field = MultiFieldHyperlinkedIdentityField
942 class Meta:
943 model = Translation
944 fields = (
945 "language",
946 "component",
947 "language_code",
948 "id",
949 "filename",
950 "revision",
951 "web_url",
952 "share_url",
953 "translate_url",
954 "url",
955 "is_template",
956 "is_source",
957 "total",
958 "total_words",
959 "translated",
960 "translated_words",
961 "translated_percent",
962 "fuzzy",
963 "fuzzy_words",
964 "fuzzy_percent",
965 "failing_checks",
966 "failing_checks_words",
967 "failing_checks_percent",
968 "have_suggestion",
969 "have_comment",
970 "last_change",
971 "last_author",
972 "repository_url",
973 "file_url",
974 "statistics_url",
975 "changes_list_url",
976 "units_list_url",
977 )
978 extra_kwargs = { # noqa: RUF012
979 "url": {
980 "view_name": "api:translation-detail",
981 "lookup_field": (
982 "component__project__slug",
983 "component__slug",
984 "language__code",
985 ),
986 }
987 }
990class ReadOnlySerializer(serializers.Serializer):
991 def update(self, instance, validated_data) -> None:
992 return None
994 def create(self, validated_data) -> None:
995 return None
998class LockSerializer(serializers.ModelSerializer[Component]):
999 class Meta:
1000 model = Component
1001 fields = ("locked",)
1004class ProjectLockSerializer(serializers.ModelSerializer[Project]):
1005 class Meta:
1006 model = Project
1007 fields = ("locked",)
1010class LockRequestSerializer(ReadOnlySerializer):
1011 lock = serializers.BooleanField()
1014class UploadRequestSerializer(ReadOnlySerializer):
1015 file = serializers.FileField()
1016 author_email = serializers.EmailField(required=False)
1017 author_name = serializers.CharField(max_length=200, required=False)
1018 method = serializers.ChoiceField(
1019 choices=(
1020 "translate",
1021 "approve",
1022 "suggest",
1023 "fuzzy",
1024 "replace",
1025 "source",
1026 "add",
1027 ),
1028 required=False,
1029 default="translate",
1030 )
1031 fuzzy = serializers.ChoiceField(
1032 choices=("", "process", "approve"), required=False, default=""
1033 )
1034 conflicts = serializers.ChoiceField(
1035 choices=("", "ignore", "replace-translated", "replace-approved"),
1036 required=False,
1037 default="",
1038 )
1040 def validate_conflicts(self, value):
1041 # These are handled same
1042 if value == "ignore":
1043 return ""
1044 return value
1046 def check_perms(self, user: User, obj) -> None:
1047 data = self.validated_data
1048 if data["conflicts"] and not user.has_perm("upload.overwrite", obj):
1049 raise serializers.ValidationError(
1050 {"conflicts": "You can not overwrite existing translations."}
1051 )
1052 if data["conflicts"] == "replace-approved" and not (
1053 denied := user.has_perm("unit.review", obj)
1054 ):
1055 raise serializers.ValidationError({"conflicts": denied.reason})
1057 if not (denied := check_upload_method_permissions(user, obj, data["method"])):
1058 hint = "Check your permissions or use different translation object."
1059 if isinstance(denied, PermissionResult):
1060 hint = denied.reason
1061 raise serializers.ValidationError(
1062 {"method": f"This method is not available here. {hint}"}
1063 )
1066class RepoOperations(TextChoices):
1067 COMMIT = "commit", gettext_lazy("Commit")
1068 PULL = "pull", gettext_lazy("Update")
1069 PULL_REBASE = "pull-rebase", gettext_lazy("Update with rebase")
1070 PULL_MERGE = "pull-merge", gettext_lazy("Update with merge")
1071 PULL_MERGE_NOFF = (
1072 "pull-merge-noff",
1073 gettext_lazy("Update with merge without fast-forward"),
1074 )
1075 PUSH = "push", gettext_lazy("Push")
1076 RESET = "reset", gettext_lazy("Reset all changes in the Weblate repository")
1077 RESET_KEEP = (
1078 "reset-keep",
1079 gettext_lazy("Reset the Weblate repository and reapply translations"),
1080 )
1081 CLEANUP = (
1082 "cleanup",
1083 gettext_lazy("Cleanup all untracked files in the Weblate repository"),
1084 )
1085 FILE_SYNC = (
1086 "file-sync",
1087 gettext_lazy("Force writing all translations to the Weblate repository"),
1088 )
1089 FILE_SCAN = (
1090 "file-scan",
1091 gettext_lazy("Rescan all translation files in the Weblate repository"),
1092 )
1095class RepoRequestSerializer(ReadOnlySerializer):
1096 operation = serializers.ChoiceField(
1097 choices=RepoOperations.choices,
1098 )
1101class StatisticsSerializer(ReadOnlySerializer):
1102 def to_representation(self, instance):
1103 stats = instance.stats
1104 result = {
1105 "total": stats.all,
1106 "total_words": stats.all_words,
1107 "total_chars": stats.all_chars,
1108 "last_change": stats.last_changed,
1109 "recent_changes": stats.recent_changes,
1110 "translated": stats.translated,
1111 "translated_words": stats.translated_words,
1112 "translated_percent": stats.translated_percent,
1113 "translated_words_percent": stats.translated_words_percent,
1114 "translated_chars": stats.translated_chars,
1115 "translated_chars_percent": stats.translated_chars_percent,
1116 "fuzzy": stats.fuzzy,
1117 "fuzzy_percent": stats.fuzzy_percent,
1118 "fuzzy_words": stats.fuzzy_words,
1119 "fuzzy_words_percent": stats.fuzzy_words_percent,
1120 "fuzzy_chars": stats.fuzzy_chars,
1121 "fuzzy_chars_percent": stats.fuzzy_chars_percent,
1122 "failing": stats.allchecks,
1123 "failing_percent": stats.allchecks_percent,
1124 "approved": stats.approved,
1125 "approved_percent": stats.approved_percent,
1126 "approved_words": stats.approved_words,
1127 "approved_words_percent": stats.approved_words_percent,
1128 "approved_chars": stats.approved_chars,
1129 "approved_chars_percent": stats.approved_chars_percent,
1130 "readonly": stats.readonly,
1131 "readonly_percent": stats.readonly_percent,
1132 "readonly_words": stats.readonly_words,
1133 "readonly_words_percent": stats.readonly_words_percent,
1134 "readonly_chars": stats.readonly_chars,
1135 "readonly_chars_percent": stats.readonly_chars_percent,
1136 "suggestions": stats.suggestions,
1137 "comments": stats.comments,
1138 }
1139 if hasattr(instance, "language"):
1140 result["code"] = instance.language.code
1141 result["name"] = instance.language.name
1142 elif hasattr(instance, "name"): 1142 ↛ 1144line 1142 didn't jump to line 1144 because the condition on line 1142 was always true
1143 result["name"] = instance.name
1144 if hasattr(instance, "get_absolute_url"): 1144 ↛ 1146line 1144 didn't jump to line 1146 because the condition on line 1144 was always true
1145 result["url"] = get_site_url(instance.get_absolute_url())
1146 if hasattr(instance, "get_translate_url"):
1147 result["translate_url"] = get_site_url(instance.get_translate_url())
1148 return result
1151class UserStatisticsSerializer(ReadOnlySerializer):
1152 def to_representation(self, instance):
1153 profile = instance.profile
1154 return {
1155 "translated": profile.translated,
1156 "suggested": profile.suggested,
1157 "uploaded": profile.uploaded,
1158 "commented": profile.commented,
1159 "languages": profile.languages.count(),
1160 }
1163class PluralField(serializers.ListField):
1164 def __init__(self, child_allow_blank=False, **kwargs) -> None:
1165 kwargs["child"] = serializers.CharField(
1166 trim_whitespace=False, allow_blank=child_allow_blank
1167 )
1168 super().__init__(**kwargs)
1170 def get_attribute(self, instance):
1171 return getattr(instance, f"get_{self.field_name}_plurals")()
1174class MemorySerializer(serializers.ModelSerializer[Memory]):
1175 class Meta:
1176 model = Memory
1177 fields = (
1178 "id",
1179 "source",
1180 "target",
1181 "source_language",
1182 "target_language",
1183 "origin",
1184 "project",
1185 "from_file",
1186 "shared",
1187 )
1190class LabelSerializer(serializers.ModelSerializer[Label]):
1191 class Meta:
1192 model = Label
1193 fields = ("id", "name", "description", "color")
1194 read_only_fields = ("project",)
1197class UnitLabelsSerializer(serializers.RelatedField, LabelSerializer):
1198 def get_queryset(self):
1199 """
1200 List of available labels for an unit.
1202 The queryset argument is only ever required for writable relationship field,
1203 in which case it is used for performing the model instance lookup, that maps
1204 from the primitive user input, into a model instance.
1205 """
1206 unit = self.parent.parent.instance
1207 if unit is None: 1207 ↛ 1210line 1207 didn't jump to line 1210 because the condition on line 1207 was never true
1208 # HTTP 404 Not Found on HTML page still shows the form
1209 # but it has no unit attached
1210 return Label.objects.none()
1211 project = unit.translation.component.project
1212 return project.label_set.all()
1214 def to_internal_value(self, data):
1215 try:
1216 pk = int(data)
1217 except ValueError as err:
1218 msg = "Invalid label ID."
1219 raise serializers.ValidationError(msg) from err
1220 try:
1221 label = self.get_queryset().get(id=pk)
1222 except Label.DoesNotExist as err:
1223 msg = "Label with this ID was not found in this project."
1224 raise serializers.ValidationError(msg) from err
1225 return label
1228class UnitFlatLabelsSerializer(UnitLabelsSerializer):
1229 def to_representation(self, value):
1230 return value.id
1233class UnitSerializer(serializers.ModelSerializer[Unit]):
1234 web_url = AbsoluteURLField(source="get_absolute_url", read_only=True)
1235 translation = MultiFieldHyperlinkedIdentityField(
1236 view_name="api:translation-detail",
1237 lookup_field=(
1238 "translation__component__project__slug",
1239 "translation__component__slug",
1240 "translation__language__code",
1241 ),
1242 strip_parts=1,
1243 )
1244 language_code = serializers.CharField(
1245 source="translation.language.code", read_only=True
1246 )
1247 source_unit = serializers.HyperlinkedRelatedField(
1248 read_only=True, view_name="api:unit-detail"
1249 )
1250 source = PluralField()
1251 target = PluralField()
1252 timestamp = serializers.DateTimeField(read_only=True)
1253 last_updated = serializers.DateTimeField(read_only=True)
1254 pending = serializers.BooleanField(source="has_pending_changes", read_only=True)
1255 labels = UnitLabelsSerializer(many=True)
1257 class Meta:
1258 model = Unit
1259 fields = (
1260 "translation",
1261 "language_code",
1262 "source",
1263 "previous_source",
1264 "target",
1265 "id_hash",
1266 "content_hash",
1267 "location",
1268 "context",
1269 "note",
1270 "flags",
1271 "labels",
1272 "state",
1273 "fuzzy",
1274 "translated",
1275 "approved",
1276 "position",
1277 "has_suggestion",
1278 "has_comment",
1279 "has_failing_check",
1280 "num_words",
1281 "source_unit",
1282 "priority",
1283 "id",
1284 "web_url",
1285 "url",
1286 "explanation",
1287 "extra_flags",
1288 "pending",
1289 "timestamp",
1290 "last_updated",
1291 )
1292 extra_kwargs = { # noqa: RUF012
1293 "url": {"view_name": "api:unit-detail"},
1294 }
1297class UnitWriteSerializer(serializers.ModelSerializer[Unit]):
1298 """Serializer for updating source unit."""
1300 target = PluralField()
1301 labels = UnitFlatLabelsSerializer(many=True)
1303 class Meta:
1304 model = Unit
1305 fields = (
1306 "target",
1307 "state",
1308 "explanation",
1309 "extra_flags",
1310 "labels",
1311 )
1313 def to_internal_value(self, data):
1314 # Allow blank target for untranslated strings
1315 if isinstance(data, dict) and data.get("state") in {0, "0"}:
1316 self.fields["target"].child.allow_blank = True
1317 return super().to_internal_value(data)
1320class NewUnitSerializer(serializers.Serializer):
1321 state = serializers.ChoiceField(
1322 choices=[
1323 choice for choice in StringState.choices if choice[0] != STATE_READONLY
1324 ],
1325 required=False,
1326 )
1328 def as_kwargs(self, data: dict | None = None) -> NewUnitParams:
1329 raise NotImplementedError
1331 def validate(self, attrs):
1332 try:
1333 data = self.as_kwargs(attrs)
1334 except KeyError:
1335 # Probably some fields validation has failed
1336 return attrs
1337 self._context["translation"].validate_new_unit_data(**data)
1338 return attrs
1341class MonolingualUnitSerializer(NewUnitSerializer):
1342 key = serializers.CharField()
1343 value = PluralField()
1345 def as_kwargs(self, data: dict | None = None) -> NewUnitParams:
1346 if data is None:
1347 data = self.validated_data
1348 return NewUnitParams(
1349 context=data["key"],
1350 source=data["value"],
1351 target=None,
1352 state=data.get("state", None),
1353 )
1356class BilingualUnitSerializer(NewUnitSerializer):
1357 context = serializers.CharField(required=False)
1358 source = PluralField()
1359 target = PluralField()
1361 def as_kwargs(self, data: dict | None = None) -> NewUnitParams:
1362 if data is None:
1363 data = self.validated_data
1364 return NewUnitParams(
1365 context=data.get("context", ""),
1366 source=data["source"],
1367 target=data.get("target", ""),
1368 state=data.get("state", None),
1369 )
1372class BilingualSourceUnitSerializer(BilingualUnitSerializer):
1373 target = PluralField(required=False, child_allow_blank=True)
1376class CategorySerializer(RemovableSerializer[Category]):
1377 project = serializers.HyperlinkedRelatedField(
1378 view_name="api:project-detail",
1379 lookup_field="slug",
1380 queryset=Project.objects.none(),
1381 required=True,
1382 )
1383 category = serializers.HyperlinkedRelatedField(
1384 view_name="api:category-detail",
1385 queryset=Category.objects.none(),
1386 required=False,
1387 )
1388 statistics_url = serializers.HyperlinkedIdentityField(
1389 view_name="api:category-statistics",
1390 lookup_field="pk",
1391 )
1393 class Meta:
1394 model = Category
1395 fields = (
1396 "id",
1397 "name",
1398 "slug",
1399 "project",
1400 "category",
1401 "url",
1402 "statistics_url",
1403 )
1404 extra_kwargs = { # noqa: RUF012
1405 "url": {"view_name": "api:category-detail"},
1406 }
1408 def __init__(self, *args, **kwargs) -> None:
1409 super().__init__(*args, **kwargs)
1410 user = self.context["request"].user
1411 self.fields["project"].queryset = user.managed_projects
1412 self.fields["category"].queryset = Category.objects.filter(
1413 project__in=user.managed_projects
1414 )
1416 def validate(self, attrs):
1417 # Call model validation here, DRF does not do that
1418 if self.instance:
1419 instance = copy(self.instance)
1420 for key, value in attrs.items():
1421 setattr(instance, key, value)
1422 else:
1423 instance = Category(**attrs)
1424 instance.clean()
1425 return attrs
1427 def to_internal_value(self, data):
1428 result = super().to_internal_value(data)
1430 # Add missing project context
1431 if "project" in self._context: 1431 ↛ 1432line 1431 didn't jump to line 1432 because the condition on line 1431 was never true
1432 result["project"] = self._context["project"]
1433 elif self.instance:
1434 result["project"] = self.instance.project
1436 # Workaround for https://github.com/encode/django-rest-framework/issues/7489
1437 if "category" not in result: 1437 ↛ 1439line 1437 didn't jump to line 1439 because the condition on line 1437 was always true
1438 result["category"] = None
1439 return result
1442class ScreenshotSerializer(RemovableSerializer[Screenshot]):
1443 translation = MultiFieldHyperlinkedIdentityField(
1444 view_name="api:translation-detail",
1445 lookup_field=(
1446 "translation__component__project__slug",
1447 "translation__component__slug",
1448 "translation__language__code",
1449 ),
1450 strip_parts=1,
1451 )
1452 file_url = serializers.HyperlinkedRelatedField(
1453 read_only=True, source="pk", view_name="api:screenshot-file"
1454 )
1455 units = serializers.HyperlinkedRelatedField(
1456 many=True, read_only=True, view_name="api:unit-detail"
1457 )
1459 class Meta:
1460 model = Screenshot
1461 fields = (
1462 "id",
1463 "name",
1464 "repository_filename",
1465 "translation",
1466 "file_url",
1467 "units",
1468 "url",
1469 )
1470 extra_kwargs = { # noqa: RUF012
1471 "url": {"view_name": "api:screenshot-detail"}
1472 }
1475class ScreenshotCreateSerializer(ScreenshotSerializer):
1476 class Meta:
1477 model = Screenshot
1478 fields = (
1479 "name",
1480 "repository_filename",
1481 "translation",
1482 "file_url",
1483 "units",
1484 "url",
1485 "image",
1486 )
1487 extra_kwargs = { # noqa: RUF012
1488 "url": {"view_name": "api:screenshot-detail"}
1489 }
1492class ScreenshotFileSerializer(serializers.ModelSerializer[Screenshot]):
1493 image = serializers.ImageField(validators=[validate_bitmap])
1495 class Meta:
1496 model = Screenshot
1497 fields = ("image",)
1498 extra_kwargs = { # noqa: RUF012
1499 "url": {"view_name": "api:screenshot-file"}
1500 }
1503class ChangeSerializer(RemovableSerializer[Change]):
1504 action_name = serializers.CharField(source="get_action_display", read_only=True)
1505 component = MultiFieldHyperlinkedIdentityField(
1506 view_name="api:component-detail",
1507 lookup_field=("component__project__slug", "component__slug"),
1508 strip_parts=1,
1509 )
1510 translation = MultiFieldHyperlinkedIdentityField(
1511 view_name="api:translation-detail",
1512 lookup_field=(
1513 "translation__component__project__slug",
1514 "translation__component__slug",
1515 "translation__language__code",
1516 ),
1517 strip_parts=1,
1518 )
1519 unit = serializers.HyperlinkedRelatedField(
1520 read_only=True, view_name="api:unit-detail"
1521 )
1522 user = serializers.HyperlinkedRelatedField(
1523 read_only=True, view_name="api:user-detail", lookup_field="username"
1524 )
1525 author = serializers.HyperlinkedRelatedField(
1526 read_only=True, view_name="api:user-detail", lookup_field="username"
1527 )
1529 class Meta:
1530 model = Change
1531 fields = (
1532 "unit",
1533 "component",
1534 "translation",
1535 "user",
1536 "author",
1537 "timestamp",
1538 "action",
1539 "target",
1540 "old",
1541 "details",
1542 "id",
1543 "action_name",
1544 "url",
1545 )
1546 extra_kwargs = { # noqa: RUF012
1547 "url": {"view_name": "api:change-detail"}
1548 }
1551class AutoComponentListSerializer(serializers.ModelSerializer[AutoComponentList]):
1552 class Meta:
1553 model = AutoComponentList
1554 fields = (
1555 "project_match",
1556 "component_match",
1557 )
1560class ComponentListSerializer(serializers.ModelSerializer[ComponentList]):
1561 components = MultiFieldHyperlinkedIdentityField(
1562 view_name="api:component-detail",
1563 lookup_field=("project__slug", "slug"),
1564 many=True,
1565 read_only=True,
1566 )
1567 auto_assign = AutoComponentListSerializer(
1568 many=True, source="autocomponentlist_set", read_only=True
1569 )
1571 class Meta:
1572 model = ComponentList
1573 fields = (
1574 "name",
1575 "slug",
1576 "id",
1577 "show_dashboard",
1578 "components",
1579 "auto_assign",
1580 "url",
1581 )
1582 extra_kwargs = { # noqa: RUF012
1583 "url": {"view_name": "api:componentlist-detail", "lookup_field": "slug"}
1584 }
1587class AddonSerializer(serializers.ModelSerializer[Addon]):
1588 component = MultiFieldHyperlinkedIdentityField(
1589 view_name="api:component-detail",
1590 lookup_field=("component__project__slug", "component__slug"),
1591 read_only=True,
1592 strip_parts=1,
1593 )
1594 project = serializers.HyperlinkedRelatedField(
1595 view_name="api:project-detail",
1596 lookup_field="slug",
1597 read_only=True,
1598 )
1599 configuration = serializers.JSONField(required=False)
1601 class Meta:
1602 model = Addon
1603 fields = (
1604 "component",
1605 "project",
1606 "name",
1607 "id",
1608 "configuration",
1609 "url",
1610 )
1611 extra_kwargs = { # noqa: RUF012
1612 "url": {"view_name": "api:addon-detail"}
1613 }
1615 @staticmethod
1616 def check_addon(name, queryset) -> None:
1617 installed = set(queryset.values_list("name", flat=True))
1618 available = {
1619 x.name for x in ADDONS.values() if x.multiple or x.name not in installed
1620 }
1621 if name not in available:
1622 raise serializers.ValidationError(
1623 {"name": f"Add-on already installed: {name}"}
1624 )
1626 def validate(self, attrs):
1627 instance = self.instance
1628 try:
1629 name = attrs["name"]
1630 except KeyError as error:
1631 if self.partial and instance:
1632 name = instance.name
1633 else:
1634 raise serializers.ValidationError(
1635 {"name": "Can not change add-on name"}
1636 ) from error
1637 # Update or create
1638 component = instance.component if instance else self._context.get("component")
1639 project = instance.project if instance else self._context.get("project")
1641 # This could probably work, but it safer not to allow it
1642 if instance and instance.name != name: 1642 ↛ 1643line 1642 didn't jump to line 1643 because the condition on line 1642 was never true
1643 raise serializers.ValidationError({"name": "Can not change add-on name"})
1644 try:
1645 addon_class = ADDONS[name]
1646 except KeyError as error:
1647 raise serializers.ValidationError(
1648 {"name": f"Add-on not found: {name}"}
1649 ) from error
1651 # Don't allow duplicate add-ons
1652 addon = addon_class(Addon())
1653 if not component and addon_class.needs_component:
1654 raise serializers.ValidationError(
1655 {"component": "This add-on can only be installed on the component."}
1656 )
1657 if not instance:
1658 if component:
1659 self.check_addon(name, Addon.objects.filter_component(component))
1660 if not addon.can_install(component, None):
1661 raise serializers.ValidationError(
1662 {"name": f"could not enable add-on {name}, not compatible"}
1663 )
1664 if project:
1665 self.check_addon(name, Addon.objects.filter_project(project))
1667 if addon.has_settings() and "configuration" in attrs:
1668 form = addon.get_add_form(
1669 None,
1670 component=component,
1671 project=project,
1672 data=attrs["configuration"],
1673 )
1674 form.is_valid()
1675 if not form.is_valid():
1676 raise serializers.ValidationError(
1677 {"configuration": list(get_form_errors(form))}
1678 )
1679 return attrs
1681 def create(self, validated_data):
1682 validated_data["acting_user"] = self.context["request"].user
1683 return super().create(validated_data)
1685 def save(self, **kwargs):
1686 result = super().save(**kwargs)
1687 self.instance.addon.post_configure()
1688 return result
1691class MetricsSerializer(ReadOnlySerializer):
1692 units = serializers.IntegerField(source="all")
1693 units_translated = serializers.IntegerField(source="translated")
1694 users = serializers.IntegerField(source="get_users")
1695 changes = serializers.IntegerField(source="total_changes")
1696 projects = serializers.IntegerField(source="get_projects")
1697 components = serializers.IntegerField(source="get_components")
1698 translations = serializers.IntegerField(source="get_translations")
1700 languages = serializers.IntegerField(source="get_languages")
1701 checks = serializers.IntegerField(source="get_checks")
1702 configuration_errors = serializers.IntegerField(source="get_configuration_errors")
1703 suggestions = serializers.IntegerField(source="get_suggestions")
1704 celery_queues = serializers.DictField(
1705 child=serializers.IntegerField(), source="get_celery_queues"
1706 )
1707 name = serializers.CharField(source="get_name")
1710@extend_schema_serializer(
1711 examples=[
1712 OpenApiExample(
1713 "Service settings example",
1714 value={
1715 "service": "service_name",
1716 "configuration": {"key": "xxxxx", "url": "https://api.service.com/"},
1717 },
1718 )
1719 ]
1720)
1721class SingleServiceConfigSerializer(serializers.Serializer):
1722 service = serializers.CharField()
1723 configuration = serializers.DictField()
1726@extend_schema_serializer(
1727 examples=[
1728 OpenApiExample(
1729 "Service settings example",
1730 value={
1731 "service1": {"key": "XXXXXXX", "url": "https://api.service.com/"},
1732 "service2": {"secret": "SECRET_KEY", "credentials": "XXXXXXX"},
1733 },
1734 request_only=False,
1735 response_only=True,
1736 )
1737 ]
1738)
1739class ProjectMachinerySettingsSerializer(serializers.Serializer):
1740 def to_representation(self, instance: Project):
1741 return dict(instance.machinery_settings)
1744class ProjectMachinerySettingsSerializerExtension(OpenApiSerializerExtension):
1745 target_class = ProjectMachinerySettingsSerializer
1747 def map_serializer(self, auto_schema: AutoSchema, direction):
1748 return build_object_type(properties={"service_name": build_basic_type(dict)})
1751def edit_service_settings_response_serializer(
1752 method: str, *codes
1753) -> dict[int, serializers.Serializer]:
1754 serializers_ = {
1755 200: inline_serializer(
1756 f"{method}_200_Message_response_serializer",
1757 fields={
1758 "message": serializers.CharField(),
1759 },
1760 ),
1761 201: inline_serializer(
1762 f"{method}_201_Message_response_serializer",
1763 fields={
1764 "message": serializers.CharField(),
1765 },
1766 ),
1767 400: inline_serializer(
1768 f"{method}_400_Error_message_serializer",
1769 fields={"errors": serializers.CharField()},
1770 ),
1771 }
1772 return {code: serializers_[code] for code in codes}
1775class ErrorCode423Enum(models.TextChoices):
1776 REPOSITORY_LOCKED = "repository-locked"
1777 COMPONENT_LOCKED = "component-locked"
1778 UNKNOWN_LOCKED = "unknown-locked"
1781class Error423Serializer(serializers.Serializer):
1782 code = serializers.ChoiceField(choices=ErrorCode423Enum.choices)
1783 detail = serializers.CharField()
1784 attr = serializers.CharField(allow_null=True)
1787class ErrorResponse423Serializer(serializers.Serializer):
1788 type = serializers.ChoiceField(choices=ServerErrorEnum.choices)
1789 errors = Error423Serializer(many=True)