Repository: WeblateOrg/weblate

Issue:
Update of quality checks failed
**Describe the bug**

If several composats have the **same translation key** (with the same failed check so):
* [ok] the translation from the componant1 has auto repercussion on the component2 translation (very good!);
* [ko] but, the failed check linked to the component 2, which no longer applies, **remains present**;

**To Reproduce the bug**

1. Create two components (`testsmall1` and `testsmall2` ) with the same content :
```xml
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
  <file original="file1" source-language="" datatype="plaintext" target-language="en-US">
    <body>
      <trans-unit id="unit1">
        <source xml:space="preserve">Test...</source>
        <target xml:space="preserve">Test</target>
      </trans-unit>
    </body>
  </file>
</xliff>
```
2. In `testsmall1`, correct the translation (addition of '...') to respond to the 'Mismatched full stop' alert :
![test1](https://user-images.githubusercontent.com/25720810/101991567-e1309b80-3cad-11eb-894b-7b50a020a9d5.PNG)

3. After this action, the alert disappears in `testsmall1` : OK

4. [ko] In `testsmall2`, the same character string is corrected auto translated, but the alert 'Mismatched full stop' remains signaled :
![test2](https://user-images.githubusercontent.com/25720810/101991618-4dab9a80-3cae-11eb-97a0-1de4d4d2f7eb.PNG)

**Expected behavior**

Auto-update (make disappear) the checks alerts when it's necessary.

**Server configuration and status**

 * Weblate: 4.3.2
 * Django: 3.1.3
 * siphashc: 2.1
 * Whoosh: 2.7.4
 * translate-toolkit: 3.2.0
 * lxml: 4.6.1
 * Pillow: 8.0.1
 * bleach: 3.2.1
 * python-dateutil: 2.8.1
 * social-auth-core: 3.3.3
 * social-auth-app-django: 4.0.0
 * django-crispy-forms: 1.9.2
 * oauthlib: 3.1.0
 * django-compressor: 2.4
 * djangorestframework: 3.12.1
 * django-filter: 2.4.0
 * django-appconf: 1.0.4
 * user-agents: 2.2.0
 * filelock: 3.0.12
 * setuptools: 40.8.0
 * jellyfish: 0.8.2
 * openpyxl: 3.0.5
 * celery: 4.4.7
 * kombu: 4.6.11
 * translation-finder: 2.5
 * weblate-language-data: 2020.11
 * html2text: 2020.1.16
 * pycairo: 1.16.2
 * pygobject: 3.30.4
 * diff-match-patch: 20200713
 * requests: 2.24.0
 * django-redis: 4.12.1
 * hiredis: 1.1.0
 * sentry_sdk: 0.19.2
 * Cython: 0.29.21
 * misaka: 2.1.1
 * GitPython: 3.1.11
 * borgbackup: 1.1.14
 * pyparsing: 2.4.7
 * Python: 3.7.3
 * Git: 2.20.1
 * psycopg2: 2.8.6
 * psycopg2-binary: 2.8.6
 * phply: 1.2.5
 * chardet: 3.0.4
 * ruamel.yaml: 0.16.12
 * tesserocr: 2.5.1
 * akismet: 1.1
 * boto3: 1.16.12
 * zeep: 4.0.0
 * aeidon: 1.7.0
 * iniparse: 0.5
 * mysqlclient: 2.0.1
 * Mercurial: 5.6
 * git-svn: 2.20.1
 * git-review: 1.28.0
 * Redis server: 6.0.9
 * PostgreSQL server: 13.1
 * Database backends: django.db.backends.postgresql
 * Cache backends: default:RedisCache, avatar:FileBasedCache
 * Email setup: django.core.mail.backends.smtp.EmailBackend: relay-01.kelis.fr
 * OS encoding: filesystem=utf-8, default=utf-8
 * Celery: redis://cache:6379/1, redis://cache:6379/1, regular
 * Platform: Linux 5.4.0-54-generic (x86_64)

Thank you for your answer



Discussion:

Resolving patch:
diff --git a/weblate/trans/models/unit.py b/weblate/trans/models/unit.py
--- a/weblate/trans/models/unit.py
+++ b/weblate/trans/models/unit.py
@@ -757,15 +757,13 @@ def save_backend(
         if propagate:
             was_propagated = self.propagate(user, change_action, author=author)
 
+        changed = (
+            self.old_unit.state == self.state and self.old_unit.target == self.target
+        )
         # Return if there was no change
         # We have to explicitly check for fuzzy flag change on monolingual
         # files, where we handle it ourselves without storing to backend
-        # The propagation avoids short cicruit here as we need to run the checks.
-        if (
-            self.old_unit.state == self.state
-            and self.old_unit.target == self.target
-            and not was_propagated
-        ):
+        if changed and not was_propagated:
             return False
 
         update_fields = ["target", "state", "original_state", "pending"]
@@ -787,7 +785,7 @@ def save_backend(
         self.save(
             update_fields=update_fields,
             run_checks=run_checks,
-            propagate_checks=propagate,
+            propagate_checks=was_propagated or changed,
         )
 
         # Generate Change object for this change
@@ -982,17 +980,6 @@ def run_checks(self, propagate: Optional[bool] = None):
 
         if create:
             Check.objects.bulk_create(create, batch_size=500, ignore_conflicts=True)
-            # Propagate checks which need it (for example consistency)
-            if needs_propagate and propagate is not False:
-                for unit in self.same_source_units:
-                    try:
-                        unit.run_checks(False)
-                    except Unit.DoesNotExist:
-                        # This can happen in some corner cases like changing
-                        # source language of a project - the source language is
-                        # changed first and then components are updated. But
-                        # not all are yet updated and this spans across them.
-                        continue
             # Trigger source checks on target check update (multiple failing checks)
             if not self.is_source:
                 if self.is_batch_update:
@@ -1001,6 +988,17 @@ def run_checks(self, propagate: Optional[bool] = None):
                     ] = self.source_unit
                 else:
                     self.source_unit.run_checks()
+        # Propagate checks which need it (for example consistency)
+        if (needs_propagate and propagate is not False) or propagate is True:
+            for unit in self.same_source_units:
+                try:
+                    unit.run_checks(False)
+                except Unit.DoesNotExist:
+                    # This can happen in some corner cases like changing
+                    # source language of a project - the source language is
+                    # changed first and then components are updated. But
+                    # not all are yet updated and this spans across them.
+                    continue
 
         # Delete no longer failing checks
         if old_checks:


Test patch:
diff --git a/weblate/trans/tests/test_edit.py b/weblate/trans/tests/test_edit.py
--- a/weblate/trans/tests/test_edit.py
+++ b/weblate/trans/tests/test_edit.py
@@ -572,6 +572,28 @@ def test_merge_inconsistent(self):
         unit = self.get_unit()
         self.assertEqual(unit.all_checks_names, set())
 
+    def test_edit_propagated(self):
+        units = Unit.objects.filter(
+            translation__language__code="cs", source="Thank you for using Weblate."
+        )
+        self.create_link_existing()
+        self.assertEqual(set(units.values_list("target", flat=True)), {""})
+        self.edit_unit("Thank you for using Weblate.", "Díky za použití Weblate")
+        self.assertEqual(
+            set(units.values_list("target", flat=True)), {"Díky za použití Weblate"}
+        )
+        self.assertEqual(
+            [unit.all_checks_names for unit in units.iterator()],
+            [{"end_stop"}, {"end_stop"}],
+        )
+        self.edit_unit("Thank you for using Weblate.", "Díky za použití Weblate.")
+        self.assertEqual(
+            set(units.values_list("target", flat=True)), {"Díky za použití Weblate."}
+        )
+        self.assertEqual(
+            [unit.all_checks_names for unit in units.iterator()], [set(), set()]
+        )
+
     def test_revert(self):
         source = "Hello, world!\n"
         target = "Nazdar svete!\n"