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

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

2# 

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

4 

5# Generated by Django 5.2.3 on 2025-07-07 06:44 

6 

7from collections import defaultdict 

8from typing import Any 

9 

10from django.db import migrations, models 

11 

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} 

78 

79 

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 = [] 

84 

85 addon_configs = collect_addon_configurations(apps) 

86 

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

94 

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

104 

105 # remove configurations from msgmerge addon 

106 Addon.objects.filter(name="weblate.gettext.msgmerge").update(configuration={}) 

107 

108 

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 

117 

118 

119def collect_addon_configurations(apps) -> dict[str, list | dict]: 

120 """Collect and organize addon configurations by scope.""" 

121 Addon = apps.get_model("addons", "Addon") 

122 

123 addons = Addon.objects.filter( 

124 name__in=ADDON_CONFIG_TO_FILE_FORMAT_PARAMS.keys() 

125 ).select_related("component", "project") 

126 

127 site_wide = [] 

128 project_wide = defaultdict(list) 

129 component_wide = defaultdict(list) 

130 

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) 

134 

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 } 

146 

147 

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) 

154 

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) 

158 

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) 

162 

163 return file_format_params 

164 

165 

166class Migration(migrations.Migration): 

167 dependencies = [ 

168 ("trans", "0045_alter_change_action"), 

169 ("addons", "0006_alter_addonactivitylog_component"), 

170 ] 

171 

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 ]