Repository: mozilla/pontoon

Issue:
Refine pretranslation logic for strings with rejected suggestions
Currently, a string with suggestions is ignored by pretranslation.

We should:
* Pretranslate if all suggestions are submitted by users and rejected. 
* Not pretranslate if a pretranslation was already rejected.


Discussion:

Resolving patch:
diff --git a/pontoon/pretranslation/__init__.py b/pontoon/pretranslation/__init__.py
--- a/pontoon/pretranslation/__init__.py
+++ b/pontoon/pretranslation/__init__.py
@@ -0,0 +1,4 @@
+AUTHORS = {
+    "tm": "pontoon-tm@example.com",
+    "gt": "pontoon-gt@example.com",
+}
diff --git a/pontoon/pretranslation/pretranslate.py b/pontoon/pretranslation/pretranslate.py
--- a/pontoon/pretranslation/pretranslate.py
+++ b/pontoon/pretranslation/pretranslate.py
@@ -14,6 +14,7 @@
     get_google_translate_data,
     get_translation_memory_data,
 )
+from pontoon.pretranslation import AUTHORS
 
 
 log = logging.getLogger(__name__)
@@ -66,10 +67,7 @@ def get_pretranslations(entity, locale):
         - a user (representing TM or GT service)
     """
     source = entity.string
-    services = {
-        "tm": User.objects.get(email="pontoon-tm@example.com"),
-        "gt": User.objects.get(email="pontoon-gt@example.com"),
-    }
+    services = {k: User.objects.get(email=email) for k, email in AUTHORS.items()}
 
     if entity.resource.format == "ftl":
         source_ast = parser.parse_entry(source)
diff --git a/pontoon/pretranslation/tasks.py b/pontoon/pretranslation/tasks.py
--- a/pontoon/pretranslation/tasks.py
+++ b/pontoon/pretranslation/tasks.py
@@ -8,8 +8,10 @@
     Entity,
     TranslatedResource,
     Translation,
+    User,
 )
 from pontoon.actionlog.models import ActionLog
+from pontoon.pretranslation import AUTHORS
 from pontoon.pretranslation.pretranslate import (
     get_pretranslations,
     update_changed_instances,
@@ -68,7 +70,7 @@ def pretranslate(self, project_pk, locales=None, entities=None):
 
     entities = entities.prefetch_related("resource")
 
-    # get available TranslatedResource pairs
+    # Fetch all available locale-resource pairs (TranslatedResource objects)
     tr_pairs = (
         TranslatedResource.objects.filter(
             resource__project=project,
@@ -83,12 +85,14 @@ def pretranslate(self, project_pk, locales=None, entities=None):
         .distinct()
     )
 
-    # Fetch all distinct locale-entity pairs for which translation exists
+    # Fetch all locale-entity pairs with non-rejected or pretranslated translations
+    pt_authors = [User.objects.get(email=email) for email in AUTHORS.values()]
     translated_entities = (
         Translation.objects.filter(
             locale__in=locales,
             entity__in=entities,
         )
+        .filter(Q(rejected=False) | Q(user__in=pt_authors))
         .annotate(
             locale_entity=Concat(
                 "locale_id", V("-"), "entity_id", output_field=CharField()


Test patch:
diff --git a/pontoon/pretranslation/tests/test_tasks.py b/pontoon/pretranslation/tests/test_tasks.py
--- a/pontoon/pretranslation/tests/test_tasks.py
+++ b/pontoon/pretranslation/tests/test_tasks.py
@@ -8,6 +8,7 @@
     EntityFactory,
     ResourceFactory,
     TranslatedResourceFactory,
+    TranslationFactory,
     ProjectLocaleFactory,
 )
 
@@ -61,3 +62,51 @@ def test_pretranslate(gt_mock, project_a, locale_a, resource_a, locale_b):
 
     # latest_translation belongs to pretranslations.
     assert project_a.latest_translation in translations
+
+
+@patch("pontoon.pretranslation.tasks.get_pretranslations")
+@pytest.mark.django_db
+def test_which_strings_to_pretranslate(gt_mock, project_a, locale_a, resource_a):
+    """
+    Verify that we only pretranslate:
+    - strings without any translations
+    - strings with only rejected translations, submitted by humans
+    """
+    project_a.pretranslation_enabled = True
+    project_a.save()
+
+    ProjectLocaleFactory.create(
+        project=project_a,
+        locale=locale_a,
+        pretranslation_enabled=True,
+    )
+
+    resource = ResourceFactory.create(
+        project=project_a, path="resource.po", format="po"
+    )
+    TranslatedResourceFactory.create(resource=resource, locale=locale_a)
+
+    no_translations = EntityFactory.create(resource=resource)
+    non_rejected = EntityFactory.create(resource=resource)
+    rejected_by_human = EntityFactory.create(resource=resource)
+    rejected_by_machine = EntityFactory.create(resource=resource)
+
+    gt_user = User.objects.get(email="pontoon-gt@example.com")
+    gt_mock.return_value = [("pretranslation", None, gt_user)]
+
+    TranslationFactory.create(entity=non_rejected, locale=locale_a, rejected=False)
+    TranslationFactory.create(entity=rejected_by_human, locale=locale_a, rejected=True)
+    TranslationFactory.create(
+        entity=rejected_by_machine,
+        locale=locale_a,
+        rejected=True,
+        user=gt_user,
+    )
+
+    pretranslate(project_a.pk)
+    project_a.refresh_from_db()
+
+    assert len(no_translations.translation_set.filter(string="pretranslation")) == 1
+    assert len(non_rejected.translation_set.filter(string="pretranslation")) == 0
+    assert len(rejected_by_human.translation_set.filter(string="pretranslation")) == 1
+    assert len(rejected_by_machine.translation_set.filter(string="pretranslation")) == 0