Coverage for app/venv/lib/python3.14/site-packages/weblate/trans/migrations/0046_component_file_format_params.py: 34%
57 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
5# Generated by Django 5.2.3 on 2025-07-07 06:44
7from collections import defaultdict
8from typing import Any
10from django.db import migrations, models
12ADDON_CONFIG_TO_FILE_FORMAT_PARAMS: dict[str, dict[str, Any]] = {
13 "weblate.json.customize": {
14 "file_formats": (
15 "json",
16 "json-nested",
17 "webextension",
18 "i18next",
19 "i18nextv4",
20 "arb",
21 "go-i18n-json",
22 "go-i18n-json-v2",
23 "formatjs",
24 "gotext",
25 ),
26 "format_params": {
27 "sort_keys": "json_sort_keys",
28 "use_compact_separators": "json_use_compact_separators",
29 "indent": "json_indent",
30 "style": "json_indent_style",
31 },
32 },
33 "weblate.gettext.msgmerge": {
34 "file_formats": ("po",),
35 "format_params": {
36 "previous": "po_keep_previous",
37 "no_location": "po_no_location",
38 "fuzzy": "po_fuzzy_matching",
39 },
40 },
41 "weblate.gettext.customize": {
42 "file_formats": (
43 "po",
44 "po-mono",
45 ),
46 "format_params": {
47 "width": "po_line_wrap",
48 },
49 },
50 "weblate.yaml.customize": {
51 "file_formats": (
52 "yaml",
53 "ruby-yaml",
54 ),
55 "format_params": {
56 "indent": "yaml_indent",
57 "width": "yaml_line_wrap",
58 "line_break": "yaml_line_break",
59 },
60 },
61 "weblate.xml.customize": {
62 "file_formats": (
63 "ts",
64 "plainxliff",
65 "xliff",
66 "poxliff",
67 "resx",
68 "aresource",
69 "moko-resource",
70 "cmp-resource",
71 "tbx",
72 ),
73 "format_params": {
74 "closing_tags": "xml_closing_tags",
75 },
76 },
77}
80def migrate_addons_config(apps, schema_editor) -> None:
81 Component = apps.get_model("trans", "Component")
82 Addon = apps.get_model("addons", "Addon")
83 to_update = []
85 addon_configs = collect_addon_configurations(apps)
87 for component in Component.objects.all(): 87 ↛ 88line 87 didn't jump to line 88 because the loop on line 87 never started
88 file_format_params = merge_file_format_params(component, addon_configs)
89 if file_format_params:
90 component.file_format_params = file_format_params
91 to_update.append(component)
92 if to_update: 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true
93 Component.objects.bulk_update(to_update, ["file_format_params"])
95 # delete stale addons
96 # weblate.xml.customize was missed here, it is handled in
97 # weblate/addons/migrations/0009_remove_migrated_customize_addons.py
98 to_delete = [
99 "weblate.json.customize",
100 "weblate.gettext.customize",
101 "weblate.yaml.customize",
102 ]
103 Addon.objects.filter(name__in=to_delete).delete()
105 # remove configurations from msgmerge addon
106 Addon.objects.filter(name="weblate.gettext.msgmerge").update(configuration={})
109def convert_addon_config_to_file_format_params(addon) -> tuple[tuple, dict[str, Any]]:
110 """Convert addon configuration to file format parameters."""
111 attrs = ADDON_CONFIG_TO_FILE_FORMAT_PARAMS[addon.name]
112 params = {}
113 for addon_config, param_name in attrs["format_params"].items():
114 if addon_config in addon.configuration:
115 params[param_name] = addon.configuration[addon_config]
116 return attrs["file_formats"], params
119def collect_addon_configurations(apps) -> dict[str, list | dict]:
120 """Collect and organize addon configurations by scope."""
121 Addon = apps.get_model("addons", "Addon")
123 addons = Addon.objects.filter(
124 name__in=ADDON_CONFIG_TO_FILE_FORMAT_PARAMS.keys()
125 ).select_related("component", "project")
127 site_wide = []
128 project_wide = defaultdict(list)
129 component_wide = defaultdict(list)
131 for addon in addons: 131 ↛ 132line 131 didn't jump to line 132 because the loop on line 131 never started
132 file_formats, params = convert_addon_config_to_file_format_params(addon)
133 config_entry = (file_formats, params)
135 if not addon.component and not addon.project:
136 site_wide.append(config_entry)
137 elif addon.project and not addon.component:
138 project_wide[addon.project_id].append(config_entry)
139 elif addon.component:
140 component_wide[addon.component_id].append(config_entry)
141 return {
142 "site_wide": site_wide,
143 "project_wide": project_wide,
144 "component_wide": component_wide,
145 }
148def merge_file_format_params(component, configs) -> dict[str, bool | int | str]:
149 """Merge file format parameters for a component based on priority."""
150 file_format_params = {}
151 for file_formats, params in configs["site_wide"]:
152 if component.file_format in file_formats:
153 file_format_params.update(params)
155 for file_formats, params in configs["project_wide"].get(component.project_id, []):
156 if component.file_format in file_formats:
157 file_format_params.update(params)
159 for file_formats, params in configs["component_wide"].get(component.id, []):
160 if component.file_format in file_formats:
161 file_format_params.update(params)
163 return file_format_params
166class Migration(migrations.Migration):
167 dependencies = [
168 ("trans", "0045_alter_change_action"),
169 ("addons", "0006_alter_addonactivitylog_component"),
170 ]
172 operations = [
173 migrations.AddField(
174 model_name="component",
175 name="file_format_params",
176 field=models.JSONField(
177 blank=True, default=dict, verbose_name="File format parameters"
178 ),
179 ),
180 migrations.RunPython(
181 migrate_addons_config, migrations.RunPython.noop, elidable=True
182 ),
183 ]