Repository: mozmeao/snippets-service

Issue:
Add `duplicate` action to ASRSnippets



Discussion:
~~Moving to 2018Q4 milestone since that's not blocking Fx63~~

Attaching 2018Q4 label and removing from milestone since that's not blocking Fx63

Resolving patch:
diff --git a/snippets/base/admin/actions.py b/snippets/base/admin/actions.py
--- a/snippets/base/admin/actions.py
+++ b/snippets/base/admin/actions.py
@@ -4,5 +4,5 @@
 @transaction.atomic
 def duplicate_snippets_action(modeladmin, request, queryset):
     for snippet in queryset:
-        snippet.duplicate()
+        snippet.duplicate(request.user)
 duplicate_snippets_action.short_description = 'Duplicate selected snippets'  # noqa
diff --git a/snippets/base/admin/adminmodels.py b/snippets/base/admin/adminmodels.py
--- a/snippets/base/admin/adminmodels.py
+++ b/snippets/base/admin/adminmodels.py
@@ -14,6 +14,7 @@
 from snippets.base import forms, models
 from snippets.base.models import JINJA_ENV
 from snippets.base.admin.filters import ModifiedFilter, ReleaseFilter
+from snippets.base.admin.actions import duplicate_snippets_action
 
 
 MATCH_LOCALE_REGEX = re.compile('(\w+(?:-\w+)*)')
@@ -146,6 +147,9 @@ class ASRSnippetAdmin(admin.ModelAdmin):
     save_on_top = True
     save_as = True
     view_on_site = False
+    actions = (
+        duplicate_snippets_action,
+    )
 
     fieldsets = (
         ('ID', {'fields': ('creator', 'name', 'status', 'preview_url')}),
diff --git a/snippets/base/models.py b/snippets/base/models.py
--- a/snippets/base/models.py
+++ b/snippets/base/models.py
@@ -197,10 +197,14 @@ def __str__(self):
 class SnippetBaseModel(django_mysql.models.Model):
     uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
 
-    def duplicate(self):
-        snippet_copy = copy.copy(self)
+    def duplicate(self, creator):
+        snippet_copy = copy.deepcopy(self)
         snippet_copy.id = None
+        snippet_copy.created = None
+        snippet_copy.modified = None
+        snippet_copy.creator = creator
         snippet_copy.published = False
+        snippet_copy.ready_for_review = False
         snippet_copy.uuid = uuid.uuid4()
         snippet_copy.name = '{0} - {1}'.format(
             self.name,
@@ -213,7 +217,7 @@ def duplicate(self):
                 manager = attr
                 if manager.__class__.__name__ == 'RelatedManager':
                     for itm in manager.all():
-                        itm_copy = copy.copy(itm)
+                        itm_copy = copy.deepcopy(itm)
                         itm_copy.id = None
                         getattr(snippet_copy, field.name).add(itm_copy)
                 elif manager.__class__.__name__ == 'ManyRelatedManager':
@@ -667,6 +671,34 @@ def get_admin_url(self):
         full_url = urljoin(settings.SITE_URL, url)
         return full_url
 
+    def duplicate(self, creator):
+        snippet_copy = copy.deepcopy(self)
+        snippet_copy.id = None
+        snippet_copy.created = None
+        snippet_copy.modified = None
+        snippet_copy.status = STATUS_CHOICES['Draft']
+        snippet_copy.creator = creator
+        snippet_copy.uuid = uuid.uuid4()
+        snippet_copy.name = '{0} - {1}'.format(
+            self.name,
+            datetime.strftime(datetime.now(), '%Y.%m.%d %H:%M:%S'))
+        snippet_copy.save()
+
+        for field in self._meta.get_fields():
+            attr = getattr(self, field.name)
+            if isinstance(attr, Manager):
+                manager = attr
+                if manager.__class__.__name__ == 'RelatedManager':
+                    for itm in manager.all():
+                        itm_copy = copy.deepcopy(itm)
+                        itm_copy.id = None
+                        getattr(snippet_copy, field.name).add(itm_copy)
+                elif manager.__class__.__name__ == 'ManyRelatedManager':
+                    for snippet in manager.all():
+                        getattr(snippet_copy, field.name).add(snippet)
+
+        return snippet_copy
+
 
 class Addon(models.Model):
     created = models.DateTimeField(auto_now_add=True)


Test patch:
diff --git a/snippets/base/tests/test_models.py b/snippets/base/tests/test_models.py
--- a/snippets/base/tests/test_models.py
+++ b/snippets/base/tests/test_models.py
@@ -6,7 +6,7 @@
 from mock import MagicMock, Mock, patch
 from pyquery import PyQuery as pq
 
-from snippets.base.models import Client, UploadedFile, _generate_filename
+from snippets.base.models import STATUS_CHOICES, Client, UploadedFile, _generate_filename
 from snippets.base.tests import (ASRSnippetFactory,
                                  ClientMatchRuleFactory,
                                  JSONSnippetFactory,
@@ -15,20 +15,26 @@
                                  SnippetTemplateFactory,
                                  SnippetTemplateVariableFactory,
                                  TestCase,
+                                 UserFactory,
                                  UploadedFileFactory)
 
 
 class DuplicateSnippetMixInTests(TestCase):
     def _dup_test(self, snippet):
+        user = UserFactory.create()
         snippet.client_match_rules.add(*ClientMatchRuleFactory.create_batch(3))
-        snippet_copy = snippet.duplicate()
+        snippet_copy = snippet.duplicate(user)
         self.assertEqual(snippet_copy.published, False)
-        self.assertTrue(snippet_copy.id != snippet.id)
+        self.assertNotEqual(snippet_copy.id, snippet.id)
         self.assertEqual(snippet_copy.locales.count(), 1)
         self.assertTrue(snippet_copy.locales.all()[0] == snippet.locales.all()[0])
         self.assertEqual(set(snippet_copy.client_match_rules.all()),
                          set(snippet.client_match_rules.all()))
 
+        # Only Snippet and not JSONSnippet, has creator
+        if hasattr(snippet, 'creator'):
+            self.assertNotEqual(snippet_copy.creator, snippet.creator)
+
     def test_snippet(self):
         snippet = SnippetFactory.create()
         self._dup_test(snippet)
@@ -373,3 +379,17 @@ def test_get_preview_url(self):
         expected_result = 'about:newtab?endpoint=http://example.com'
         expected_result += reverse('asr-preview', kwargs={'uuid': snippet.uuid})
         self.assertEqual(snippet.get_preview_url(), expected_result)
+
+    def test_duplicate(self):
+        user = UserFactory.create()
+        snippet = ASRSnippetFactory.create(
+            status=STATUS_CHOICES['Published'],
+            locales=['en-us', 'fr'],
+        )
+        duplicate_snippet = snippet.duplicate(user)
+
+        for attr in ['id', 'creator', 'created', 'modified', 'name', 'uuid']:
+            self.assertNotEqual(getattr(snippet, attr), getattr(duplicate_snippet, attr))
+
+        self.assertEqual(set(snippet.locales.all()), set(duplicate_snippet.locales.all()))
+        self.assertEqual(duplicate_snippet.status, STATUS_CHOICES['Draft'])