Repository: fidals/shopelectro

Issue:
Create pagination numbers
Currently we have only `next` and `previous` pagination links
But we should have all links with it's numbers as said at https://github.com/fidals/shopelectro/issues/302

https://www.shopelectro.ru/catalog/categories/izmeritelnye-pribory-211/?page=2

```
      <div class="js-catalog-pagination">
  <span>
    
      <a href="/catalog/categories/izmeritelnye-pribory-211/">
        Предыдущая страница 1
      </a>
    
    
      <a href="/catalog/categories/izmeritelnye-pribory-211/?page=3">
        Следующая страница 3
      </a>
    
  </span>
```


Discussion:
I have faced with DTL limitations, so i am going to increase the estimate to finish with it as soon as possible

Resolving patch:
diff --git a/shopelectro/settings/base.py b/shopelectro/settings/base.py
--- a/shopelectro/settings/base.py
+++ b/shopelectro/settings/base.py
@@ -496,3 +496,8 @@ def get_robots_content():
     'GM': 'gm.yml',
     'SE78': 'se78.yml',
 }
+
+# Number of pagination neighbors shown for page.
+# If PAGINATION_NEIGHBORS = 4 and number of a page = 5,
+# then will be shown neighbors by number: 3, 4, 6, 7
+PAGINATION_NEIGHBORS = 4
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
@@ -123,3 +123,8 @@ def icon_stars(rating=0):
         'half_icons': range(half_icons),
         'empty_icons': range(empty_icons),
     }
+
+
+@register.filter
+def get_item(dictionary, key):
+    return dictionary.get(key)
diff --git a/shopelectro/views/catalog.py b/shopelectro/views/catalog.py
--- a/shopelectro/views/catalog.py
+++ b/shopelectro/views/catalog.py
@@ -26,11 +26,40 @@ def get_products_count(request):
     return PRODUCTS_ON_PAGE_MOB if mobile_view else PRODUCTS_ON_PAGE_PC
 
 
-def get_paginated_page_or_404(objects, per_page, page_number):
-    try:
-        return Paginator(objects, per_page).page(page_number)
-    except InvalidPage:
-        raise http.Http404('Page does not exist')
+# @todo #539:60m Move PaginatorLinks to refarm-site.
+class PaginatorLinks:
+
+    def __init__(self, number, path, paginated: Paginator):
+        self.paginated = paginated
+        self.number = number
+        self.path = path
+
+        self.index = number - 1
+        self.neighbor_bounds = settings.PAGINATION_NEIGHBORS // 2
+        self.neighbor_range = list(self.paginated.page_range)
+
+    def page(self):
+        try:
+            return self.paginated.page(self.number)
+        except InvalidPage:
+            raise http.Http404('Page does not exist')
+
+    def showed_number(self):
+        return self.index * self.paginated.per_page + self.page().object_list.count()
+
+    def _url(self, number):
+        self.paginated.validate_number(number)
+        return self.path if number == 1 else f'{self.path}?page={number}'
+
+    def prev_numbers(self):
+        return self.neighbor_range[:self.index][-self.neighbor_bounds:]
+
+    def next_numbers(self):
+        return self.neighbor_range[self.index + 1:][:self.neighbor_bounds]
+
+    def number_url_map(self):
+        numbers = self.prev_numbers() + self.next_numbers()
+        return {number: self._url(number) for number in numbers}
 
 
 class SortingOption:
@@ -234,18 +263,24 @@ def template_context(page, tag_titles, tags):
         page.get_template_render_context = partial(
             template_context, page, tag_titles, tags)
 
-        paginated_page = get_paginated_page_or_404(all_products, products_on_page, page_number)
+        paginated = PaginatorLinks(
+            page_number,
+            self.request.path,
+            Paginator(all_products, products_on_page)
+        )
+        paginated_page = paginated.page()
+
         total_products = all_products.count()
-        products = paginated_page.object_list
-        if not products:
+        products_on_page = paginated_page.object_list
+        if not products_on_page:
             raise http.Http404('Page without products does not exist.')
 
         return {
             **context,
-            'products_data': merge_products_context(products),
+            'products_data': merge_products_context(products_on_page),
             'group_tags_pairs': group_tags_pairs,
             'total_products': total_products,
-            'products_count': (page_number - 1) * products_on_page + products.count(),
+            'paginated': paginated,
             'paginated_page': paginated_page,
             'sorting_options': settings.CATEGORY_SORTING_OPTIONS.values(),
             'limits': settings.CATEGORY_STEP_MULTIPLIERS,
@@ -301,12 +336,18 @@ def load_more(request, category_slug, offset=0, limit=0, sorting=0, tags=None):
             .distinct(sorting_option.field)
         )
 
-    paginated_page = get_paginated_page_or_404(all_products, products_on_page, page_number)
+    paginated = PaginatorLinks(
+        page_number,
+        request.path,
+        Paginator(all_products, products_on_page)
+    )
+    paginated_page = paginated.page()
     products = paginated_page.object_list
     view = request.session.get('view_type', 'tile')
 
     return render(request, 'catalog/category_products.html', {
         'products_data': merge_products_context(products),
+        'paginated': paginated,
         'paginated_page': paginated_page,
         'view_type': view,
         'prods': products_on_page,


Test patch:
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
@@ -250,10 +250,12 @@ def assert_pagination_links(self, next_, prev, page_number):
             page_number - 1,
         )
 
+    # @todo #539:30m Create test(s) for numbered pagination.
+    #  Currently we test only a prev/next page feature.
     def test_pagination_buttons(self):
         """Each button forward to a previous and a next pagination pages."""
         page_number = 3
-        prev, next_ = BeautifulSoup(
+        prev, *_, next_ = BeautifulSoup(
             self.get_category_page(query_string={'page': page_number}).content.decode('utf-8'),
             'html.parser'
         ).find(class_='js-catalog-pagination').find_all('a')