Coverage for app/venv/lib/python3.14/site-packages/weblate/screenshots/forms.py: 37%
90 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
5import io
6from typing import Any
7from urllib.parse import urlparse
9import requests
10from django import forms
11from django.conf import settings
12from django.core.files.uploadedfile import InMemoryUploadedFile
13from django.http.request import validate_host
14from django.template.loader import render_to_string
15from django.utils.html import format_html
16from django.utils.translation import gettext, gettext_lazy
18from weblate.screenshots.models import Screenshot
19from weblate.trans.forms import QueryField
20from weblate.utils.forms import SortedSelect
21from weblate.utils.requests import request
22from weblate.utils.validators import ALLOWED_IMAGES, WeblateURLValidator
25class ScreenshotImageValidationMixin:
26 def clean_images(
27 self, cleaned_data: dict[str, Any], edit: bool = False
28 ) -> dict[str, Any]:
29 image = cleaned_data.get("image")
30 image_url = cleaned_data.get("image_url")
31 if not image and not image_url:
32 raise forms.ValidationError(
33 gettext("You need to provide either image file or image URL.")
34 )
36 if (edit and ("image" not in self.changed_data) and image_url) or (
37 not edit and image_url and not image
38 ):
39 # download from image_url only if image is not provided and not updated
40 cleaned_data["image"] = self.download_image(image_url)
41 image_field = Screenshot._meta.get_field("image") # noqa: SLF001
42 image_field.run_validators(cleaned_data["image"])
44 cleaned_data.pop("image_url", None)
45 return cleaned_data
47 def download_image(self, url: str) -> InMemoryUploadedFile:
48 """Download image from the provided URL."""
49 if not validate_host(
50 urlparse(url).hostname or "", settings.ALLOWED_ASSET_DOMAINS
51 ):
52 raise forms.ValidationError(
53 {"image_url": gettext("Image URL domain is not allowed.")}
54 )
55 try:
56 with request("get", url, stream=True) as response:
57 if response.status_code != 200:
58 raise forms.ValidationError(
59 {
60 "image_url": gettext(
61 "Unable to download image from the provided URL (HTTP status code: %(code)s)."
62 )
63 % {"code": response.status_code}
64 }
65 )
66 content = b""
67 for chunk in response.iter_content(
68 chunk_size=settings.ALLOWED_ASSET_SIZE + 1
69 ):
70 if not content:
71 content = chunk
72 else:
73 # This can be slow, but it typically won't happen
74 content += chunk
75 if len(content) > settings.ALLOWED_ASSET_SIZE:
76 break
77 if len(content) > settings.ALLOWED_ASSET_SIZE:
78 raise forms.ValidationError(
79 {"image_url": gettext("Image is too big.")}
80 )
81 content_type = response.headers.get("Content-Type")
82 if not content_type or content_type not in ALLOWED_IMAGES:
83 raise forms.ValidationError(
84 {
85 "image_url": gettext("Unsupported image type: %s")
86 % content_type
87 }
88 )
89 except requests.RequestException as e:
90 raise forms.ValidationError(
91 {
92 "image_url": gettext(
93 "Unable to download image from the provided URL."
94 )
95 }
96 ) from e
97 filename = url.rsplit("/", maxsplit=1)[-1] or "screenshot"
98 return InMemoryUploadedFile(
99 file=io.BytesIO(content),
100 field_name="image",
101 name=filename,
102 content_type=content_type,
103 size=len(content),
104 charset=None,
105 )
108class ScreenshotInput(forms.FileInput):
109 def render(self, name, value, attrs=None, renderer=None, **kwargs):
110 rendered_input = super().render(name, value, attrs, renderer, **kwargs)
111 paste_button = render_to_string("screenshots/snippets/paste-button.html")
112 return format_html("{}{}", rendered_input, paste_button)
115class ScreenshotEditForm(forms.ModelForm, ScreenshotImageValidationMixin):
116 """Screenshot editing."""
118 image_url = forms.URLField(
119 required=False,
120 label=gettext_lazy("Image URL"),
121 help_text=gettext_lazy("The image will be downloaded and stored."),
122 validators=[WeblateURLValidator()],
123 )
125 class Meta:
126 model = Screenshot
127 fields = ("name", "image", "image_url", "repository_filename")
128 widgets = { # noqa: RUF012
129 "image": ScreenshotInput,
130 }
132 def clean(self):
133 cleaned_data = super().clean()
134 return self.clean_images(cleaned_data or {}, edit=True)
137class LanguageChoiceField(forms.ModelChoiceField):
138 def label_from_instance(self, obj):
139 return obj.language
142class ScreenshotForm(forms.ModelForm, ScreenshotImageValidationMixin):
143 """Screenshot upload."""
145 image_url = forms.URLField(
146 required=False,
147 label=gettext_lazy("Image URL"),
148 help_text=gettext_lazy("The image will be downloaded and stored."),
149 validators=[WeblateURLValidator()],
150 )
152 class Meta:
153 model = Screenshot
154 fields = ("name", "repository_filename", "image", "image_url", "translation")
155 widgets = { # noqa: RUF012
156 "translation": SortedSelect,
157 "image": ScreenshotInput,
158 }
159 field_classes = { # noqa: RUF012
160 "translation": LanguageChoiceField,
161 }
163 def __init__(
164 self, component, data=None, files=None, instance=None, initial=None
165 ) -> None:
166 self.component = component
167 super().__init__(data=data, files=files, instance=instance, initial=initial)
169 translations = component.translation_set.prefetch_related("language")
170 if initial and "translation" in initial:
171 translations = translations.filter(
172 pk__in=(initial["translation"].pk, component.source_translation.pk)
173 )
174 self.fields["translation"].queryset = translations
175 # This is overridden from initial arg of the form
176 self.fields["translation"].initial = component.source_translation
177 self.fields["translation"].empty_label = None
178 self.fields["image"].required = False
180 def clean(self):
181 cleaned_data = super().clean()
182 return self.clean_images(cleaned_data or {})
185class SearchForm(forms.Form):
186 q = QueryField(required=False)