Repository: fidals/shopelectro

Issue:
Render only first-leveled children at the header menu
Now we have too many categories at the header menu
![image](https://user-images.githubusercontent.com/3526796/65932829-d6c31280-e417-11e9-8b01-3ec7f32c76b5.png)



Discussion:
i'll add tests with the same task

Resolving patch:
diff --git a/shopelectro/logic/header.py b/shopelectro/logic/header.py
--- a/shopelectro/logic/header.py
+++ b/shopelectro/logic/header.py
@@ -1,3 +1,6 @@
+import typing
+from functools import lru_cache
+
 from django.conf import settings
 from django.db.models import Q
 
@@ -5,26 +8,41 @@
 from shopelectro import models
 
 
-def menu_qs() -> pages_models.PageQuerySet:
-    """
-    QuerySet with header menu items.
+class Menu:
+    DICT_TYPE = typing.Dict[models.CategoryPage, typing.List[models.CategoryPage]]
+
+    @staticmethod
+    def roots() -> pages_models.PageQuerySet:
+        """
+        QuerySet with header menu items.
 
-    Contains root categories.
-    Result can be tuned HEADER_LINKS settings option.
-    """
-    return (
-        pages_models.Page.objects.active()
-        .filter(
-            Q(slug__in=settings.HEADER_LINKS['add'])
-            | (
-                # @todo #974:30m  Optimize the header menu query.
-                #  Fetch catalog page for the header menu at the same query.
-                #  root category pages.
-                Q(parent=pages_models.CustomPage.objects.filter(slug='catalog'))
-                & Q(type='model')
-                & Q(related_model_name=models.Category._meta.db_table)
-                & ~Q(slug__in=settings.HEADER_LINKS['exclude'])
+        Contains root categories.
+        Result can be tuned HEADER_LINKS settings option.
+        """
+        return (
+            pages_models.Page.objects.active()
+            .filter(
+                Q(slug__in=settings.HEADER_LINKS['add'])
+                | (
+                    # @todo #974:30m  Optimize the header menu query.
+                    #  Fetch catalog page for the header menu at the same query.
+                    #  root category pages.
+                    Q(parent=pages_models.CustomPage.objects.filter(slug='catalog'))
+                    & Q(type='model')
+                    & Q(related_model_name=models.Category._meta.db_table)
+                    & ~Q(slug__in=settings.HEADER_LINKS['exclude'])
+                )
             )
+            .order_by('position')
         )
-        .order_by('position')
-    )
+
+    @lru_cache(maxsize=1)
+    def as_dict(self) -> DICT_TYPE:
+        return {
+            root: list(
+                root.get_children()
+                .filter(type='model')
+                .filter(related_model_name=models.Category._meta.db_table)
+            )
+            for root in self.roots().iterator()
+        }
diff --git a/shopelectro/templatetags/se_extras.py b/shopelectro/templatetags/se_extras.py
--- a/shopelectro/templatetags/se_extras.py
+++ b/shopelectro/templatetags/se_extras.py
@@ -15,8 +15,8 @@
 
 
 @register.simple_tag
-def roots():
-    return logic.header.menu_qs()
+def header_menu() -> logic.header.Menu:
+    return logic.header.Menu()
 
 
 @register.simple_tag


Test patch:
diff --git a/shopelectro/tests/tests_logic.py b/shopelectro/tests/tests_logic.py
--- a/shopelectro/tests/tests_logic.py
+++ b/shopelectro/tests/tests_logic.py
@@ -13,13 +13,21 @@ class HeaderMenu(TestCase):
 
     def test_roots_are_presented(self):
         from_db = set(models.CategoryPage.objects.get_cached_trees())
-        from_logic = set(logic.header.menu_qs())
+        from_logic = set(logic.header.Menu().as_dict().keys())
         self.assertEqual(from_db, from_logic)
 
+    def test_children_contains_no_products(self):
+        root = models.CategoryPage.objects.get_cached_trees()[0]
+        product = models.ProductPage.objects.first()
+        product.parent = root
+        product.save()
+        children = logic.header.Menu().as_dict()[root]
+        self.assertNotIn(product, children)
+
     @override_settings(HEADER_LINKS={'exclude': [to_exclude.slug], 'add': []})
     def test_exclude_option(self):
-        self.assertNotIn(self.to_exclude, logic.header.menu_qs())
+        self.assertNotIn(self.to_exclude, logic.header.Menu().as_dict().keys())
 
     @override_settings(HEADER_LINKS={'exclude': [], 'add': [to_add.slug]})
     def test_add_option(self):
-        self.assertIn(self.to_add, logic.header.menu_qs())
+        self.assertIn(self.to_add, logic.header.Menu().as_dict().keys())
diff --git a/shopelectro/tests/tests_views.py b/shopelectro/tests/tests_views.py
--- a/shopelectro/tests/tests_views.py
+++ b/shopelectro/tests/tests_views.py
@@ -1018,18 +1018,15 @@ class Header(ViewsTestCase):
     def test_menu_roots(self):
         soup = get_soup(page=self.client.get('/'))
         from_page = HeaderMenu(soup).roots()
-        from_logic = list(
-            logic.header.menu_qs().values_list('name', flat=True)
-        )
+        from_logic = [
+            root.name for root in logic.header.Menu().as_dict().keys()
+        ]
         self.assertEqual(from_logic, from_page)
 
     def test_menu_children(self):
         soup = get_soup(page=self.client.get('/'))
         menu = HeaderMenu(soup)
-        for root in logic.header.menu_qs().iterator():
+        for root, children in logic.header.Menu().as_dict().items():
             from_page = menu.children(root.name)
-            from_logic = list(
-                root.get_children().values_list('name', flat=True)
-            )
+            from_logic = [p.name for p in children]
             self.assertEqual(from_logic, from_page)
-