Coverage for app/venv/lib/python3.14/site-packages/weblate/fonts/views.py: 24%

143 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 typing import TYPE_CHECKING 

8 

9from django.contrib.auth.decorators import login_required 

10from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError 

11from django.shortcuts import redirect 

12from django.utils.decorators import method_decorator 

13from django.utils.translation import gettext 

14from django.views.generic import DetailView, ListView, View 

15 

16from weblate.fonts.forms import FontForm, FontGroupForm, FontOverrideForm 

17from weblate.fonts.models import Font, FontGroup 

18from weblate.fonts.utils import get_font_name 

19from weblate.trans.actions import ActionEvents 

20from weblate.trans.models import Change, Project 

21from weblate.utils import messages 

22from weblate.utils.views import parse_path 

23 

24if TYPE_CHECKING: 24 ↛ 25line 24 didn't jump to line 25 because the condition on line 24 was never true

25 from weblate.auth.models import AuthenticatedHttpRequest 

26 

27 

28class ProjectViewMixin(View): 

29 def setup(self, request: AuthenticatedHttpRequest, *args, **kwargs) -> None: 

30 super().setup(request, *args, **kwargs) 

31 self.project = parse_path(request, [self.kwargs["project"]], (Project,)) 

32 

33 

34@method_decorator(login_required, name="dispatch") 

35class FontListView(ProjectViewMixin, ListView): 

36 model = Font 

37 _font_form = None 

38 _group_form = None 

39 

40 def get_queryset(self): 

41 return self.project.font_set.order_by("family", "style") 

42 

43 def get_context_data(self, **kwargs): 

44 result = super().get_context_data(**kwargs) 

45 result["object"] = self.project 

46 result["font_list"] = result["object_list"] 

47 result["group_list"] = self.project.fontgroup_set.order() 

48 result["font_form"] = self._font_form or FontForm() 

49 result["group_form"] = self._group_form or FontGroupForm( 

50 auto_id="id_group_%s", project=self.project 

51 ) 

52 result["can_edit"] = self.request.user.has_perm("project.edit", self.project) 

53 return result 

54 

55 def post(self, request: AuthenticatedHttpRequest, **kwargs): 

56 if not request.user.has_perm("project.edit", self.project): 

57 raise PermissionDenied 

58 form: FontForm | FontGroupForm 

59 if request.FILES: 

60 form = self._font_form = FontForm(request.POST, request.FILES) 

61 else: 

62 form = self._group_form = FontGroupForm( 

63 request.POST, auto_id="id_group_%s", project=self.project 

64 ) 

65 if form.is_valid(): 

66 instance = form.save(commit=False) 

67 Change.objects.create( 

68 action=ActionEvents.FONT_CREATE, 

69 user=request.user, 

70 target=str(instance), 

71 project=self.project, 

72 ) 

73 instance.project = self.project 

74 instance.user = request.user 

75 try: 

76 instance.validate_unique() 

77 except ValidationError: 

78 messages.error( 

79 request, gettext("Font with the same name already exists.") 

80 ) 

81 else: 

82 instance.save() 

83 return redirect(instance) 

84 else: 

85 messages.error( 

86 request, gettext("Creation failed, please fix the errors below.") 

87 ) 

88 return self.get(request, **kwargs) 

89 

90 

91@method_decorator(login_required, name="dispatch") 

92class FontDetailView(ProjectViewMixin, DetailView): 

93 model = Font 

94 _font_form = None 

95 

96 def get_queryset(self): 

97 return self.project.font_set.all() 

98 

99 def get_context_data(self, **kwargs): 

100 result = super().get_context_data(**kwargs) 

101 result["can_edit"] = self.request.user.has_perm("project.edit", self.project) 

102 result["font_form"] = self._font_form or FontForm() 

103 return result 

104 

105 def post(self, request: AuthenticatedHttpRequest, **kwargs): 

106 self.object = self.get_object() 

107 

108 if not request.user.has_perm("project.edit", self.project): 

109 raise PermissionDenied 

110 

111 if "delete" in request.POST: 

112 self.object.delete() 

113 Change.objects.create( 

114 action=ActionEvents.FONT_REMOVE, 

115 user=request.user, 

116 target=str(self.object), 

117 project=self.project, 

118 ) 

119 messages.error(request, gettext("Font deleted.")) 

120 return redirect("fonts", project=self.project.slug) 

121 

122 form = self._fort_form = FontForm(data=request.POST, files=request.FILES) 

123 if not form.is_valid(): 

124 return self.get(request, **kwargs) 

125 

126 new_file = form.cleaned_data["font"] 

127 

128 # This should not fail as font was loaded during the form validation 

129 uploaded_family, uploaded_style = get_font_name(new_file) 

130 

131 # Enforce same family & style 

132 if uploaded_family != self.object.family or uploaded_style != self.object.style: 

133 messages.error( 

134 request, 

135 gettext( 

136 "The uploaded font must match the existing family and style: “%(family)s %(style)s”" 

137 ) 

138 % {"family": self.object.family, "style": self.object.style}, 

139 ) 

140 return self.get(request, **kwargs) 

141 

142 # Compare file content 

143 current_content = self.object.font.read() 

144 self.object.font.close() 

145 

146 if new_file.loaded_font.font_bytes == current_content: 

147 messages.info( 

148 request, 

149 gettext("The uploaded font file is identical to the current one."), 

150 ) 

151 else: 

152 self.object.font = new_file 

153 self.object.user = request.user 

154 self.object.save() 

155 Change.objects.create( 

156 action=ActionEvents.FONT_CHANGE, 

157 user=request.user, 

158 target=str(self.object), 

159 project=self.project, 

160 ) 

161 messages.success(request, gettext("Font updated successfully.")) 

162 

163 return redirect(self.object) 

164 

165 

166@method_decorator(login_required, name="dispatch") 

167class FontGroupDetailView(ProjectViewMixin, DetailView): 

168 model = FontGroup 

169 _form: FontOverrideForm | FontGroupForm | None = None 

170 _override_form = None 

171 

172 def get_queryset(self): 

173 return self.project.fontgroup_set.all() 

174 

175 def get_context_data(self, **kwargs): 

176 result = super().get_context_data(**kwargs) 

177 result["form"] = self._form or FontGroupForm( 

178 instance=self.object, project=self.project 

179 ) 

180 result["override_form"] = self._override_form or FontOverrideForm() 

181 result["can_edit"] = self.request.user.has_perm("project.edit", self.project) 

182 return result 

183 

184 def post(self, request: AuthenticatedHttpRequest, **kwargs): 

185 self.object = self.get_object() 

186 if not request.user.has_perm("project.edit", self.project): 

187 raise PermissionDenied 

188 

189 form: FontOverrideForm | FontGroupForm 

190 if "name" in request.POST: 

191 form = self._form = FontGroupForm( 

192 request.POST, instance=self.object, project=self.project 

193 ) 

194 if form.is_valid(): 

195 instance = form.save(commit=False) 

196 try: 

197 instance.validate_unique() 

198 except ValidationError: 

199 messages.error( 

200 request, 

201 gettext("Font group with the same name already exists."), 

202 ) 

203 else: 

204 instance.save() 

205 return redirect(self.object) 

206 return self.get(request, **kwargs) 

207 if "language" in request.POST: 

208 form = self._form = FontOverrideForm(request.POST) 

209 if form.is_valid(): 

210 instance = form.save(commit=False) 

211 instance.group = self.object 

212 try: 

213 instance.validate_unique() 

214 except ValidationError: 

215 messages.error( 

216 request, 

217 gettext("Font group with the same name already exists."), 

218 ) 

219 else: 

220 instance.save() 

221 return redirect(self.object) 

222 

223 return self.get(request, **kwargs) 

224 if "override" in request.POST: 

225 try: 

226 self.object.fontoverride_set.filter( 

227 pk=int(request.POST["override"]) 

228 ).delete() 

229 except (ValueError, ObjectDoesNotExist): 

230 messages.error(request, gettext("No override found.")) 

231 else: 

232 return redirect(self.object) 

233 

234 self.object.delete() 

235 messages.error(request, gettext("Font group deleted.")) 

236 return redirect("fonts", project=self.project.slug)