From 0e14abc58510032fde4f459b657f95fb26acaa02 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 22 Oct 2019 14:58:02 +0200
Subject: [PATCH 2/2] page: add a duplicate action (#24526)

---
 combo/apps/maps/models.py                    |  4 ++
 combo/data/models.py                         | 39 ++++++++++++++++
 combo/manager/templates/combo/page_view.html |  1 +
 combo/manager/urls.py                        |  2 +
 combo/manager/views.py                       | 13 ++++++
 tests/test_manager.py                        | 13 ++++++
 tests/test_maps_cells.py                     | 10 ++++
 tests/test_pages.py                          | 48 +++++++++++++++++++-
 8 files changed, 129 insertions(+), 1 deletion(-)

diff --git a/combo/apps/maps/models.py b/combo/apps/maps/models.py
index 86fed18..82b6825 100644
--- a/combo/apps/maps/models.py
+++ b/combo/apps/maps/models.py
@@ -319,3 +319,7 @@ class Map(CellBase):
         ctx['group_markers'] = self.group_markers
         ctx['marker_behaviour_onclick'] = self.marker_behaviour_onclick
         return ctx
+
+    def duplicate_m2m(self, new_cell):
+        # set layers
+        new_cell.layers.set(self.layers.all())
diff --git a/combo/data/models.py b/combo/data/models.py
index 66b9393..dc864cf 100644
--- a/combo/data/models.py
+++ b/combo/data/models.py
@@ -434,6 +434,27 @@ class Page(models.Model):
         cells = CellBase.get_cells(page_id=self.id)
         return max([self.last_update_timestamp] + [x.last_update_timestamp for x in cells])
 
+    def duplicate(self):
+        # clone current page
+        new_page = copy.deepcopy(self)
+        new_page.pk = None
+        # set title
+        new_page.title = _('Copy of %s') % self.title
+        # reset snapshot
+        new_page.snapshot = None
+        # reset order
+        new_page.order = None
+        # store new page
+        new_page.save()
+
+        # set groups
+        new_page.groups.set(self.groups.all())
+
+        for cell in self.get_cells():
+            cell.duplicate(page_target=new_page)
+
+        return new_page
+
 
 class PageSnapshot(models.Model):
     page = models.ForeignKey(Page, on_delete=models.SET_NULL, null=True)
@@ -735,6 +756,24 @@ class CellBase(six.with_metaclass(CellMeta, models.Model)):
     def import_subobjects(self, cell_json):
         pass
 
+    def duplicate(self, page_target=None):
+        # clone current cell
+        new_cell = copy.deepcopy(self)
+        new_cell.pk = None
+        # set page
+        new_cell.page = page_target or self.page
+        # store new cell
+        new_cell.save()
+
+        # set groups
+        new_cell.groups.set(self.groups.all())
+
+        if hasattr(self, 'duplicate_m2m'):
+            self.duplicate_m2m(new_cell)
+
+        return new_cell
+
+
 @register_cell_class
 class TextCell(CellBase):
     text = RichTextField(_('Text'), blank=True, null=True)
diff --git a/combo/manager/templates/combo/page_view.html b/combo/manager/templates/combo/page_view.html
index 4a7567f..91b9518 100644
--- a/combo/manager/templates/combo/page_view.html
+++ b/combo/manager/templates/combo/page_view.html
@@ -11,6 +11,7 @@
 <ul class="extra-actions-menu">
   <li><a href="{% url 'combo-manager-page-history' pk=object.id %}">{% trans 'history' %}</a></li>
   <li><a download href="{% url 'combo-manager-page-export' pk=object.id %}">{% trans 'export' %}</a></li>
+  <li><a href="{% url 'combo-manager-page-duplicate' pk=object.id %}">{% trans 'duplicate' %}</a></li>
   <li><a rel="popup" href="{% url 'combo-manager-page-delete' pk=object.id %}">{% trans 'delete' %}</a></li>
 </ul>
 </span>
diff --git a/combo/manager/urls.py b/combo/manager/urls.py
index 23c1a8d..1e55585 100644
--- a/combo/manager/urls.py
+++ b/combo/manager/urls.py
@@ -53,6 +53,8 @@ urlpatterns = [
             name='combo-manager-page-delete'),
         url(r'^pages/(?P<pk>\w+)/export$', views.page_export,
             name='combo-manager-page-export'),
+        url(r'^pages/(?P<pk>\w+)/duplicate$', views.page_duplicate,
+            name='combo-manager-page-duplicate'),
         url(r'^pages/(?P<pk>\w+)/history$', views.page_history,
             name='combo-manager-page-history'),
         url(r'^pages/(?P<page_pk>\w+)/history/(?P<pk>\w+)/$', views.snapshot_restore,
diff --git a/combo/manager/views.py b/combo/manager/views.py
index 6b051ba..0a10989 100644
--- a/combo/manager/views.py
+++ b/combo/manager/views.py
@@ -297,6 +297,19 @@ class PageExportView(DetailView):
 page_export = PageExportView.as_view()
 
 
+class PageDuplicateView(RedirectView):
+    permanent = False
+
+    def get_redirect_url(self, pk):
+        page = Page.objects.get(pk=pk)
+        new_page = page.duplicate()
+        messages.info(self.request, _('Page %s has been duplicated.') % page.title)
+        return reverse('combo-manager-page-view', kwargs={'pk': new_page.pk})
+
+
+page_duplicate = PageDuplicateView.as_view()
+
+
 class PageHistoryView(ListView):
     model = PageSnapshot
     template_name = 'combo/page_history.html'
diff --git a/tests/test_manager.py b/tests/test_manager.py
index d5c67aa..7c60e50 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -376,6 +376,19 @@ def test_site_export_import(app, admin_user):
     resp = resp.form.submit()
     assert 'File is not in the expected JSON format.' in resp.text
 
+
+def test_duplicate_page(app, admin_user):
+    page = Page.objects.create(title='One', slug='one', template_name='standard')
+    TextCell.objects.create(page=page, placeholder='content', text='Foobar', order=0)
+
+    app = login(app)
+    resp = app.get('/manage/pages/%s/' % page.pk)
+    resp = resp.click('duplicate')
+    new_page = Page.objects.latest('pk')
+    assert resp.status_int == 302
+    assert resp.location.endswith('/manage/pages/%s/' % new_page.pk)
+
+
 def test_add_edit_cell(app, admin_user):
     Page.objects.all().delete()
     page = Page(title='One', slug='one', template_name='standard')
diff --git a/tests/test_maps_cells.py b/tests/test_maps_cells.py
index d74570a..368b010 100644
--- a/tests/test_maps_cells.py
+++ b/tests/test_maps_cells.py
@@ -392,3 +392,13 @@ def test_get_geojson_properties(app, layer, user):
         features = json.loads(resp.text)['features']
         assert len(features[0]['properties']['display_fields']) == 1
         assert features[0]['properties']['layer']['properties'] == ['id']
+
+
+def test_duplicate(layer):
+    page = Page.objects.create(title='xxx', slug='new', template_name='standard')
+    cell = Map.objects.create(page=page, placeholder='content', order=0, public=True, title='Map')
+    layer.save()
+    cell.layers.add(layer)
+
+    new_cell = cell.duplicate()
+    assert list(new_cell.layers.all()) == [layer]
diff --git a/tests/test_pages.py b/tests/test_pages.py
index b8f40a9..9421a5f 100644
--- a/tests/test_pages.py
+++ b/tests/test_pages.py
@@ -9,7 +9,7 @@ from django.test import override_settings
 from django.test.client import RequestFactory
 from django.utils.six import StringIO
 from django.utils.timezone import now
-from combo.data.models import Page, CellBase, TextCell, LinkCell
+from combo.data.models import Page, PageSnapshot, CellBase, TextCell, LinkCell
 from combo.data.management.commands.import_site import Command as ImportSiteCommand
 from combo.data.management.commands.export_site import Command as ExportSiteCommand
 
@@ -174,6 +174,52 @@ def test_import_export_pages_with_links():
     assert CellBase.get_cells(page_id=new_page_1.id)[0].link_page_id == new_page_2.id
     assert CellBase.get_cells(page_id=new_page_2.id)[0].link_page_id == new_page_1.id
 
+
+def test_duplicate_page():
+    group1 = Group.objects.create(name='foobar')
+    group2 = Group.objects.create(name='fooblah')
+
+    page = Page.objects.create(
+        title='foo',
+        slug='foo',
+        description="Foo's page")
+    page.groups.set([group1, group2])
+    snapshot = PageSnapshot.objects.create(page=page)
+    page.snapshot = snapshot
+    page.save()
+
+    cell1 = TextCell.objects.create(page=page, text='foo1', order=0, placeholder='content')
+    cell1.groups.set([group1, group2])
+    cell2 = TextCell.objects.create(page=page, text='foo2', order=1, placeholder='content')
+
+    new_page = page.duplicate()
+    assert new_page.pk != page.pk
+    assert new_page.title == 'Copy of foo'
+    assert new_page.slug == page.slug
+    assert new_page.description == page.description
+    assert new_page.parent is None
+    assert new_page.snapshot is None
+    assert list(new_page.groups.all()) == [group1, group2]
+    assert len(new_page.get_cells()) == 2
+
+    new_cell1 = TextCell.objects.get(page=new_page, text='foo1')
+    new_cell2 = TextCell.objects.get(page=new_page, text='foo2')
+    assert new_cell1.pk != cell1.pk
+    assert new_cell1.text == cell1.text
+    assert new_cell1.placeholder == cell1.placeholder
+    assert list(new_cell1.groups.all()) == [group1, group2]
+    assert new_cell2.pk != cell2.pk
+    assert new_cell2.text == cell2.text
+    assert new_cell2.placeholder == cell2.placeholder
+    assert list(new_cell2.groups.all()) == []
+
+    parent = Page.objects.create()
+    page.parent = parent
+    page.save()
+    new_page = page.duplicate()
+    assert new_page.parent == parent
+
+
 def test_next_previous():
     Page.objects.all().delete()
     page = Page()
-- 
2.20.1

