diff --git a/combo/apps/maps/models.py b/combo/apps/maps/models.py
index 86fed184..82b6825d 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 728f0faf..4d086c81 100644
--- a/combo/data/models.py
+++ b/combo/data/models.py
@@ -436,6 +436,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
+ # set order
+ new_page.order = self.order + 1
+ # 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)
@@ -737,6 +758,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 87e7bbe1..fc8dbec2 100644
--- a/combo/manager/templates/combo/page_view.html
+++ b/combo/manager/templates/combo/page_view.html
@@ -12,6 +12,7 @@
{% trans 'history' %}
{% trans 'export' %}
{% trans 'add a child page' %}
+ {% trans 'Duplicate' %}
{% trans 'delete' %}
diff --git a/combo/manager/urls.py b/combo/manager/urls.py
index a99f99ec..b56f79be 100644
--- a/combo/manager/urls.py
+++ b/combo/manager/urls.py
@@ -54,6 +54,8 @@ urlpatterns = [
url(r'^pages/(?P\w+)/export$', views.page_export,
name='combo-manager-page-export'),
url(r'^pages/(?P\w+)/add/$', views.page_add_child, name='combo-manager-page-add-child'),
+ url(r'^pages/(?P\w+)/duplicate$', views.page_duplicate,
+ name='combo-manager-page-duplicate'),
url(r'^pages/(?P\w+)/history$', views.page_history,
name='combo-manager-page-history'),
url(r'^pages/(?P\w+)/history/(?P\w+)/$', views.snapshot_restore,
diff --git a/combo/manager/views.py b/combo/manager/views.py
index bad9199d..c2f375c0 100644
--- a/combo/manager/views.py
+++ b/combo/manager/views.py
@@ -325,6 +325,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 7316612a..1860de32 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -440,6 +440,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 d74570a1..368b010a 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 aa701bd7..77020e4f 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
from combo.manager.forms import PageVisibilityForm
@@ -192,6 +192,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()