We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
SE Iterate tags in templates
[trello origin](https://trello.com/c/zulRj7lF/294-se-iterate-tags-in-templates)
[seo templates doc](https://docs.google.com/document/d/18DFBsuh6NT8hjyihOJ2bxw9zEe8z_070MBQrbAq0kvE/edit#)

**Проблема**
Сеошники после некоторых применений мультисв-в обнаружили заголовок *Блоки питания для ноутбуков устанавливает пользователь и от сети 220 В* [по этому линку](https://www.shopelectro.ru/catalog/categories/bloki-pitaniia-288/tags/ustanavlivaet-polzovatel-and-ot-seti-220-v/)

Получилось нехорошо. Предлагают сделать заголовок таким:
*Блоки питания для ноутбуков от сети 220 В, выбор выходного напряжения - устанавливает пользователь*


**Решение**
Чтобы переделать заголовок, нам нужно добавить в шаблон возможность указывать имя одного конкретного тега.

А решить это можно так: добавляем полноценный tags в seo-шаблоны.
Сейчас у нас только обрезанный tags.titles

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `shopelectro/views/catalog.py`
Content:
```
1 from functools import partial
2 
3 from django.conf import settings
4 from django.http import HttpResponse, HttpResponseForbidden
5 from django.shortcuts import render, get_object_or_404
6 from django.views.decorators.http import require_POST
7 from django.urls import reverse
8 from django_user_agents.utils import get_user_agent
9 
10 from catalog.views import catalog
11 from images.models import Image
12 from pages import views as pages_views
13 
14 from shopelectro import config
15 from shopelectro import models
16 from shopelectro.views.helpers import set_csrf_cookie
17 
18 PRODUCTS_ON_PAGE_PC = 48
19 PRODUCTS_ON_PAGE_MOB = 10
20 
21 
22 def get_products_count(request):
23     """Get Products count for response context depends on the `user_agent`."""
24     mobile_view = get_user_agent(request).is_mobile
25     return PRODUCTS_ON_PAGE_MOB if mobile_view else PRODUCTS_ON_PAGE_PC
26 
27 
28 # CATALOG VIEWS
29 class CategoryTree(catalog.CategoryTree):
30     category_model = models.Category
31 
32 
33 @set_csrf_cookie
34 class ProductPage(catalog.ProductPage):
35     pk_url_kwarg = None
36     slug_url_kwarg = 'product_vendor_code'
37     slug_field = 'vendor_code'
38 
39     queryset = (
40         models.Product.objects
41         .filter(category__isnull=False)
42         .prefetch_related('product_feedbacks', 'page__images')
43         .select_related('page')
44     )
45 
46     def get_context_data(self, **kwargs):
47         context = super(ProductPage, self).get_context_data(**kwargs)
48 
49         group_tags_pairs = (
50             models.Tag.objects
51             .filter(products=self.object)
52             .get_group_tags_pairs()
53         )
54 
55         return {
56             **context,
57             'price_bounds': config.PRICE_BOUNDS,
58             'group_tags_pairs': group_tags_pairs
59         }
60 
61 
62 # SHOPELECTRO-SPECIFIC VIEWS
63 @set_csrf_cookie
64 class IndexPage(pages_views.CustomPageView):
65 
66     def get_context_data(self, **kwargs):
67         """Extended method. Add product's images to context."""
68         context = super(IndexPage, self).get_context_data(**kwargs)
69         mobile_view = get_user_agent(self.request).is_mobile
70 
71         top_products = (
72             models.Product.objects
73             .filter(id__in=settings.TOP_PRODUCTS)
74             .prefetch_related('category')
75             .select_related('page')
76         )
77 
78         images = Image.objects.get_main_images_by_pages(
79             models.ProductPage.objects.filter(
80                 shopelectro_product__in=top_products
81             )
82         )
83 
84         categories = models.Category.objects.get_root_categories_by_products(
85             top_products)
86 
87         prepared_top_products = []
88         if not mobile_view:
89             prepared_top_products = [
90                 (product, images.get(product.page), categories.get(product))
91                 for product in top_products
92             ]
93 
94         return {
95             **context,
96             'category_tile': config.MAIN_PAGE_TILE,
97             'prepared_top_products': prepared_top_products,
98         }
99 
100 
101 def merge_products_and_images(products):
102     images = Image.objects.get_main_images_by_pages(
103         models.ProductPage.objects.filter(shopelectro_product__in=products)
104     )
105 
106     return [
107         (product, images.get(product.page))
108         for product in products
109     ]
110 
111 
112 @set_csrf_cookie
113 class CategoryPage(catalog.CategoryPage):
114 
115     def get_context_data(self, **kwargs):
116         """Add sorting options and view_types in context."""
117         context = super(CategoryPage, self).get_context_data(**kwargs)
118         products_on_page = get_products_count(self.request)
119 
120         # tile is default view_type
121         view_type = self.request.session.get('view_type', 'tile')
122 
123         category = context['category']
124 
125         sorting = int(self.kwargs.get('sorting', 0))
126         sorting_option = config.category_sorting(sorting)
127 
128         all_products = (
129             models.Product.objects
130             .prefetch_related('page__images')
131             .select_related('page')
132             .get_by_category(category, ordering=(sorting_option, ))
133         )
134 
135         group_tags_pairs = (
136             models.Tag.objects
137             .filter(products__in=all_products)
138             .get_group_tags_pairs()
139         )
140 
141         tags = self.kwargs.get('tags')
142         tags_metadata = {
143             'titles': '',
144         }
145 
146         if tags:
147             slugs = models.Tag.parse_url_tags(tags)
148             tags = models.Tag.objects.filter(slug__in=slugs)
149 
150             all_products = (
151                 all_products
152                 .filter(tags__in=tags)
153                 # Use distinct because filtering by QuerySet tags,
154                 # that related with products by many-to-many relation.
155                 .distinct(sorting_option.lstrip('-'))
156             )
157 
158             tags_titles = models.Tag.serialize_title_tags(
159                 tags.get_group_tags_pairs()
160             )
161 
162             tags_metadata['titles'] = tags_titles
163 
164         def template_context(page, tags):
165             return {
166                 'page': page,
167                 'tags': tags,
168             }
169 
170         page = context['page']
171         page.get_template_render_context = partial(
172             template_context, page, tags_metadata)
173 
174         products = all_products.get_offset(0, products_on_page)
175 
176         return {
177             **context,
178             'product_image_pairs': merge_products_and_images(products),
179             'group_tags_pairs': group_tags_pairs,
180             'total_products': all_products.count(),
181             'sorting_options': config.category_sorting(),
182             'sort': sorting,
183             'tags': tags,
184             'view_type': view_type,
185             'tags_metadata': tags_metadata,
186             'skip_canonical': bool(tags),
187         }
188 
189 
190 def load_more(request, category_slug, offset=0, sorting=0, tags=None):
191     """
192     Load more products of a given category.
193 
194     :param sorting: preferred sorting index from CATEGORY_SORTING tuple
195     :param request: HttpRequest object
196     :param category_slug: Slug for a given category
197     :param offset: used for slicing QuerySet.
198     :return:
199     """
200     products_on_page = get_products_count(request)
201 
202     category = get_object_or_404(models.CategoryPage, slug=category_slug).model
203     sorting_option = config.category_sorting(int(sorting))
204 
205     products = (
206         models.Product.objects
207         .prefetch_related('page__images')
208         .select_related('page')
209         .get_by_category(category, ordering=(sorting_option,))
210     )
211 
212     if tags:
213         tag_entities = models.Tag.objects.filter(
214             slug__in=models.Tag.parse_url_tags(tags)
215         )
216 
217         products = (
218             products
219             .filter(tags__in=tag_entities)
220             # Use distinct because filtering by QuerySet tags,
221             # that related with products by many-to-many relation.
222             .distinct(sorting_option.lstrip('-'))
223         )
224 
225     products = products.get_offset(int(offset), products_on_page)
226     view = request.session.get('view_type', 'tile')
227 
228     return render(request, 'catalog/category_products.html', {
229         'product_image_pairs': merge_products_and_images(products),
230         'view_type': view,
231         'prods': products_on_page,
232     })
233 
234 
235 @require_POST
236 def save_feedback(request):
237     def get_keys_from_post(*args):
238         return {arg: request.POST.get(arg, '') for arg in args}
239 
240     product_id = request.POST.get('id')
241     product = models.Product.objects.filter(id=product_id).first()
242     if not (product_id and product):
243         return HttpResponse(status=422)
244 
245     fields = ['rating', 'name', 'dignities', 'limitations', 'general']
246     feedback_data = get_keys_from_post(*fields)
247 
248     models.ProductFeedback.objects.create(product=product, **feedback_data)
249     return HttpResponse('ok')
250 
251 
252 @require_POST
253 def delete_feedback(request):
254     if not request.user.is_authenticated:
255         return HttpResponseForbidden('Not today, sly guy...')
256 
257     feedback_id = request.POST.get('id')
258     feedback = models.ProductFeedback.objects.filter(id=feedback_id).first()
259     if not (feedback_id and feedback):
260         return HttpResponse(status=422)
261 
262     feedback.delete()
263     return HttpResponse('Feedback with id={} was deleted.'.format(feedback_id))
264 
265 
266 class ProductsWithoutImages(catalog.ProductsWithoutImages):
267     model = models.Product
268 
269 
270 class ProductsWithoutText(catalog.ProductsWithoutText):
271     model = models.Product
272
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/shopelectro/views/catalog.py b/shopelectro/views/catalog.py
--- a/shopelectro/views/catalog.py
+++ b/shopelectro/views/catalog.py
@@ -141,6 +141,7 @@
         tags = self.kwargs.get('tags')
         tags_metadata = {
             'titles': '',
+            'raw': [],
         }
 
         if tags:
@@ -160,6 +161,7 @@
             )
 
             tags_metadata['titles'] = tags_titles
+            tags_metadata['raw'] = tags
 
         def template_context(page, tags):
             return {

{"golden_diff": "diff --git a/shopelectro/views/catalog.py b/shopelectro/views/catalog.py\n--- a/shopelectro/views/catalog.py\n+++ b/shopelectro/views/catalog.py\n@@ -141,6 +141,7 @@\n         tags = self.kwargs.get('tags')\n         tags_metadata = {\n             'titles': '',\n+            'raw': [],\n         }\n \n         if tags:\n@@ -160,6 +161,7 @@\n             )\n \n             tags_metadata['titles'] = tags_titles\n+            tags_metadata['raw'] = tags\n \n         def template_context(page, tags):\n             return {\n", "issue": "SE Iterate tags in templates\n[trello origin](https://trello.com/c/zulRj7lF/294-se-iterate-tags-in-templates)\r\n[seo templates doc](https://docs.google.com/document/d/18DFBsuh6NT8hjyihOJ2bxw9zEe8z_070MBQrbAq0kvE/edit#)\r\n\r\n**\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430**\r\n\u0421\u0435\u043e\u0448\u043d\u0438\u043a\u0438 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u043c\u0443\u043b\u044c\u0442\u0438\u0441\u0432-\u0432 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u043b\u0438 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a *\u0411\u043b\u043e\u043a\u0438 \u043f\u0438\u0442\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u043d\u043e\u0443\u0442\u0431\u0443\u043a\u043e\u0432 \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0438 \u043e\u0442 \u0441\u0435\u0442\u0438 220 \u0412* [\u043f\u043e \u044d\u0442\u043e\u043c\u0443 \u043b\u0438\u043d\u043a\u0443](https://www.shopelectro.ru/catalog/categories/bloki-pitaniia-288/tags/ustanavlivaet-polzovatel-and-ot-seti-220-v/)\r\n\r\n\u041f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c \u043d\u0435\u0445\u043e\u0440\u043e\u0448\u043e. \u041f\u0440\u0435\u0434\u043b\u0430\u0433\u0430\u044e\u0442 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u0442\u0430\u043a\u0438\u043c:\r\n*\u0411\u043b\u043e\u043a\u0438 \u043f\u0438\u0442\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u043d\u043e\u0443\u0442\u0431\u0443\u043a\u043e\u0432 \u043e\u0442 \u0441\u0435\u0442\u0438 220 \u0412, \u0432\u044b\u0431\u043e\u0440 \u0432\u044b\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f - \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c*\r\n\r\n\r\n**\u0420\u0435\u0448\u0435\u043d\u0438\u0435**\r\n\u0427\u0442\u043e\u0431\u044b \u043f\u0435\u0440\u0435\u0434\u0435\u043b\u0430\u0442\u044c \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a, \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432 \u0448\u0430\u0431\u043b\u043e\u043d \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0438\u043c\u044f \u043e\u0434\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u0442\u0435\u0433\u0430.\r\n\r\n\u0410 \u0440\u0435\u0448\u0438\u0442\u044c \u044d\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0442\u0430\u043a: \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043f\u043e\u043b\u043d\u043e\u0446\u0435\u043d\u043d\u044b\u0439 tags \u0432 seo-\u0448\u0430\u0431\u043b\u043e\u043d\u044b.\r\n\u0421\u0435\u0439\u0447\u0430\u0441 \u0443 \u043d\u0430\u0441 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0431\u0440\u0435\u0437\u0430\u043d\u043d\u044b\u0439 tags.titles\n", "before_files": [{"content": "from functools import partial\n\nfrom django.conf import settings\nfrom django.http import HttpResponse, HttpResponseForbidden\nfrom django.shortcuts import render, get_object_or_404\nfrom django.views.decorators.http import require_POST\nfrom django.urls import reverse\nfrom django_user_agents.utils import get_user_agent\n\nfrom catalog.views import catalog\nfrom images.models import Image\nfrom pages import views as pages_views\n\nfrom shopelectro import config\nfrom shopelectro import models\nfrom shopelectro.views.helpers import set_csrf_cookie\n\nPRODUCTS_ON_PAGE_PC = 48\nPRODUCTS_ON_PAGE_MOB = 10\n\n\ndef get_products_count(request):\n    \"\"\"Get Products count for response context depends on the `user_agent`.\"\"\"\n    mobile_view = get_user_agent(request).is_mobile\n    return PRODUCTS_ON_PAGE_MOB if mobile_view else PRODUCTS_ON_PAGE_PC\n\n\n# CATALOG VIEWS\nclass CategoryTree(catalog.CategoryTree):\n    category_model = models.Category\n\n\n@set_csrf_cookie\nclass ProductPage(catalog.ProductPage):\n    pk_url_kwarg = None\n    slug_url_kwarg = 'product_vendor_code'\n    slug_field = 'vendor_code'\n\n    queryset = (\n        models.Product.objects\n        .filter(category__isnull=False)\n        .prefetch_related('product_feedbacks', 'page__images')\n        .select_related('page')\n    )\n\n    def get_context_data(self, **kwargs):\n        context = super(ProductPage, self).get_context_data(**kwargs)\n\n        group_tags_pairs = (\n            models.Tag.objects\n            .filter(products=self.object)\n            .get_group_tags_pairs()\n        )\n\n        return {\n            **context,\n            'price_bounds': config.PRICE_BOUNDS,\n            'group_tags_pairs': group_tags_pairs\n        }\n\n\n# SHOPELECTRO-SPECIFIC VIEWS\n@set_csrf_cookie\nclass IndexPage(pages_views.CustomPageView):\n\n    def get_context_data(self, **kwargs):\n        \"\"\"Extended method. Add product's images to context.\"\"\"\n        context = super(IndexPage, self).get_context_data(**kwargs)\n        mobile_view = get_user_agent(self.request).is_mobile\n\n        top_products = (\n            models.Product.objects\n            .filter(id__in=settings.TOP_PRODUCTS)\n            .prefetch_related('category')\n            .select_related('page')\n        )\n\n        images = Image.objects.get_main_images_by_pages(\n            models.ProductPage.objects.filter(\n                shopelectro_product__in=top_products\n            )\n        )\n\n        categories = models.Category.objects.get_root_categories_by_products(\n            top_products)\n\n        prepared_top_products = []\n        if not mobile_view:\n            prepared_top_products = [\n                (product, images.get(product.page), categories.get(product))\n                for product in top_products\n            ]\n\n        return {\n            **context,\n            'category_tile': config.MAIN_PAGE_TILE,\n            'prepared_top_products': prepared_top_products,\n        }\n\n\ndef merge_products_and_images(products):\n    images = Image.objects.get_main_images_by_pages(\n        models.ProductPage.objects.filter(shopelectro_product__in=products)\n    )\n\n    return [\n        (product, images.get(product.page))\n        for product in products\n    ]\n\n\n@set_csrf_cookie\nclass CategoryPage(catalog.CategoryPage):\n\n    def get_context_data(self, **kwargs):\n        \"\"\"Add sorting options and view_types in context.\"\"\"\n        context = super(CategoryPage, self).get_context_data(**kwargs)\n        products_on_page = get_products_count(self.request)\n\n        # tile is default view_type\n        view_type = self.request.session.get('view_type', 'tile')\n\n        category = context['category']\n\n        sorting = int(self.kwargs.get('sorting', 0))\n        sorting_option = config.category_sorting(sorting)\n\n        all_products = (\n            models.Product.objects\n            .prefetch_related('page__images')\n            .select_related('page')\n            .get_by_category(category, ordering=(sorting_option, ))\n        )\n\n        group_tags_pairs = (\n            models.Tag.objects\n            .filter(products__in=all_products)\n            .get_group_tags_pairs()\n        )\n\n        tags = self.kwargs.get('tags')\n        tags_metadata = {\n            'titles': '',\n        }\n\n        if tags:\n            slugs = models.Tag.parse_url_tags(tags)\n            tags = models.Tag.objects.filter(slug__in=slugs)\n\n            all_products = (\n                all_products\n                .filter(tags__in=tags)\n                # Use distinct because filtering by QuerySet tags,\n                # that related with products by many-to-many relation.\n                .distinct(sorting_option.lstrip('-'))\n            )\n\n            tags_titles = models.Tag.serialize_title_tags(\n                tags.get_group_tags_pairs()\n            )\n\n            tags_metadata['titles'] = tags_titles\n\n        def template_context(page, tags):\n            return {\n                'page': page,\n                'tags': tags,\n            }\n\n        page = context['page']\n        page.get_template_render_context = partial(\n            template_context, page, tags_metadata)\n\n        products = all_products.get_offset(0, products_on_page)\n\n        return {\n            **context,\n            'product_image_pairs': merge_products_and_images(products),\n            'group_tags_pairs': group_tags_pairs,\n            'total_products': all_products.count(),\n            'sorting_options': config.category_sorting(),\n            'sort': sorting,\n            'tags': tags,\n            'view_type': view_type,\n            'tags_metadata': tags_metadata,\n            'skip_canonical': bool(tags),\n        }\n\n\ndef load_more(request, category_slug, offset=0, sorting=0, tags=None):\n    \"\"\"\n    Load more products of a given category.\n\n    :param sorting: preferred sorting index from CATEGORY_SORTING tuple\n    :param request: HttpRequest object\n    :param category_slug: Slug for a given category\n    :param offset: used for slicing QuerySet.\n    :return:\n    \"\"\"\n    products_on_page = get_products_count(request)\n\n    category = get_object_or_404(models.CategoryPage, slug=category_slug).model\n    sorting_option = config.category_sorting(int(sorting))\n\n    products = (\n        models.Product.objects\n        .prefetch_related('page__images')\n        .select_related('page')\n        .get_by_category(category, ordering=(sorting_option,))\n    )\n\n    if tags:\n        tag_entities = models.Tag.objects.filter(\n            slug__in=models.Tag.parse_url_tags(tags)\n        )\n\n        products = (\n            products\n            .filter(tags__in=tag_entities)\n            # Use distinct because filtering by QuerySet tags,\n            # that related with products by many-to-many relation.\n            .distinct(sorting_option.lstrip('-'))\n        )\n\n    products = products.get_offset(int(offset), products_on_page)\n    view = request.session.get('view_type', 'tile')\n\n    return render(request, 'catalog/category_products.html', {\n        'product_image_pairs': merge_products_and_images(products),\n        'view_type': view,\n        'prods': products_on_page,\n    })\n\n\n@require_POST\ndef save_feedback(request):\n    def get_keys_from_post(*args):\n        return {arg: request.POST.get(arg, '') for arg in args}\n\n    product_id = request.POST.get('id')\n    product = models.Product.objects.filter(id=product_id).first()\n    if not (product_id and product):\n        return HttpResponse(status=422)\n\n    fields = ['rating', 'name', 'dignities', 'limitations', 'general']\n    feedback_data = get_keys_from_post(*fields)\n\n    models.ProductFeedback.objects.create(product=product, **feedback_data)\n    return HttpResponse('ok')\n\n\n@require_POST\ndef delete_feedback(request):\n    if not request.user.is_authenticated:\n        return HttpResponseForbidden('Not today, sly guy...')\n\n    feedback_id = request.POST.get('id')\n    feedback = models.ProductFeedback.objects.filter(id=feedback_id).first()\n    if not (feedback_id and feedback):\n        return HttpResponse(status=422)\n\n    feedback.delete()\n    return HttpResponse('Feedback with id={} was deleted.'.format(feedback_id))\n\n\nclass ProductsWithoutImages(catalog.ProductsWithoutImages):\n    model = models.Product\n\n\nclass ProductsWithoutText(catalog.ProductsWithoutText):\n    model = models.Product\n", "path": "shopelectro/views/catalog.py"}], "after_files": [{"content": "from functools import partial\n\nfrom django.conf import settings\nfrom django.http import HttpResponse, HttpResponseForbidden\nfrom django.shortcuts import render, get_object_or_404\nfrom django.views.decorators.http import require_POST\nfrom django.urls import reverse\nfrom django_user_agents.utils import get_user_agent\n\nfrom catalog.views import catalog\nfrom images.models import Image\nfrom pages import views as pages_views\n\nfrom shopelectro import config\nfrom shopelectro import models\nfrom shopelectro.views.helpers import set_csrf_cookie\n\nPRODUCTS_ON_PAGE_PC = 48\nPRODUCTS_ON_PAGE_MOB = 10\n\n\ndef get_products_count(request):\n    \"\"\"Get Products count for response context depends on the `user_agent`.\"\"\"\n    mobile_view = get_user_agent(request).is_mobile\n    return PRODUCTS_ON_PAGE_MOB if mobile_view else PRODUCTS_ON_PAGE_PC\n\n\n# CATALOG VIEWS\nclass CategoryTree(catalog.CategoryTree):\n    category_model = models.Category\n\n\n@set_csrf_cookie\nclass ProductPage(catalog.ProductPage):\n    pk_url_kwarg = None\n    slug_url_kwarg = 'product_vendor_code'\n    slug_field = 'vendor_code'\n\n    queryset = (\n        models.Product.objects\n        .filter(category__isnull=False)\n        .prefetch_related('product_feedbacks', 'page__images')\n        .select_related('page')\n    )\n\n    def get_context_data(self, **kwargs):\n        context = super(ProductPage, self).get_context_data(**kwargs)\n\n        group_tags_pairs = (\n            models.Tag.objects\n            .filter(products=self.object)\n            .get_group_tags_pairs()\n        )\n\n        return {\n            **context,\n            'price_bounds': config.PRICE_BOUNDS,\n            'group_tags_pairs': group_tags_pairs\n        }\n\n\n# SHOPELECTRO-SPECIFIC VIEWS\n@set_csrf_cookie\nclass IndexPage(pages_views.CustomPageView):\n\n    def get_context_data(self, **kwargs):\n        \"\"\"Extended method. Add product's images to context.\"\"\"\n        context = super(IndexPage, self).get_context_data(**kwargs)\n        mobile_view = get_user_agent(self.request).is_mobile\n\n        top_products = (\n            models.Product.objects\n            .filter(id__in=settings.TOP_PRODUCTS)\n            .prefetch_related('category')\n            .select_related('page')\n        )\n\n        images = Image.objects.get_main_images_by_pages(\n            models.ProductPage.objects.filter(\n                shopelectro_product__in=top_products\n            )\n        )\n\n        categories = models.Category.objects.get_root_categories_by_products(\n            top_products)\n\n        prepared_top_products = []\n        if not mobile_view:\n            prepared_top_products = [\n                (product, images.get(product.page), categories.get(product))\n                for product in top_products\n            ]\n\n        return {\n            **context,\n            'category_tile': config.MAIN_PAGE_TILE,\n            'prepared_top_products': prepared_top_products,\n        }\n\n\ndef merge_products_and_images(products):\n    images = Image.objects.get_main_images_by_pages(\n        models.ProductPage.objects.filter(shopelectro_product__in=products)\n    )\n\n    return [\n        (product, images.get(product.page))\n        for product in products\n    ]\n\n\n@set_csrf_cookie\nclass CategoryPage(catalog.CategoryPage):\n\n    def get_context_data(self, **kwargs):\n        \"\"\"Add sorting options and view_types in context.\"\"\"\n        context = super(CategoryPage, self).get_context_data(**kwargs)\n        products_on_page = get_products_count(self.request)\n\n        # tile is default view_type\n        view_type = self.request.session.get('view_type', 'tile')\n\n        category = context['category']\n\n        sorting = int(self.kwargs.get('sorting', 0))\n        sorting_option = config.category_sorting(sorting)\n\n        all_products = (\n            models.Product.objects\n            .prefetch_related('page__images')\n            .select_related('page')\n            .get_by_category(category, ordering=(sorting_option, ))\n        )\n\n        group_tags_pairs = (\n            models.Tag.objects\n            .filter(products__in=all_products)\n            .get_group_tags_pairs()\n        )\n\n        tags = self.kwargs.get('tags')\n        tags_metadata = {\n            'titles': '',\n            'raw': [],\n        }\n\n        if tags:\n            slugs = models.Tag.parse_url_tags(tags)\n            tags = models.Tag.objects.filter(slug__in=slugs)\n\n            all_products = (\n                all_products\n                .filter(tags__in=tags)\n                # Use distinct because filtering by QuerySet tags,\n                # that related with products by many-to-many relation.\n                .distinct(sorting_option.lstrip('-'))\n            )\n\n            tags_titles = models.Tag.serialize_title_tags(\n                tags.get_group_tags_pairs()\n            )\n\n            tags_metadata['titles'] = tags_titles\n            tags_metadata['raw'] = tags\n\n        def template_context(page, tags):\n            return {\n                'page': page,\n                'tags': tags,\n            }\n\n        page = context['page']\n        page.get_template_render_context = partial(\n            template_context, page, tags_metadata)\n\n        products = all_products.get_offset(0, products_on_page)\n\n        return {\n            **context,\n            'product_image_pairs': merge_products_and_images(products),\n            'group_tags_pairs': group_tags_pairs,\n            'total_products': all_products.count(),\n            'sorting_options': config.category_sorting(),\n            'sort': sorting,\n            'tags': tags,\n            'view_type': view_type,\n            'tags_metadata': tags_metadata,\n            'skip_canonical': bool(tags),\n        }\n\n\ndef load_more(request, category_slug, offset=0, sorting=0, tags=None):\n    \"\"\"\n    Load more products of a given category.\n\n    :param sorting: preferred sorting index from CATEGORY_SORTING tuple\n    :param request: HttpRequest object\n    :param category_slug: Slug for a given category\n    :param offset: used for slicing QuerySet.\n    :return:\n    \"\"\"\n    products_on_page = get_products_count(request)\n\n    category = get_object_or_404(models.CategoryPage, slug=category_slug).model\n    sorting_option = config.category_sorting(int(sorting))\n\n    products = (\n        models.Product.objects\n        .prefetch_related('page__images')\n        .select_related('page')\n        .get_by_category(category, ordering=(sorting_option,))\n    )\n\n    if tags:\n        tag_entities = models.Tag.objects.filter(\n            slug__in=models.Tag.parse_url_tags(tags)\n        )\n\n        products = (\n            products\n            .filter(tags__in=tag_entities)\n            # Use distinct because filtering by QuerySet tags,\n            # that related with products by many-to-many relation.\n            .distinct(sorting_option.lstrip('-'))\n        )\n\n    products = products.get_offset(int(offset), products_on_page)\n    view = request.session.get('view_type', 'tile')\n\n    return render(request, 'catalog/category_products.html', {\n        'product_image_pairs': merge_products_and_images(products),\n        'view_type': view,\n        'prods': products_on_page,\n    })\n\n\n@require_POST\ndef save_feedback(request):\n    def get_keys_from_post(*args):\n        return {arg: request.POST.get(arg, '') for arg in args}\n\n    product_id = request.POST.get('id')\n    product = models.Product.objects.filter(id=product_id).first()\n    if not (product_id and product):\n        return HttpResponse(status=422)\n\n    fields = ['rating', 'name', 'dignities', 'limitations', 'general']\n    feedback_data = get_keys_from_post(*fields)\n\n    models.ProductFeedback.objects.create(product=product, **feedback_data)\n    return HttpResponse('ok')\n\n\n@require_POST\ndef delete_feedback(request):\n    if not request.user.is_authenticated:\n        return HttpResponseForbidden('Not today, sly guy...')\n\n    feedback_id = request.POST.get('id')\n    feedback = models.ProductFeedback.objects.filter(id=feedback_id).first()\n    if not (feedback_id and feedback):\n        return HttpResponse(status=422)\n\n    feedback.delete()\n    return HttpResponse('Feedback with id={} was deleted.'.format(feedback_id))\n\n\nclass ProductsWithoutImages(catalog.ProductsWithoutImages):\n    model = models.Product\n\n\nclass ProductsWithoutText(catalog.ProductsWithoutText):\n    model = models.Product\n", "path": "shopelectro/views/catalog.py"}]}