Repository: fidals/shopelectro

Issue:
tests_views.py:190-193: Fix doubled tags issue....
The puzzle `398-023fcade` from #398 has to be resolved:

https://github.com/fidals/shopelectro/blob/6806692e83bba09f669baf022ed379e56d7e0b4d/shopelectro/tests/tests_views.py#L190-L193

The puzzle was created by duker33 on 17-Aug-18. 

Estimate: 60 minutes,     

If you have any technical questions, don't ask me, submit new tickets instead. The task will be "done" when the problem is fixed and the text of the puzzle is _removed_ from the source code. Here is more about [PDD](http://www.yegor256.com/2009/03/04/pdd.html) and [about me](http://www.yegor256.com/2017/04/05/pdd-in-action.html).
.drone.yml:124-128: Repair docker-up pipeline Seems we...
The puzzle `408-5cad790a` from #408 has to be resolved:

https://github.com/fidals/shopelectro/blob/bb8eb28f8caadf88972333884ef98636a97c108d/.drone.yml#L124-L128

The puzzle was created by Artemiy on 15-Aug-18. 

Estimate: 60 minutes,     

If you have any technical questions, don't ask me, submit new tickets instead. The task will be "done" when the problem is fixed and the text of the puzzle is _removed_ from the source code. Here is more about [PDD](http://www.yegor256.com/2009/03/04/pdd.html) and [about me](http://www.yegor256.com/2017/04/05/pdd-in-action.html).


Discussion:
@artemiyds2 @duker33 please pay attention to this new issue.
thought about the decisions some time. There are two ones:
- create tag urls with tag group name before
- create hashes only for repeated tag names

The second one is the most productive, i think. It's short and simple.
I'll describe it with test and pdd subtask
@artemiyds2 @duker33 please pay attention to this new issue.

Resolving patch:
diff --git a/shopelectro/models.py b/shopelectro/models.py
--- a/shopelectro/models.py
+++ b/shopelectro/models.py
@@ -1,3 +1,5 @@
+import random
+import string
 from itertools import chain, groupby
 from operator import attrgetter
 from typing import List, Tuple
@@ -22,6 +24,13 @@
 from pages.models import CustomPage, ModelPage, Page, SyncPageMixin, PageManager
 
 
+def randomize_slug(slug: str) -> str:
+    slug_hash = ''.join(
+        random.choices(string.ascii_lowercase, k=settings.SLUG_HASH_SIZE)
+    )
+    return f'{slug}_{slug_hash}'
+
+
 class SECategoryQuerySet(TreeQuerySet):
     def get_categories_tree_with_pictures(self) -> 'SECategoryQuerySet':
         categories_with_pictures = (
@@ -213,6 +222,8 @@ def __str__(self):
 
 class TagQuerySet(models.QuerySet):
 
+    SLUG_HASH_SIZE = 5
+
     def get_group_tags_pairs(self) -> List[Tuple[TagGroup, List['Tag']]]:
         ordering = settings.TAGS_ORDER
         distinct = [order.lstrip('-') for order in ordering]
@@ -247,6 +258,10 @@ def get_group_tags_pairs(self):
 
 class Tag(models.Model):
 
+    # Uncomment it after moving to refarm with rf#162
+    # class Meta:
+    #     unique_together = ('name', 'group')
+
     objects = TagManager()
 
     uuid = models.UUIDField(default=uuid4, editable=False)
@@ -256,6 +271,7 @@ class Tag(models.Model):
         default=0, blank=True, db_index=True, verbose_name=_('position'),
     )
 
+    # Set it as unique with rf#162
     slug = models.SlugField(default='')
 
     group = models.ForeignKey(
@@ -271,6 +287,9 @@ def save(self, *args, **kwargs):
             self.slug = slugify(
                 unidecode(self.name.replace('.', '-').replace('+', '-'))
             )
+        doubled_tag_qs = self.__class__.objects.filter(slug=self.slug)
+        if doubled_tag_qs:
+            self.slug = randomize_slug(self.slug)
         super(Tag, self).save(*args, **kwargs)
 
     @staticmethod
diff --git a/shopelectro/settings/base.py b/shopelectro/settings/base.py
--- a/shopelectro/settings/base.py
+++ b/shopelectro/settings/base.py
@@ -479,3 +479,6 @@ def get_robots_content():
     'wholesale_medium': 50000,
     'wholesale_small': 20000,
 }
+
+# random string to append to doubled slugs
+SLUG_HASH_SIZE = 5
diff --git a/shopelectro/urls.py b/shopelectro/urls.py
--- a/shopelectro/urls.py
+++ b/shopelectro/urls.py
@@ -51,18 +51,18 @@ def cache_page(arg):  # Ignore PyFlakesBear
     # "category" group
     url(r'^categories/(?P<slug>[\w-]+)/$',
         cached_2h(views.CategoryPage.as_view()), name='category'),
-    url(r'^categories/(?P<slug>[\w-]+)/tags/(?P<tags>[\w-]+)/$',
+    url(r'^categories/(?P<slug>[\w-]+)/tags/(?P<tags>[\w_-]+)/$',
         cached_2h(views.CategoryPage.as_view()), name='category'),
     url(r'^categories/(?P<slug>[\w-]+)/(?P<sorting>[0-9]*)/$',
         views.CategoryPage.as_view(), name='category'),
-    url(r'^categories/(?P<slug>[\w-]+)/(?P<sorting>[0-9]*)/tags/(?P<tags>[\w-]+)/$',
+    url(r'^categories/(?P<slug>[\w-]+)/(?P<sorting>[0-9]*)/tags/(?P<tags>[\w_-]+)/$',
         views.CategoryPage.as_view(), name='category'),
     # "load more" group
     url(r'categories/(?P<category_slug>[\w-]+)/load-more/'
         r'(?P<offset>[0-9]+)/(?P<sorting>[0-9]*)/$',
         views.load_more, name='load_more'),
     url(r'categories/(?P<category_slug>[\w-]+)/load-more/'
-        r'(?P<offset>[0-9]+)/(?P<sorting>[0-9]*)/tags/(?P<tags>[\w-]+)/$',
+        r'(?P<offset>[0-9]+)/(?P<sorting>[0-9]*)/tags/(?P<tags>[\w_-]+)/$',
         views.load_more, name='load_more'),
     # rest of urls
     url(r'^no-images/$', views.ProductsWithoutImages.as_view(),


Test patch:
diff --git a/shopelectro/tests/tests_commands.py b/shopelectro/tests/tests_commands.py
--- a/shopelectro/tests/tests_commands.py
+++ b/shopelectro/tests/tests_commands.py
@@ -29,6 +29,15 @@
 """
 
 
+def get_tag_as_dict(group: str, tag: str):
+    return {
+        str(uuid.uuid4()): {
+            'name': group,
+            'tags': {uuid.uuid4(): {'name': tag}}
+        }
+    }
+
+
 class UpdateProducts(TestCase):
 
     @classmethod
@@ -149,6 +158,7 @@ def tearDownClass(cls):
 
     @classmethod
     def call_command_patched(cls, name):
+        """Patch with test constants and call."""
         with mock.patch(
             'shopelectro.management.commands.price.Command.IGNORED_CATEGORIES_BY_TARGET',
             new_callable=mock.PropertyMock
diff --git a/shopelectro/tests/tests_models.py b/shopelectro/tests/tests_models.py
--- a/shopelectro/tests/tests_models.py
+++ b/shopelectro/tests/tests_models.py
@@ -1,7 +1,7 @@
 from django.forms.models import model_to_dict
 from django.test import TestCase
 
-from shopelectro.models import Product
+from shopelectro.models import Product, Tag, TagGroup
 
 
 class ProductModel(TestCase):
@@ -21,3 +21,24 @@ def test_creation_deactivated_product(self):
             ))
         except Exception as error:
             self.fail(f'Creation of existing product failed: {{ error }}')
+
+
+class TagTest(TestCase):
+
+    fixtures = ['dump.json']
+
+    def test_double_named_tag_saving(self):
+        """Two tags with the same name should have unique slugs."""
+        def save_doubled_tag(tag_from_):
+            group_to = TagGroup.objects.exclude(id=tag_from_.group.id).first()
+            tag_to = Tag(
+                group=group_to, name=tag_from_.name, position=tag_from_.position
+            )
+            # required to create `tag.products` field
+            tag_to.save()
+            tag_to.products.set(tag_from.products.all())
+            tag_to.save()
+            return tag_to
+        tag_from = Tag.objects.first()
+        tag_to = save_doubled_tag(tag_from)
+        self.assertNotEqual(tag_from.slug, tag_to.slug)
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
@@ -5,7 +5,6 @@
 They all should be using Django's TestClient.
 """
 import json
-import unittest
 from functools import partial
 from itertools import chain
 from operator import attrgetter
@@ -77,12 +76,18 @@ def get_category_page(
 
 class CatalogTags(BaseCatalogTestCase):
 
+    # @todo #522:15m Move method `CatalogTags.create_doubled_tag` to test helpers.
+    #  And rm code doubling at
+    # `shopelectro.tests.tests_models.TagTest#test_double_named_tag_creation`.
     def create_doubled_tag(self):
         tag_from = Tag.objects.first()
         group_to = TagGroup.objects.exclude(id=tag_from.group.id).first()
-        return Tag.objects.create(
+        tag_to = Tag.objects.create(
             group=group_to, name=tag_from.name, position=tag_from.position
         )
+        tag_to.products.set(tag_from.products.all())
+        tag_to.save()
+        return tag_to
 
     def test_category_page_contains_all_tags(self):
         """Category contains all Product's tags."""
@@ -191,11 +196,6 @@ class instance.
         tag_names = ', '.join([t.name for t in tags])
         self.assertContains(response, tag_names)
 
-    # @todo #398:60m Fix doubled tags issue.
-    #  CategoryTagsPage processes doubled tag in bad way.
-    #  Doubled tags - tags with the same name, but from different tag groups.
-    #  See details in test below.
-    @unittest.expectedFailure
     def test_doubled_tag(self):
         """Category tags page filtered by the same tag from different tag groups."""
         tag_ = self.create_doubled_tag()