Repository: translate/pootle

Issue:
Stats: refresh WORDCOUNT_STATS after accepting of a suggestion for untranslated unit
This is regression. Tests need to be added.
- open untranslated unit;
- add a new suggestion;
- accept the suggestion;

`Need translation` count on browse view will not be updated.



Discussion:
The bug was introduced in https://github.com/translate/pootle/commit/410db0181abc0c3154477bcec96981804b0afe0d.


Resolving patch:
diff --git a/pootle/core/mixins/treeitem.py b/pootle/core/mixins/treeitem.py
--- a/pootle/core/mixins/treeitem.py
+++ b/pootle/core/mixins/treeitem.py
@@ -496,7 +496,7 @@ def update_dirty_cache(self):
         if _dirty:
             self._dirty_cache = set()
             self.register_all_dirty()
-            create_update_cache_job(self, _dirty)
+            create_update_cache_job_wrapper(self, _dirty)
 
     def update_all_cache(self):
         """Add a RQ job which updates all cached stats of current TreeItem
@@ -522,7 +522,7 @@ def _update_cache_job(self, keys, decrement):
 
             if keys_for_parent:
                 for p in self.get_parents():
-                    create_update_cache_job(p, keys_for_parent, decrement)
+                    create_update_cache_job_wrapper(p, keys_for_parent, decrement)
                 self.unregister_dirty(decrement)
             else:
                 self.unregister_all_dirty(decrement)
@@ -538,7 +538,7 @@ def update_parent_cache(self):
         all_cache_methods = set(CachedMethods.get_all())
         for p in self.get_parents():
             p.register_all_dirty()
-            create_update_cache_job(p, all_cache_methods)
+            create_update_cache_job_wrapper(p, all_cache_methods)
 
 
 class JobWrapper():
@@ -644,6 +644,7 @@ def save_enqueued(self, pipe):
         if job.timeout is None:
             job.timeout = self.timeout
         job.save(pipeline=pipe)
+        self.job = job
 
     def save_deferred(self, depends_on, pipe):
         """
@@ -674,8 +675,15 @@ def update_cache_job(instance):
     job_wrapper.clear_job_params()
 
 
-def create_update_cache_job(instance, keys, decrement=1):
+def create_update_cache_job_wrapper(instance, keys, decrement=1):
     queue = get_queue('default')
+    if queue._async:
+        create_update_cache_job(queue, instance, keys, decrement=decrement)
+    else:
+        instance._update_cache_job(keys, decrement=decrement)
+
+
+def create_update_cache_job(queue, instance, keys, decrement=1):
     queue.connection.sadd(queue.redis_queues_keys, queue.key)
     job_wrapper = JobWrapper.create(update_cache_job,
                                     instance=instance,
@@ -741,4 +749,5 @@ def create_update_cache_job(instance, keys, decrement=1):
                 logger.debug('RETRY after WatchError for %s' % last_job_key)
                 continue
     logger.debug('ENQUEUE %s (job_id=%s)' % (last_job_key, job_wrapper.id))
+
     queue.push_job_id(job_wrapper.id)


Test patch:
diff --git a/tests/data/po/tutorial/it/tutorial.po b/tests/data/po/tutorial/it/tutorial.po
new file mode 100644
--- /dev/null
+++ b/tests/data/po/tutorial/it/tutorial.po
@@ -0,0 +1,22 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Pootle Tests\n"
+
+#: fish.c
+msgid "fish"
+msgstr ""
+
+#: test.c
+msgid "test"
+msgstr "rest"
+
+#: fish.c
+msgid "%d fish"
+msgid_plural "%d fishies"
+msgstr[0] ""
+msgstr[1] ""
diff --git a/tests/fixtures/mock.py b/tests/fixtures/mock.py
--- a/tests/fixtures/mock.py
+++ b/tests/fixtures/mock.py
@@ -18,3 +18,10 @@
 # functions, therefore the patching has no effect
 mp = monkeypatch()
 mp.setattr('django.utils.functional.cached_property', property)
+
+
+class FakeJob(object):
+    id = 'FAKE_JOB_ID'
+
+
+mp.setattr('rq.get_current_job', lambda: FakeJob())
diff --git a/tests/fixtures/models/language.py b/tests/fixtures/models/language.py
--- a/tests/fixtures/models/language.py
+++ b/tests/fixtures/models/language.py
@@ -68,6 +68,12 @@ def spanish(english):
     return _require_language('es', 'Spanish')
 
 
+@pytest.fixture
+def italian(english):
+    """Require the Spanish language."""
+    return _require_language('it', 'Italian')
+
+
 @pytest.fixture
 def fish(english):
     """Require the Fish language ><(((º>"""
diff --git a/tests/fixtures/models/store.py b/tests/fixtures/models/store.py
--- a/tests/fixtures/models/store.py
+++ b/tests/fixtures/models/store.py
@@ -70,6 +70,13 @@ def af_tutorial_po(settings, afrikaans_tutorial, system):
     return _require_store(afrikaans_tutorial, po_directory, 'tutorial.po')
 
 
+@pytest.fixture
+def it_tutorial_po(settings, italian_tutorial, system):
+    """Require the /it/tutorial/tutorial.po store."""
+    po_directory = settings.POOTLE_TRANSLATION_DIRECTORY
+    return _require_store(italian_tutorial, po_directory, 'tutorial.po')
+
+
 @pytest.fixture
 def af_tutorial_subdir_po(settings, afrikaans_tutorial, system):
     """Require the /af/tutorial/subdir/tutorial.po store."""
diff --git a/tests/fixtures/models/translation_project.py b/tests/fixtures/models/translation_project.py
--- a/tests/fixtures/models/translation_project.py
+++ b/tests/fixtures/models/translation_project.py
@@ -47,5 +47,11 @@ def french_tutorial(french, tutorial):
 
 @pytest.fixture
 def spanish_tutorial(spanish, tutorial):
-    """Require French Tutorial."""
+    """Require Spanish Tutorial."""
     return _require_tp(spanish, tutorial)
+
+
+@pytest.fixture
+def italian_tutorial(italian, tutorial):
+    """Require Italian Tutorial."""
+    return _require_tp(italian, tutorial)
diff --git a/tests/models/unit.py b/tests/models/unit.py
--- a/tests/models/unit.py
+++ b/tests/models/unit.py
@@ -14,6 +14,7 @@
 from django.contrib.auth import get_user_model
 from django.utils import timezone
 
+from pootle.core.mixins.treeitem import CachedMethods
 from pootle_store.util import UNTRANSLATED, FUZZY, TRANSLATED
 
 
@@ -240,3 +241,28 @@ def test_accept_suggestion_changes_state(issue_2401_po, system):
 
     unit.accept_suggestion(suggestion, tp, system)
     assert unit.state == TRANSLATED
+
+@pytest.mark.django_db
+def test_accept_suggestion_update_wordcount(it_tutorial_po, system):
+    """Tests that accepting a suggestion for an untranslated unit will
+    change the wordcount stats of the unit's store.
+    """
+
+    # Parse store
+    it_tutorial_po.update(overwrite=False, only_newer=False)
+
+    untranslated_unit = it_tutorial_po.getitem(0)
+    suggestion_text = 'foo bar baz'
+
+    sugg, added = untranslated_unit.add_suggestion(suggestion_text)
+    assert sugg is not None
+    assert added
+    assert len(untranslated_unit.get_suggestions()) == 1
+    assert it_tutorial_po.get_cached(CachedMethods.SUGGESTIONS) == 1
+    assert it_tutorial_po.get_cached(CachedMethods.WORDCOUNT_STATS)['translated'] == 1
+    assert untranslated_unit.state == UNTRANSLATED
+    untranslated_unit.accept_suggestion(sugg,
+                                        it_tutorial_po.translation_project,
+                                        system)
+    assert untranslated_unit.state == TRANSLATED
+    assert it_tutorial_po.get_cached(CachedMethods.WORDCOUNT_STATS)['translated'] == 2
diff --git a/tests/settings.py b/tests/settings.py
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -38,6 +38,14 @@
     },
 }
 
+# Using synchronous mode for testing
+RQ_QUEUES = {
+    'default': {
+        'USE_REDIS_CACHE': 'redis',
+        'DEFAULT_TIMEOUT': 360,
+        'ASYNC': False,
+    },
+}
 
 # Mail server settings
 EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'