Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 18 additions & 16 deletions pontoon/batch/actions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from django.db.models import QuerySet
from django.utils import timezone

from pontoon.actionlog.models import ActionLog
from pontoon.base.badge_utils import badges_review_level, badges_translation_level
from pontoon.base.models import (
Entity,
Locale,
Translation,
TranslationMemoryEntry,
)
from pontoon.base.models.translation import TranslationQuerySet
from pontoon.batch import utils
from pontoon.messaging.notifications import send_badge_notification
from pontoon.translations.utils import parse_db_string_to_json
Expand Down Expand Up @@ -45,7 +48,7 @@ def batch_action_template(form, user, translations, locale):
}


def approve_translations(form, user, translations, locale):
def approve_translations(user, locale: Locale, translations: TranslationQuerySet):
"""Approve a series of translations.

For documentation, refer to the `batch_action_template` function.
Expand Down Expand Up @@ -117,7 +120,7 @@ def approve_translations(form, user, translations, locale):
}


def reject_translations(form, user, translations, locale):
def reject_translations(user, locale: Locale, entities: QuerySet[Entity]):
"""Reject a series of translations.

Note that this function doesn't use the `translations` parameter, as it
Expand All @@ -129,7 +132,7 @@ def reject_translations(form, user, translations, locale):
"""
suggestions = Translation.objects.filter(
locale=locale,
entity__pk__in=form.cleaned_data["entities"],
entity__in=entities,
approved=False,
rejected=False,
)
Expand All @@ -148,7 +151,7 @@ def reject_translations(form, user, translations, locale):
performed_by=user,
translation=t,
)
for t in translations
for t in suggestions
]
ActionLog.objects.bulk_create(actions_to_log)

Expand Down Expand Up @@ -184,7 +187,9 @@ def reject_translations(form, user, translations, locale):
}


def replace_translations(form, user, translations, locale):
def replace_translations(
user, locale: Locale, translations: TranslationQuerySet, find: str, replace: str
):
"""Replace characters in a series of translations.

Replaces all occurences of the content of the `find` parameter with the
Expand All @@ -193,9 +198,6 @@ def replace_translations(form, user, translations, locale):
For documentation, refer to the `batch_action_template` function.

"""
find = form.cleaned_data["find"]
replace = form.cleaned_data["replace"]
latest_translation_pk = None

(
old_translations,
Expand Down Expand Up @@ -265,8 +267,9 @@ def replace_translations(form, user, translations, locale):

changed_translation_pks = [c.pk for c in changed_translations]

if changed_translation_pks:
latest_translation_pk = max(changed_translation_pks)
latest_translation_pk = (
max(changed_translation_pks) if changed_translation_pks else None
)

return {
"count": count,
Expand All @@ -279,19 +282,19 @@ def replace_translations(form, user, translations, locale):
}


def copy_translation_from_locale(form, user, translations, locale):
def copy_translation_from_locale(
user, locale: Locale, entities: QuerySet[Entity], other_locale: str
):
"""
Copy translations approved in a source locale and add them as suggestions
in the target locale. Existing active translations in the target locale
are deactivated before the new suggestions are created.
"""

other_locale = form.cleaned_data["other_locale"]

other_locale_translations = list(
Translation.objects.filter(
locale__code=other_locale,
entity__pk__in=form.cleaned_data["entities"],
entity__in=entities,
approved=True,
)
)
Expand All @@ -301,7 +304,7 @@ def copy_translation_from_locale(form, user, translations, locale):
already_active_entity_pks = set(
Translation.objects.filter(
locale=locale,
entity__pk__in=form.cleaned_data["entities"],
entity__in=entities,
active=True,
).values_list("entity__pk", flat=True)
)
Expand All @@ -311,7 +314,6 @@ def copy_translation_from_locale(form, user, translations, locale):

if not other_locale:
# Copy from other locale (entity.string directly)
entities = Entity.objects.filter(pk__in=form.cleaned_data["entities"])
for entity in entities:
value, properties = parse_db_string_to_json(
entity.resource.format, entity.string
Expand Down
10 changes: 8 additions & 2 deletions pontoon/batch/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
from django import forms

from pontoon.base import utils
from pontoon.batch.actions import ACTIONS_FN_MAP


class BatchActionsForm(forms.Form):
"""Handles the arguments passed to the batch actions view."""

locale = forms.CharField()
action = forms.ChoiceField(choices=[(x, x) for x in ACTIONS_FN_MAP.keys()])
action = forms.ChoiceField(
choices=[
("approve", "approve"),
("reject", "reject"),
("replace", "replace"),
("copy_from_locale", "copy_from_locale"),
]
)
entities = forms.CharField(required=False)
find = forms.CharField(required=False)
replace = forms.CharField(required=False)
Expand Down
19 changes: 2 additions & 17 deletions pontoon/batch/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from unittest.mock import MagicMock

import pytest

from fluent.syntax import FluentParser, FluentSerializer
Expand Down Expand Up @@ -95,14 +93,7 @@ def test_copy_from_another_locale():
entity=entity, locale=source_locale, string="key = value", approved=True
)

# Mock form and call the function
form = MagicMock()
form.cleaned_data = {
"other_locale": "en-GB",
"entities": [entity.pk],
}

copy_translation_from_locale(form, user, Translation.objects.none(), target_locale)
copy_translation_from_locale(user, target_locale, [entity], "en-GB")
result = Translation.objects.filter(locale=target_locale, entity=entity)

# Assert a suggestion was created in the target locale
Expand Down Expand Up @@ -146,13 +137,7 @@ def test_copy_from_another_locale_copies_all_strings():
entity=entity2, locale=source_locale, string="key2 = value2", approved=True
)

form = MagicMock()
form.cleaned_data = {
"other_locale": "en-GB",
"entities": [entity1.pk, entity2.pk],
}

copy_translation_from_locale(form, user, Translation.objects.none(), target_locale)
copy_translation_from_locale(user, target_locale, [entity1, entity2], "en-GB")

# entity1 already has an active translation - new suggestion should be active=False
assert (
Expand Down
50 changes: 36 additions & 14 deletions pontoon/batch/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import logging

from typing import cast

from django.contrib.auth.decorators import login_required
from django.db import transaction
from django.http import JsonResponse
Expand All @@ -11,14 +13,19 @@
Locale,
Project,
TranslatedResource,
Translation,
TranslationMemoryEntry,
)
from pontoon.base.models.translation import Translation, TranslationQuerySet
from pontoon.base.services import readonly_exists
from pontoon.base.user_utils import can_translate
from pontoon.base.utils import require_AJAX
from pontoon.batch import forms
from pontoon.batch.actions import ACTIONS_FN_MAP
from pontoon.batch.actions import (
approve_translations,
copy_translation_from_locale,
reject_translations,
replace_translations,
)


log = logging.getLogger(__name__)
Expand Down Expand Up @@ -78,6 +85,7 @@ def batch_edit_translations(request):
# Also make sure projects are not enabled in read-only mode for a locale.
projects_pk = entities.values_list("resource__project__pk", flat=True)
projects = Project.objects.filter(pk__in=projects_pk.distinct())
user = request.user

for project in projects:
if not can_translate(request.user, project, locale) or readonly_exists(
Expand All @@ -91,20 +99,34 @@ def batch_edit_translations(request):
status=403,
)

active_translations = Translation.objects.filter(
active=True,
locale=locale,
entity__in=entities,
translations = cast(
TranslationQuerySet,
Translation.objects.filter(active=True, locale=locale, entity__in=entities),
)

# Execute the actual action.
action_function = ACTIONS_FN_MAP[form.cleaned_data["action"]]
action_status = action_function(
form,
request.user,
active_translations,
locale,
)
match form.cleaned_data["action"]:
case "approve":
action_status = approve_translations(user, locale, translations)
case "copy_from_locale":
action_status = copy_translation_from_locale(
user, locale, entities, form.cleaned_data["other_locale"]
)
case "reject":
action_status = reject_translations(user, locale, entities)
case "replace":
action_status = replace_translations(
user,
locale,
translations,
form.cleaned_data["find"],
form.cleaned_data["replace"],
)
case _:
return JsonResponse(
{"status": False, "message": "Unsupported action"},
status=400,
)

if action_status.get("error"):
return JsonResponse(action_status)
Expand All @@ -123,7 +145,7 @@ def batch_edit_translations(request):
TranslatedResource.objects.filter(pk__in=tr_pks).calculate_stats()

# Mark translations as changed
active_translations.bulk_mark_changed()
translations.bulk_mark_changed()

# Reset term translations for entities belonging to the Terminology project
changed_entity_pks = [entity.pk for entity in action_status["changed_entities"]]
Expand Down
Loading