manager: add tar format for site export/import (#45128)

This commit is contained in:
Nicolas Roche 2020-07-25 20:54:33 +02:00
parent 0f856f1334
commit c0158d6172
5 changed files with 114 additions and 22 deletions

View File

@ -196,4 +196,8 @@ class CellVisibilityForm(forms.Form):
class SiteImportForm(forms.Form):
site_json = forms.FileField(label=_('Site Export File'))
site_file = forms.FileField(label=_('Site Export File'))
class SiteExportForm(forms.Form):
include_asset = forms.BooleanField(label=_('Include assets into the export'), required=False)

View File

@ -7,7 +7,7 @@
<a class="extra-actions-menu-opener"></a>
<a rel="popup" href="{% url 'combo-manager-page-add' %}">{% trans 'New' %}</a>
<ul class="extra-actions-menu">
<li><a download href="{% url 'combo-manager-site-export' %}">{% trans 'Export Site' %}</a></li>
<li><a href="{% url 'combo-manager-site-export' %}" rel="popup" data-autoclose-dialog="true">{% trans 'Export Site' %}</a></li>
<li><a href="{% url 'combo-manager-site-import' %}">{% trans 'Import Site' %}</a></li>
<li><a href="{% url 'combo-manager-invalid-cell-report' %}">{% trans 'Anomaly report' %}</a></li>
{% for extra_action in extra_actions %}

View File

@ -0,0 +1,17 @@
{% extends "combo/manager_base.html" %}
{% load i18n %}
{% block appbar %}
<h2>{% trans "Site Export" %}</h2>
{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<div class="buttons">
<button>{% trans 'Export' %}</button>
<a class="cancel" href="{% url 'combo-manager-homepage' %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -18,6 +18,7 @@ import hashlib
import datetime
import json
from operator import attrgetter
import tarfile
from django.conf import settings
from django.contrib import messages
@ -30,6 +31,7 @@ from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text, force_bytes
from django.utils.formats import date_format
from django.utils.six import BytesIO
from django.utils.timezone import localtime
from django.views.decorators.csrf import requires_csrf_token
from django.views.generic import (RedirectView, DetailView,
@ -37,10 +39,11 @@ from django.views.generic import (RedirectView, DetailView,
from combo.data.models import Page, CellBase, ParentContentCell, PageSnapshot, LinkListCell
from combo.data.library import get_cell_class
from combo.data.utils import export_site, import_site, MissingGroups
from combo.data.utils import (
export_site, export_site_tar, import_site, import_site_tar, MissingGroups)
from combo import plugins
from .forms import (PageEditTitleForm, PageVisibilityForm, SiteImportForm,
from .forms import (PageEditTitleForm, PageVisibilityForm, SiteImportForm, SiteExportForm,
PageEditRedirectionForm, PageSelectTemplateForm, PageEditSlugForm,
PageEditPictureForm, PageEditIncludeInNavigationForm,
PageEditDescriptionForm, CellVisibilityForm, PageDuplicateForm, PageExportForm)
@ -61,14 +64,23 @@ class HomepageView(ListView):
homepage = HomepageView.as_view()
class SiteExportView(ListView):
model = Page
class SiteExportView(FormView):
form_class = SiteExportForm
template_name = 'combo/site_export.html'
def render_to_response(self, context, **response_kwargs):
response = HttpResponse(content_type='application/json')
json.dump(export_site(), response, indent=2)
def post(self, request, *args, **kwargs):
if request.POST.get('include_asset'):
fd = BytesIO()
export_site_tar(fd)
response = HttpResponse(content=fd.getvalue(), content_type='application/x-tar')
response['Content-Disposition'] = 'attachment; filename="site-export.tar"'
else:
response = HttpResponse(content_type='application/json')
response['Content-Disposition'] = 'attachment; filename="site-export.json"'
json.dump(export_site(), response, indent=2)
return response
site_export = SiteExportView.as_view()
@ -78,16 +90,28 @@ class SiteImportView(FormView):
success_url = reverse_lazy('combo-manager-homepage')
def form_valid(self, form):
fd = self.request.FILES['site_file'].file
try:
json_site = json.loads(force_text(self.request.FILES['site_json'].read()))
except ValueError:
form.add_error('site_json', _('File is not in the expected JSON format.'))
return self.form_invalid(form)
tarfile.open(mode='r', fileobj=fd)
except tarfile.TarError as e:
try:
fd.seek(0)
json_site = json.loads(force_text(fd.read()))
except ValueError:
form.add_error('site_file', _('File is not in the expected TAR or JSON format.'))
return self.form_invalid(form)
else:
format = 'json'
else:
format = 'tar'
fd.seek(0)
try:
import_site(json_site, request=self.request)
if format == 'json':
import_site(json_site, request=self.request)
else:
import_site_tar(fd, request=self.request)
except MissingGroups as e:
form.add_error('site_json', force_text(e))
form.add_error('site_file', force_text(e))
return self.form_invalid(form)
return super(SiteImportView, self).form_valid(form)

View File

@ -6,6 +6,7 @@ import os
import re
import shutil
from django.core.files import File
from django.core.files.storage import default_storage
from django.urls import reverse
from django.conf import settings
@ -608,7 +609,7 @@ def test_export_page_order():
assert ordered_list[3] == page3
def test_site_export_import(app, admin_user):
def test_site_export_import_json(app, admin_user):
Page.objects.all().delete()
page1 = Page(title='One', slug='one', template_name='standard')
page1.save()
@ -631,6 +632,7 @@ def test_site_export_import(app, admin_user):
app = login(app)
resp = app.get('/manage/')
resp = resp.click('Export Site')
resp = resp.form.submit()
assert resp.headers['content-type'] == 'application/json'
site_export = resp.body
@ -639,7 +641,7 @@ def test_site_export_import(app, admin_user):
app = login(app)
resp = app.get('/manage/')
resp = resp.click('Import Site')
resp.form['site_json'] = Upload('site-export.json', site_export, 'application/json')
resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json')
resp = resp.form.submit()
assert Page.objects.count() == 4
assert LinkCell.objects.count() == 2
@ -649,9 +651,52 @@ def test_site_export_import(app, admin_user):
# check with invalid file
resp = app.get('/manage/')
resp = resp.click('Import Site')
resp.form['site_json'] = Upload('site-export.json', b'invalid content', 'application/json')
resp.form['site_file'] = Upload('site-export.json', b'invalid content', 'application/json')
resp = resp.form.submit()
assert 'File is not in the expected JSON format.' in resp.text
assert 'File is not in the expected TAR or JSON format.' in resp.text
def test_site_export_import_tar(app, admin_user):
Page.objects.all().delete()
page1 = Page(title='One', slug='one', template_name='standard')
page1.save()
cell = TextCell(page=page1, placeholder='content', text='Foobar', order=0)
cell.save()
Asset(key='collectivity:banner', asset=File(BytesIO(b'test'), 'test.png')).save()
path = default_storage.path('')
assert open('%s/assets/test.png' % path, 'r').read() == 'test'
app = login(app)
resp = app.get('/manage/')
resp = resp.click('Export Site')
resp.form['include_asset'] = True
resp = resp.form.submit()
assert resp.headers['content-type'] == 'application/x-tar'
site_export = resp.body
Page.objects.all().delete()
Asset.objects.filter(key='collectivity:banner').delete()
assert Page.objects.count() == 0
assert TextCell.objects.count() == 0
assert Asset.objects.filter(key='collectivity:banner').count() == 0
open('%s/assets/test.png' % path, 'w').write('foo')
app = login(app)
resp = app.get('/manage/')
resp = resp.click('Import Site')
resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json')
resp = resp.form.submit()
assert Page.objects.count() == 1
assert TextCell.objects.count() == 1
assert Asset.objects.filter(key='collectivity:banner').count() == 1
assert open('%s/assets/test.png' % path, 'r').read() == 'foo'
os.remove('%s/assets/test.png' % path)
app = login(app)
resp = app.get('/manage/')
resp = resp.click('Import Site')
resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json')
resp = resp.form.submit()
assert open('%s/assets/test.png' % path, 'r').read() == 'test'
def test_site_export_import_missing_group(app, admin_user):
@ -664,6 +709,7 @@ def test_site_export_import_missing_group(app, admin_user):
app = login(app)
resp = app.get('/manage/')
resp = resp.click('Export Site')
resp = resp.form.submit()
assert resp.headers['content-type'] == 'application/json'
site_export = resp.body
@ -673,7 +719,7 @@ def test_site_export_import_missing_group(app, admin_user):
app = login(app)
resp = app.get('/manage/')
resp = resp.click('Import Site')
resp.form['site_json'] = Upload('site-export.json', site_export, 'application/json')
resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json')
resp = resp.form.submit()
assert 'Missing groups: foobar' in resp.text
@ -685,13 +731,14 @@ def test_site_export_import_unknown_parent(app, admin_user):
app = login(app)
resp = app.get('/manage/')
resp = resp.click('Export Site')
resp = resp.form.submit()
payload = json.loads(force_str(resp.body))
payload['pages'][0]['fields']['exclude_from_navigation'] = False
payload['pages'][0]['fields']['parent'] = ['unknown-parent']
resp = app.get('/manage/')
resp = resp.click('Import Site')
resp.form['site_json'] = Upload('site-export.json', force_bytes(json.dumps(payload)), 'application/json')
resp.form['site_file'] = Upload('site-export.json', force_bytes(json.dumps(payload)), 'application/json')
resp = resp.form.submit().follow()
assert 'Unknown parent for page &quot;One&quot;; parent has been reset and page was excluded from navigation.' in resp.text