Coverage for app/venv/lib/python3.14/site-packages/weblate/utils/forms.py: 66%
134 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
4from __future__ import annotations
6from typing import Literal
8from crispy_forms.layout import Div, Field
9from crispy_forms.utils import TEMPLATE_PACK
10from django import forms
11from django.core.exceptions import ValidationError
12from django.db.models import Q
13from django.forms.models import ModelChoiceIterator
14from django.template.loader import render_to_string
15from django.utils.text import normalize_newlines
16from django.utils.translation import gettext, gettext_lazy
17from pyparsing import ParseException
19from weblate.formats.helpers import CONTROLCHARS
20from weblate.trans.defines import EMAIL_LENGTH, USERNAME_LENGTH
21from weblate.trans.filter import FILTERS
22from weblate.trans.util import sort_unicode
23from weblate.utils.errors import report_error
25from .validators import WeblateServiceURLValidator, validate_email, validate_username
28class QueryField(forms.CharField):
29 def __init__(
30 self, parser: Literal["unit", "user", "superuser"] = "unit", **kwargs
31 ) -> None:
32 if "label" not in kwargs: 32 ↛ 34line 32 didn't jump to line 34 because the condition on line 32 was always true
33 kwargs["label"] = gettext_lazy("Query")
34 if "required" not in kwargs:
35 kwargs["required"] = False
36 if "widget" not in kwargs: 36 ↛ 38line 36 didn't jump to line 38 because the condition on line 36 was always true
37 kwargs["widget"] = forms.Textarea(attrs={"cols": None, "rows": 1})
38 self.parser = parser
39 super().__init__(**kwargs)
41 def clean(self, value):
42 from weblate.utils.search import parse_query
44 if not value: 44 ↛ 48line 44 didn't jump to line 48 because the condition on line 44 was always true
45 if self.required: 45 ↛ 47line 45 didn't jump to line 47 because the condition on line 45 was always true
46 raise ValidationError(gettext("Missing query string."))
47 return ""
48 try:
49 parse_query(value, parser=self.parser)
50 except (ValueError, ParseException) as error:
51 raise ValidationError(
52 gettext("Could not parse query string: {}").format(error)
53 ) from error
54 except Exception as error:
55 report_error("Error parsing search query")
56 raise ValidationError(
57 gettext("Could not parse query string: {}").format(error)
58 ) from error
59 return value
62class UsernameField(forms.CharField):
63 default_validators = [validate_username] # noqa: RUF012
65 def __init__(
66 self,
67 *,
68 max_length: int | None = None,
69 min_length: int | None = None,
70 strip: bool = True,
71 empty_value: str = "",
72 **kwargs,
73 ) -> None:
74 params = {
75 "max_length": USERNAME_LENGTH,
76 "help_text": gettext_lazy(
77 "Username may only contain letters, "
78 "numbers or the following characters: @ . + - _"
79 ),
80 "label": gettext_lazy("Username"),
81 "required": True,
82 }
83 params.update(kwargs)
84 self.valid = None
86 super().__init__(
87 max_length=max_length,
88 min_length=min_length,
89 strip=strip,
90 empty_value=empty_value,
91 **kwargs,
92 )
95class UserField(forms.CharField):
96 def __init__(
97 self,
98 queryset=None,
99 empty_label="---------",
100 to_field_name=None,
101 limit_choices_to=None,
102 blank=None,
103 **kwargs,
104 ) -> None:
105 # This swallows some parameters to mimic ModelChoiceField API
106 super().__init__(**kwargs)
108 def widget_attrs(self, widget):
109 attrs = super().widget_attrs(widget)
110 attrs["dir"] = "ltr"
111 attrs["class"] = "user-autocomplete"
112 attrs["spellcheck"] = "false"
113 attrs["autocorrect"] = "off"
114 attrs["autocomplete"] = "off"
115 attrs["autocapitalize"] = "off"
116 return attrs
118 def clean(self, value):
119 from weblate.auth.models import User
121 if not value:
122 if self.required:
123 raise ValidationError(gettext("Missing username or e-mail."))
124 return None
125 if any(char in value for char in CONTROLCHARS):
126 raise ValidationError(
127 gettext("String contains control character: %s") % repr(value)
128 )
129 try:
130 return User.objects.get(Q(username=value) | Q(email=value))
131 except User.DoesNotExist as error:
132 raise ValidationError(gettext("Could not find any such user.")) from error
133 except User.MultipleObjectsReturned as error:
134 raise ValidationError(gettext("More possible users were found.")) from error
137class EmailField(forms.EmailField):
138 """
139 Slightly restricted EmailField.
141 We block some additional local parts and customize error messages.
142 """
144 default_validators = [validate_email] # noqa: RUF012
146 def __init__(self, *args, **kwargs) -> None:
147 kwargs.setdefault("max_length", EMAIL_LENGTH)
148 super().__init__(*args, **kwargs)
151class SortedChoiceWidget(forms.widgets.ChoiceWidget):
152 """Wrapper class to sort choices alphabetically."""
154 def optgroups(self, name, value, attrs=None):
155 groups = super().optgroups(name, value, attrs)
156 return sort_unicode(groups, lambda val: str(val[1][0]["label"]))
159class SortedSelect(SortedChoiceWidget, forms.Select):
160 """Wrapper class to sort choices alphabetically."""
163class ColorWidget(forms.RadioSelect):
164 def __init__(self, attrs=None, choices=()) -> None:
165 attrs = {**(attrs or {}), "class": "color_edit"}
166 super().__init__(attrs, choices)
169class SortedSelectMultiple(SortedSelect, forms.SelectMultiple):
170 """Wrapper class to sort choices alphabetically."""
173class ContextDiv(Div):
174 def __init__(self, *fields, **kwargs) -> None:
175 self.context = kwargs.pop("context", {})
176 super().__init__(*fields, **kwargs)
178 def render(self, form, context, template_pack=TEMPLATE_PACK, **kwargs):
179 template = self.get_template_name(template_pack)
180 return render_to_string(template, self.context)
183class SearchField(Field):
184 def __init__(self, *args, **kwargs) -> None:
185 kwargs["template"] = "snippets/query-field.html"
186 super().__init__(*args, **kwargs)
188 def render(self, form, context, template_pack=TEMPLATE_PACK, **kwargs):
189 extra_context = {"custom_filter_list": self.get_search_query_choices()}
190 return super().render(form, context, template_pack, extra_context, **kwargs)
192 def get_search_query_choices(self):
193 """Return all filtering choices for query field."""
194 filter_keys = [
195 "nottranslated",
196 "todo",
197 "translated",
198 "fuzzy",
199 "suggestions",
200 "variants",
201 "screenshots",
202 "labels",
203 "context",
204 "nosuggestions",
205 "comments",
206 "allchecks",
207 "approved",
208 "unapproved",
209 ]
210 return [
211 (key, FILTERS.get_filter_name(key), FILTERS.get_filter_query(key))
212 for key in filter_keys
213 ]
216class CachedQueryIterator(ModelChoiceIterator):
217 """
218 Choice iterator for cached querysets.
220 It assumes the queryset is reused and avoids using an iterator or counting queries.
221 """
223 def __iter__(self):
224 if self.field.empty_label is not None:
225 yield ("", self.field.empty_label)
226 for obj in self.queryset:
227 yield self.choice(obj)
229 def __len__(self) -> int:
230 return len(self.queryset) + (1 if self.field.empty_label is not None else 0)
232 def __bool__(self) -> bool:
233 return self.field.empty_label is not None or bool(self.queryset)
236class CachedModelChoiceField(forms.ModelChoiceField):
237 iterator = CachedQueryIterator
239 def _get_queryset(self):
240 return self._queryset
242 def _set_queryset(self, queryset) -> None:
243 self._queryset = queryset
244 self.widget.choices = self.choices
246 queryset = property(_get_queryset, _set_queryset)
249class CachedModelMultipleChoiceField(
250 CachedModelChoiceField, forms.ModelMultipleChoiceField
251):
252 pass
255class WeblateServiceURLField(forms.URLField):
256 default_validators = [WeblateServiceURLValidator()] # noqa: RUF012
259class NormalizedNewlineCharField(forms.CharField):
260 def to_python(self, value):
261 return normalize_newlines(str(super().to_python(value)))
264class WeblateDateInput(forms.DateInput):
265 input_type = "date"