Repository: translate/pootle

Issue:
Stats: handling of empty directories
Stats for  empty directories won't be set after we merge #3621 and switch to the new refreshing stats process which starts from every Store object if we try to fill empty cache. 
Hence there are questions:
- Should we show directories and translation projects if they are empty?
- How to initialize zero stats for empty directories?



Discussion:
There shouldn't be empty directories in the UI

Since we have `obsolete` field for `Directory` object we can adjust `update_stores` command to mark empty directories (or directories containing obsolete directories only) as obsolete. Then we can get rid of `disabled` field of `TranslationProject` (#3674).


Resolving patch:
diff --git a/pootle/apps/pootle_app/project_tree.py b/pootle/apps/pootle_app/project_tree.py
--- a/pootle/apps/pootle_app/project_tree.py
+++ b/pootle/apps/pootle_app/project_tree.py
@@ -243,14 +243,20 @@ def add_files(translation_project, ignored_files, ext, relative_dir, db_dir,
         db_dir,
     )
 
+    is_empty = len(files) == 0
     for db_subdir in db_subdirs:
         fs_subdir = os.path.join(relative_dir, db_subdir.name)
-        _files, _new_files = add_files(translation_project, ignored_files, ext,
-                                       fs_subdir, db_subdir, file_filter)
+        _files, _new_files, _is_empty = \
+            add_files(translation_project, ignored_files, ext, fs_subdir,
+                      db_subdir, file_filter)
         files += _files
         new_files += _new_files
+        is_empty &= _is_empty
 
-    return files, new_files
+    if is_empty:
+        db_dir.makeobsolete()
+
+    return files, new_files, is_empty
 
 
 def to_podir_path(path):
diff --git a/pootle/apps/pootle_translationproject/models.py b/pootle/apps/pootle_translationproject/models.py
--- a/pootle/apps/pootle_translationproject/models.py
+++ b/pootle/apps/pootle_translationproject/models.py
@@ -359,7 +359,7 @@ def scan_files(self):
         else:
             file_filter = lambda filename: True
 
-        all_files, new_files = add_files(
+        all_files, new_files, is_empty = add_files(
                 self,
                 ignored_files,
                 ext,


Test patch:
diff --git a/tests/data/po/tutorial/es/subdir/remove_tutorial.po b/tests/data/po/tutorial/es/subdir/remove_tutorial.po
new file mode 100644
--- /dev/null
+++ b/tests/data/po/tutorial/es/subdir/remove_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 "apple"
+msgstr ""
+
+#: test.c
+msgid "tomato"
+msgstr ""
+
+#: fish.c
+msgid "%d apple"
+msgid_plural "%d apples"
+msgstr[0] ""
+msgstr[1] ""
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
@@ -62,6 +62,12 @@ def french(english):
     return _require_language('fr', 'French')
 
 
+@pytest.fixture
+def spanish(english):
+    """Require the Spanish language."""
+    return _require_language('es', 'Spanish')
+
+
 @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
@@ -90,3 +90,10 @@ def fr_tutorial_remove_sync_po(settings, french_tutorial, system):
     """Require the /fr/tutorial/remove_sync_tutorial.po store."""
     po_directory = settings.PODIRECTORY
     return _require_store(french_tutorial, po_directory, 'remove_sync_tutorial.po')
+
+
+@pytest.fixture
+def es_tutorial_subdir_remove_po(settings, spanish_tutorial, system):
+    """Require the /es/tutorial/subdir/remove_tutorial.po store."""
+    po_directory = settings.PODIRECTORY
+    return _require_store(spanish_tutorial, po_directory, 'subdir/remove_tutorial.po')
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
@@ -43,3 +43,9 @@ def arabic_tutorial_disabled(arabic, tutorial):
 def french_tutorial(french, tutorial):
     """Require French Tutorial."""
     return _require_tp(french, tutorial)
+
+
+@pytest.fixture
+def spanish_tutorial(spanish, tutorial):
+    """Require French Tutorial."""
+    return _require_tp(spanish, tutorial)
diff --git a/tests/models/directory.py b/tests/models/directory.py
--- a/tests/models/directory.py
+++ b/tests/models/directory.py
@@ -68,3 +68,18 @@ def test_delete_mark_obsolete_resurrect_sync(fr_tutorial_subdir_to_remove_po):
     updated_store.sync(only_newer=False)
     # Now file and directory for the store should exist
     assert os.path.exists(updated_store.file.path)
+
+
+@pytest.mark.django_db
+def test_scan_empty_project_obsolete_dirs(es_tutorial_subdir_remove_po):
+    """Tests that the in-DB Directories are marked as obsolete
+    if the on-disk directories are empty.
+    """
+    spanish_tutorial = es_tutorial_subdir_remove_po.translation_project
+    os.remove(es_tutorial_subdir_remove_po.file.path)
+
+    spanish_tutorial.scan_files()
+    for item in spanish_tutorial.directory.child_dirs.all():
+        assert item.obsolete
+
+    assert spanish_tutorial.directory.obsolete