Skip to content

Commit 4f87656

Browse files
authored
feat(screenshot): add change events for screenshot unit assignment and removal (#17067)
Implement proper change event tracking for screenshot-unit relationships to enable autotranslate functionality with screenshot filters. Add SCREENSHOT_REMOVED action event for tracking unit removal.
1 parent 96b3de3 commit 4f87656

File tree

10 files changed

+219
-12
lines changed

10 files changed

+219
-12
lines changed

docs/specs/openapi.yaml

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

weblate/addons/autotranslate.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def change_event(self, change: Change, activity_log_id: int | None = None) -> No
105105
elif change.action in {
106106
ActionEvents.SCREENSHOT_UPLOADED,
107107
ActionEvents.SCREENSHOT_ADDED,
108+
ActionEvents.SCREENSHOT_REMOVED,
108109
}:
109110
if change.screenshot is not None:
110111
units.extend(change.screenshot.units.all())

weblate/api/tests.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5109,6 +5109,13 @@ def test_create(self) -> None:
51095109
},
51105110
)
51115111
self.assertEqual(Screenshot.objects.count(), 2)
5112+
self.assertEqual(
5113+
Change.objects.filter(
5114+
action=ActionEvents.SCREENSHOT_UPLOADED,
5115+
screenshot__name="Test create screenshot",
5116+
).count(),
5117+
1,
5118+
)
51125119

51135120
def test_patch_screenshot(self) -> None:
51145121
self.do_request(
@@ -5185,36 +5192,52 @@ def test_units_invalid(self) -> None:
51855192

51865193
def test_units(self) -> None:
51875194
self.authenticate(True)
5195+
screenshot = Screenshot.objects.get()
51885196
unit = self.component.source_translation.unit_set.all()[0]
51895197
response = self.client.post(
5190-
reverse("api:screenshot-units", kwargs={"pk": Screenshot.objects.get().pk}),
5198+
reverse("api:screenshot-units", kwargs={"pk": screenshot.pk}),
51915199
{"unit_id": unit.pk},
51925200
)
51935201
self.assertEqual(response.status_code, 200)
51945202
self.assertIn(str(unit.pk), response.data["units"][0])
5203+
added_changes = Change.objects.filter(
5204+
action=ActionEvents.SCREENSHOT_ADDED,
5205+
screenshot=screenshot,
5206+
unit=unit,
5207+
)
5208+
self.assertEqual(added_changes.count(), 1)
5209+
self.assertEqual(added_changes[0].user, self.user)
51955210

51965211
def test_units_delete(self) -> None:
51975212
self.authenticate(True)
5213+
screenshot = Screenshot.objects.get()
51985214
unit = self.component.source_translation.unit_set.all()[0]
51995215
self.client.post(
5200-
reverse("api:screenshot-units", kwargs={"pk": Screenshot.objects.get().pk}),
5216+
reverse("api:screenshot-units", kwargs={"pk": screenshot.pk}),
52015217
{"unit_id": unit.pk},
52025218
)
52035219
response = self.client.delete(
52045220
reverse(
52055221
"api:screenshot-delete-units",
5206-
kwargs={"pk": Screenshot.objects.get().pk, "unit_id": 100000},
5222+
kwargs={"pk": screenshot.pk, "unit_id": 100000},
52075223
),
52085224
)
52095225
self.assertEqual(response.status_code, 404)
52105226
response = self.client.delete(
52115227
reverse(
52125228
"api:screenshot-delete-units",
5213-
kwargs={"pk": Screenshot.objects.get().pk, "unit_id": unit.pk},
5229+
kwargs={"pk": screenshot.pk, "unit_id": unit.pk},
52145230
),
52155231
)
52165232
self.assertEqual(response.status_code, 204)
5217-
self.assertEqual(Screenshot.objects.get().units.all().count(), 0)
5233+
self.assertEqual(screenshot.units.all().count(), 0)
5234+
removed_changes = Change.objects.filter(
5235+
action=ActionEvents.SCREENSHOT_REMOVED,
5236+
screenshot=screenshot,
5237+
unit=unit,
5238+
)
5239+
self.assertEqual(removed_changes.count(), 1)
5240+
self.assertEqual(removed_changes[0].user, self.user)
52185241

52195242

52205243
class ChangeAPITest(APIBaseTest):

weblate/api/views.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2310,7 +2310,7 @@ def units(self, request: Request, **kwargs):
23102310
except (Unit.DoesNotExist, ValueError) as error:
23112311
raise ValidationError({"unit_id": str(error)}) from error
23122312

2313-
obj.units.add(unit)
2313+
obj.add_unit(unit, user=request.user)
23142314
serializer = ScreenshotSerializer(obj, context={"request": request})
23152315

23162316
return Response(serializer.data, status=HTTP_200_OK)
@@ -2329,7 +2329,7 @@ def delete_units(self, request: Request, pk, unit_id):
23292329
unit = obj.translation.unit_set.get(pk=unit_id)
23302330
except Unit.DoesNotExist as error:
23312331
raise Http404(str(error)) from error
2332-
obj.units.remove(unit)
2332+
obj.remove_unit(unit, user=request.user)
23332333
return Response(status=HTTP_204_NO_CONTENT)
23342334

23352335
def create(self, request: Request, *args, **kwargs):
@@ -2359,11 +2359,21 @@ def create(self, request: Request, *args, **kwargs):
23592359
)
23602360
serializer.is_valid(raise_exception=True)
23612361
instance = serializer.save(translation=translation, user=request.user)
2362+
23622363
instance.change_set.create(
2363-
action=ActionEvents.SCREENSHOT_ADDED,
2364+
action=ActionEvents.SCREENSHOT_UPLOADED,
23642365
user=request.user,
23652366
target=instance.name,
23662367
)
2368+
2369+
for unit in instance.units.all():
2370+
instance.change_set.create(
2371+
action=ActionEvents.SCREENSHOT_ADDED,
2372+
user=request.user,
2373+
target=instance.name,
2374+
unit=unit,
2375+
)
2376+
23672377
return Response(serializer.data, status=HTTP_201_CREATED)
23682378

23692379
def update(self, request: Request, *args, **kwargs):

weblate/screenshots/models.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from weblate.auth.models import get_anonymous
2323
from weblate.checks.flags import Flags
2424
from weblate.screenshots.fields import ScreenshotField
25+
from weblate.trans.actions import ActionEvents
2526
from weblate.trans.mixins import UserDisplayMixin
2627
from weblate.trans.models import Translation, Unit
2728
from weblate.trans.models.alert import update_alerts
@@ -99,6 +100,26 @@ def get_absolute_url(self) -> str:
99100
def filter_name(self) -> str:
100101
return f"screenshot:{Flags.format_value(self.name)}"
101102

103+
def add_unit(self, unit: Unit, user: User | None = None) -> None:
104+
"""Add a unit to this screenshot and create a change event."""
105+
self.units.add(unit)
106+
self.change_set.create(
107+
action=ActionEvents.SCREENSHOT_ADDED,
108+
user=user or self.user,
109+
target=self.name,
110+
unit=unit,
111+
)
112+
113+
def remove_unit(self, unit: Unit, user: User | None = None) -> None:
114+
"""Remove a unit from this screenshot and create a change event."""
115+
self.units.remove(unit)
116+
self.change_set.create(
117+
action=ActionEvents.SCREENSHOT_REMOVED,
118+
user=user or self.user,
119+
target=self.name,
120+
unit=unit,
121+
)
122+
102123

103124
@receiver(m2m_changed, sender=Screenshot.units.through)
104125
@disable_for_loaddata

weblate/screenshots/tests.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from weblate.lang.models import Language
2222
from weblate.screenshots.models import Screenshot
2323
from weblate.screenshots.views import get_tesseract, ocr_get_strings
24+
from weblate.trans.actions import ActionEvents
25+
from weblate.trans.models import Change
2426
from weblate.trans.tests.test_models import RepoTestCase
2527
from weblate.trans.tests.test_views import FixtureTestCase
2628
from weblate.trans.tests.utils import create_test_user, get_test_file
@@ -57,6 +59,12 @@ def test_upload(self) -> None:
5759
response = self.do_upload()
5860
self.assertContains(response, "Obrazek")
5961
self.assertEqual(Screenshot.objects.count(), 1)
62+
uploaded_changes = Change.objects.filter(
63+
action=ActionEvents.SCREENSHOT_UPLOADED,
64+
screenshot=Screenshot.objects.get(),
65+
)
66+
self.assertEqual(uploaded_changes.count(), 1)
67+
self.assertEqual(uploaded_changes[0].user, self.user)
6068

6169
def test_upload_fail(self) -> None:
6270
self.make_manager()
@@ -74,6 +82,19 @@ def test_upload_source(self) -> None:
7482
screenshot = Screenshot.objects.all()[0]
7583
self.assertEqual(screenshot.name, "Obrazek")
7684
self.assertEqual(screenshot.units.count(), 1)
85+
uploaded_changes = Change.objects.filter(
86+
action=ActionEvents.SCREENSHOT_UPLOADED,
87+
screenshot=screenshot,
88+
)
89+
self.assertEqual(uploaded_changes.count(), 1)
90+
self.assertEqual(uploaded_changes[0].user, self.user)
91+
added_changes = Change.objects.filter(
92+
action=ActionEvents.SCREENSHOT_ADDED,
93+
screenshot=screenshot,
94+
unit=source,
95+
)
96+
self.assertEqual(added_changes.count(), 1)
97+
self.assertEqual(added_changes[0].user, self.user)
7798

7899
def test_upload_source_invalid(self) -> None:
79100
self.make_manager()
@@ -133,6 +154,13 @@ def test_source_manipulations(self) -> None:
133154
self.assertEqual(data["responseCode"], 200)
134155
self.assertEqual(data["status"], True)
135156
self.assertEqual(screenshot.units.count(), 1)
157+
added_changes = Change.objects.filter(
158+
action=ActionEvents.SCREENSHOT_ADDED,
159+
screenshot=screenshot,
160+
unit_id=source_pk,
161+
)
162+
self.assertEqual(added_changes.count(), 1)
163+
self.assertEqual(added_changes[0].user, self.user)
136164

137165
# Updated listing
138166
response = self.client.get(
@@ -146,6 +174,13 @@ def test_source_manipulations(self) -> None:
146174
{"source": source_pk},
147175
)
148176
self.assertEqual(screenshot.units.count(), 0)
177+
removed_changes = Change.objects.filter(
178+
action=ActionEvents.SCREENSHOT_REMOVED,
179+
screenshot=screenshot,
180+
unit_id=source_pk,
181+
)
182+
self.assertEqual(removed_changes.count(), 1)
183+
self.assertEqual(removed_changes[0].user, self.user)
149184

150185
def test_ocr_backend(self) -> None:
151186
# Extract strings

weblate/screenshots/views.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ def try_add_source(request: AuthenticatedHttpRequest, obj) -> bool:
193193
except (Unit.DoesNotExist, ValueError):
194194
return False
195195

196-
obj.units.add(source)
196+
obj.add_unit(source, user=request.user)
197197
return True
198198

199199

@@ -231,7 +231,7 @@ def post(self, request: AuthenticatedHttpRequest, **kwargs):
231231
)
232232
request.user.profile.increase_count("uploaded")
233233
obj.change_set.create(
234-
action=ActionEvents.SCREENSHOT_ADDED,
234+
action=ActionEvents.SCREENSHOT_UPLOADED,
235235
user=request.user,
236236
target=obj.name,
237237
)
@@ -322,7 +322,13 @@ def get_screenshot(request: AuthenticatedHttpRequest, pk):
322322
def remove_source(request: AuthenticatedHttpRequest, pk):
323323
obj = get_screenshot(request, pk)
324324

325-
obj.units.remove(request.POST["source"])
325+
try:
326+
unit = obj.translation.unit_set.get(pk=int(request.POST["source"]))
327+
except (Unit.DoesNotExist, ValueError):
328+
messages.error(request, gettext("Invalid unit."))
329+
return redirect(obj)
330+
331+
obj.remove_unit(unit, user=request.user)
326332

327333
messages.success(request, gettext("Source has been removed."))
328334

weblate/trans/actions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ class ActionEvents(IntegerChoices):
172172
FORCE_SYNC = 83, gettext_lazy("Forced synchronization of translations")
173173
# Translators: Name of event in the history
174174
FORCE_SCAN = 84, gettext_lazy("Forced rescan of translations")
175+
# Translators: Name of event in the history
176+
SCREENSHOT_REMOVED = 85, gettext_lazy("Screenshot removed")
175177

176178

177179
# Actions which are logged

0 commit comments

Comments
 (0)