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
« 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 typing import TYPE_CHECKING
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
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
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
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,))
34@method_decorator(login_required, name="dispatch")
35class FontListView(ProjectViewMixin, ListView):
36 model = Font
37 _font_form = None
38 _group_form = None
40 def get_queryset(self):
41 return self.project.font_set.order_by("family", "style")
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
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)
91@method_decorator(login_required, name="dispatch")
92class FontDetailView(ProjectViewMixin, DetailView):
93 model = Font
94 _font_form = None
96 def get_queryset(self):
97 return self.project.font_set.all()
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
105 def post(self, request: AuthenticatedHttpRequest, **kwargs):
106 self.object = self.get_object()
108 if not request.user.has_perm("project.edit", self.project):
109 raise PermissionDenied
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)
122 form = self._fort_form = FontForm(data=request.POST, files=request.FILES)
123 if not form.is_valid():
124 return self.get(request, **kwargs)
126 new_file = form.cleaned_data["font"]
128 # This should not fail as font was loaded during the form validation
129 uploaded_family, uploaded_style = get_font_name(new_file)
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)
142 # Compare file content
143 current_content = self.object.font.read()
144 self.object.font.close()
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."))
163 return redirect(self.object)
166@method_decorator(login_required, name="dispatch")
167class FontGroupDetailView(ProjectViewMixin, DetailView):
168 model = FontGroup
169 _form: FontOverrideForm | FontGroupForm | None = None
170 _override_form = None
172 def get_queryset(self):
173 return self.project.fontgroup_set.all()
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
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
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)
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)
234 self.object.delete()
235 messages.error(request, gettext("Font group deleted."))
236 return redirect("fonts", project=self.project.slug)