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

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

2# 

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

4 

5from __future__ import annotations 

6 

7from copy import copy 

8from typing import TYPE_CHECKING, TypeVar, cast 

9from zipfile import BadZipfile 

10 

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 

25 

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) 

58 

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 

62 

63 from weblate.auth.models import AuthenticatedHttpRequest 

64 

65_MT = TypeVar("_MT", bound=Model) # Model Type 

66 

67 

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 

89 

90 

91class MultiFieldHyperlinkedIdentityField(serializers.HyperlinkedIdentityField): 

92 lookup_field: tuple[str, ...] # type: ignore[assignment] 

93 

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 

100 

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. 

104 

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 

111 

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) 

116 

117 

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 

124 

125 

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) 

130 

131 if remove_fields: 

132 # for multiple fields in a list 

133 for field_name in remove_fields: 

134 self.fields.pop(field_name) 

135 

136 

137class LanguagePluralSerializer(serializers.ModelSerializer[Plural]): 

138 class Meta: 

139 model = Plural 

140 fields = ( 

141 "id", 

142 "source", 

143 "number", 

144 "formula", 

145 "type", 

146 ) 

147 

148 

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 ) 

157 

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 } 

176 

177 @property 

178 def is_source_language(self): 

179 return ( 

180 isinstance(self.parent, ComponentSerializer) 

181 and self.field_name == "source_language" 

182 ) 

183 

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 

190 

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 

196 

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 

202 

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) 

208 

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 

217 

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) 

224 

225 

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 ) 

251 

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 } 

280 

281 

282class BasicUserSerializer(serializers.ModelSerializer[User]): 

283 class Meta: 

284 model = User 

285 fields = ( 

286 "id", 

287 "full_name", 

288 "username", 

289 ) 

290 

291 

292@extend_schema_field(str) 

293class PermissionSerializer(serializers.RelatedField[Permission, str, str]): 

294 class Meta: 

295 model = Permission 

296 

297 def to_representation(self, value): 

298 return value.codename 

299 

300 def get_queryset(self): 

301 return Permission.objects.all() 

302 

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 

309 

310 

311class RoleSerializer(serializers.ModelSerializer[Role]): 

312 permissions = PermissionSerializer(many=True) 

313 

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 } 

325 

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 

333 

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 

347 

348 

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 ) 

378 

379 id = serializers.IntegerField(read_only=True) 

380 user = serializers.HyperlinkedRelatedField( 

381 read_only=True, view_name="api:user-detail", lookup_field="username" 

382 ) 

383 

384 class Meta: 

385 model = Comment 

386 fields = ("scope", "comment", "timestamp", "user_email", "id", "user") 

387 

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 

392 

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) 

397 

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) 

402 

403 return value 

404 

405 def create(self, validated_data): 

406 request = self.context["request"] 

407 unit = self.context["unit"] 

408 

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) 

413 

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 

419 

420 return Comment.objects.add( 

421 request=request, 

422 unit=unit, 

423 text=text, 

424 scope=scope, 

425 user=user, 

426 timestamp=timestamp, 

427 ) 

428 

429 

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 ) 

467 

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 } 

487 

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 

492 

493 

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) 

527 

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 } 

562 

563 

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) 

569 

570 

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) 

577 

578 

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 ) 

588 

589 def get_attribute(self, instance): 

590 return instance 

591 

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) 

596 

597 

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) 

624 

625 repo = RepoField(max_length=REPO_LENGTH) 

626 

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) 

633 

634 serializer_url_field = MultiFieldHyperlinkedIdentityField 

635 

636 zipfile = serializers.FileField(required=False) 

637 docfile = serializers.FileField(required=False) 

638 disable_autoshare = serializers.BooleanField(required=False) 

639 

640 enforced_checks = serializers.JSONField(required=False) 

641 

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 ) 

654 

655 task_url = RelatedTaskField(lookup_field="background_task_id") 

656 

657 addons = serializers.HyperlinkedIdentityField( 

658 view_name="api:addon-detail", 

659 source="addon_set", 

660 many=True, 

661 read_only=True, 

662 ) 

663 

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 } 

742 

743 def __init__(self, *args, **kwargs) -> None: 

744 super().__init__(*args, **kwargs) 

745 

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"] 

751 

752 if project is not None: 

753 self.fields["category"].queryset = project.category_set.all() 

754 

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 

764 

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 

777 

778 def to_internal_value(self, data): 

779 # Preprocess to inject params based on content 

780 data = data.copy() 

781 

782 # Provide a reasonable default 

783 if "manage_units" not in data and data.get("template"): 

784 data["manage_units"] = "1" 

785 

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" 

791 

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) 

796 

797 # DRF processing 

798 result = super().to_internal_value(data) 

799 

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 ) 

806 

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 

812 

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 

816 

817 return result 

818 

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) 

824 

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 ) 

831 

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) 

839 

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

844 

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 

858 

859 # Call model validation here, DRF does not do that 

860 instance.clean() 

861 

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 

868 

869 

870class NotificationSerializer(serializers.ModelSerializer[Subscription]): 

871 project = ProjectSerializer(read_only=True) 

872 component = ComponentSerializer(read_only=True) 

873 

874 class Meta: 

875 model = Subscription 

876 fields = ( 

877 "notification", 

878 "id", 

879 "scope", 

880 "frequency", 

881 "project", 

882 "component", 

883 ) 

884 

885 

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 ) 

939 

940 serializer_url_field = MultiFieldHyperlinkedIdentityField 

941 

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 } 

988 

989 

990class ReadOnlySerializer(serializers.Serializer): 

991 def update(self, instance, validated_data) -> None: 

992 return None 

993 

994 def create(self, validated_data) -> None: 

995 return None 

996 

997 

998class LockSerializer(serializers.ModelSerializer[Component]): 

999 class Meta: 

1000 model = Component 

1001 fields = ("locked",) 

1002 

1003 

1004class ProjectLockSerializer(serializers.ModelSerializer[Project]): 

1005 class Meta: 

1006 model = Project 

1007 fields = ("locked",) 

1008 

1009 

1010class LockRequestSerializer(ReadOnlySerializer): 

1011 lock = serializers.BooleanField() 

1012 

1013 

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 ) 

1039 

1040 def validate_conflicts(self, value): 

1041 # These are handled same 

1042 if value == "ignore": 

1043 return "" 

1044 return value 

1045 

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

1056 

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 ) 

1064 

1065 

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 ) 

1093 

1094 

1095class RepoRequestSerializer(ReadOnlySerializer): 

1096 operation = serializers.ChoiceField( 

1097 choices=RepoOperations.choices, 

1098 ) 

1099 

1100 

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 

1149 

1150 

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 } 

1161 

1162 

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) 

1169 

1170 def get_attribute(self, instance): 

1171 return getattr(instance, f"get_{self.field_name}_plurals")() 

1172 

1173 

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 ) 

1188 

1189 

1190class LabelSerializer(serializers.ModelSerializer[Label]): 

1191 class Meta: 

1192 model = Label 

1193 fields = ("id", "name", "description", "color") 

1194 read_only_fields = ("project",) 

1195 

1196 

1197class UnitLabelsSerializer(serializers.RelatedField, LabelSerializer): 

1198 def get_queryset(self): 

1199 """ 

1200 List of available labels for an unit. 

1201 

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

1213 

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 

1226 

1227 

1228class UnitFlatLabelsSerializer(UnitLabelsSerializer): 

1229 def to_representation(self, value): 

1230 return value.id 

1231 

1232 

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) 

1256 

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 } 

1295 

1296 

1297class UnitWriteSerializer(serializers.ModelSerializer[Unit]): 

1298 """Serializer for updating source unit.""" 

1299 

1300 target = PluralField() 

1301 labels = UnitFlatLabelsSerializer(many=True) 

1302 

1303 class Meta: 

1304 model = Unit 

1305 fields = ( 

1306 "target", 

1307 "state", 

1308 "explanation", 

1309 "extra_flags", 

1310 "labels", 

1311 ) 

1312 

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) 

1318 

1319 

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 ) 

1327 

1328 def as_kwargs(self, data: dict | None = None) -> NewUnitParams: 

1329 raise NotImplementedError 

1330 

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 

1339 

1340 

1341class MonolingualUnitSerializer(NewUnitSerializer): 

1342 key = serializers.CharField() 

1343 value = PluralField() 

1344 

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 ) 

1354 

1355 

1356class BilingualUnitSerializer(NewUnitSerializer): 

1357 context = serializers.CharField(required=False) 

1358 source = PluralField() 

1359 target = PluralField() 

1360 

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 ) 

1370 

1371 

1372class BilingualSourceUnitSerializer(BilingualUnitSerializer): 

1373 target = PluralField(required=False, child_allow_blank=True) 

1374 

1375 

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 ) 

1392 

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 } 

1407 

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 ) 

1415 

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 

1426 

1427 def to_internal_value(self, data): 

1428 result = super().to_internal_value(data) 

1429 

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 

1435 

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 

1440 

1441 

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 ) 

1458 

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 } 

1473 

1474 

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 } 

1490 

1491 

1492class ScreenshotFileSerializer(serializers.ModelSerializer[Screenshot]): 

1493 image = serializers.ImageField(validators=[validate_bitmap]) 

1494 

1495 class Meta: 

1496 model = Screenshot 

1497 fields = ("image",) 

1498 extra_kwargs = { # noqa: RUF012 

1499 "url": {"view_name": "api:screenshot-file"} 

1500 } 

1501 

1502 

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 ) 

1528 

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 } 

1549 

1550 

1551class AutoComponentListSerializer(serializers.ModelSerializer[AutoComponentList]): 

1552 class Meta: 

1553 model = AutoComponentList 

1554 fields = ( 

1555 "project_match", 

1556 "component_match", 

1557 ) 

1558 

1559 

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 ) 

1570 

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 } 

1585 

1586 

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) 

1600 

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 } 

1614 

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 ) 

1625 

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

1640 

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 

1650 

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

1666 

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 

1680 

1681 def create(self, validated_data): 

1682 validated_data["acting_user"] = self.context["request"].user 

1683 return super().create(validated_data) 

1684 

1685 def save(self, **kwargs): 

1686 result = super().save(**kwargs) 

1687 self.instance.addon.post_configure() 

1688 return result 

1689 

1690 

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

1699 

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

1708 

1709 

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

1724 

1725 

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) 

1742 

1743 

1744class ProjectMachinerySettingsSerializerExtension(OpenApiSerializerExtension): 

1745 target_class = ProjectMachinerySettingsSerializer 

1746 

1747 def map_serializer(self, auto_schema: AutoSchema, direction): 

1748 return build_object_type(properties={"service_name": build_basic_type(dict)}) 

1749 

1750 

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} 

1773 

1774 

1775class ErrorCode423Enum(models.TextChoices): 

1776 REPOSITORY_LOCKED = "repository-locked" 

1777 COMPONENT_LOCKED = "component-locked" 

1778 UNKNOWN_LOCKED = "unknown-locked" 

1779 

1780 

1781class Error423Serializer(serializers.Serializer): 

1782 code = serializers.ChoiceField(choices=ErrorCode423Enum.choices) 

1783 detail = serializers.CharField() 

1784 attr = serializers.CharField(allow_null=True) 

1785 

1786 

1787class ErrorResponse423Serializer(serializers.Serializer): 

1788 type = serializers.ChoiceField(choices=ServerErrorEnum.choices) 

1789 errors = Error423Serializer(many=True)