admin: add export/import for categories (#59628) #558

Merged
fpeters merged 1 commits from wip/59628-categories-export-import into main 2023-08-11 00:31:01 +02:00
4 changed files with 134 additions and 6 deletions

View File

@ -1,6 +1,10 @@
import pytest
import io
import xml.etree.ElementTree as ET
from wcs.categories import Category
import pytest
from webtest import Upload
from wcs.categories import CardDefCategory, Category
from wcs.formdef import FormDef
from wcs.qommon.http_request import HTTPRequest
@ -241,3 +245,61 @@ def test_categories_edit_roles(pub):
resp = app.get('/backoffice/forms/categories/1/edit')
assert resp.form['export_roles$element0'].value == role_a.id
def test_categories_export(pub):
Category.wipe()
category = Category(name='foobar')
category.store()
app = login(get_app(pub))
resp = app.get('/backoffice/forms/categories/1/')
resp = resp.click('Export')
xml_export = resp.text
xml_export_fd = io.StringIO(xml_export)
imported_category = Category.import_from_xml(xml_export_fd)
assert imported_category.name == category.name
def test_categories_import(pub):
app = login(get_app(pub))
Category.wipe()
category = Category(name='foobar')
category.store()
category_xml = ET.tostring(category.export_to_xml(include_id=True))
Category.wipe()
CardDefCategory.wipe()
# import to wrong category kind
resp = app.get('/backoffice/cards/categories/')
resp = resp.click(href='import')
resp.forms[0]['file'] = Upload('category.wcs', category_xml)
resp = resp.forms[0].submit()
assert 'Invalid File' in resp.text
assert Category.count() == 0
assert CardDefCategory.count() == 0
# successful import
resp = app.get('/backoffice/forms/categories/')
resp = resp.click(href='import')
resp.forms[0]['file'] = Upload('category.wcs', category_xml)
resp = resp.forms[0].submit()
assert Category.count() == 1
assert {x.slug for x in Category.select()} == {'foobar'}
# repeat import -> slug change
resp = app.get('/backoffice/forms/categories/')
resp = resp.click(href='import')
resp.forms[0]['file'] = Upload('category.wcs', category_xml)
resp = resp.forms[0].submit()
assert Category.count() == 2
assert {x.slug for x in Category.select()} == {'foobar', 'foobar-2'}
# cancel
resp = app.get('/backoffice/forms/categories/')
resp = resp.click(href='import')
resp.forms[0]['file'] = Upload('category.wcs', category_xml)
resp = resp.forms[0].submit('cancel')
assert Category.count() == 2

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
from quixote import get_publisher, get_request, get_response, redirect
from quixote import get_publisher, get_request, get_response, get_session, redirect
from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
@ -38,7 +38,15 @@ from wcs.formdef import FormDef
from wcs.mail_templates import MailTemplate
from wcs.qommon import _, misc, template
from wcs.qommon.errors import AccessForbiddenError, TraversalError
from wcs.qommon.form import Form, HtmlWidget, SingleSelectWidget, StringWidget, WidgetList, WysiwygTextWidget
from wcs.qommon.form import (
FileWidget,
Form,
HtmlWidget,
SingleSelectWidget,
StringWidget,
WidgetList,
WysiwygTextWidget,
)
from wcs.workflows import Workflow
@ -198,6 +206,7 @@ class CategoryPage(Directory):
_q_exports = [
'',
'edit',
'export',
'delete',
'description',
('history', 'snapshots_dir'),
@ -301,6 +310,13 @@ class CategoryPage(Directory):
r += form.render()
return r.getvalue()
def export(self):
return misc.xml_response(
self.category,
filename=f'category-{self.category.slug}.wcs',
content_type='text/xml',
)
class CardDefCategoryPage(CategoryPage):
category_class = CardDefCategory
@ -358,7 +374,7 @@ class DataSourceCategoryPage(CategoryPage):
class CategoriesDirectory(Directory):
_q_exports = ['', 'new', 'update_order', ('application', 'applications_dir')]
_q_exports = ['', 'new', ('import', 'p_import'), 'update_order', ('application', 'applications_dir')]
base_section = 'forms'
category_class = Category
@ -425,6 +441,51 @@ class CategoriesDirectory(Directory):
r += form.render()
return r.getvalue()
def p_import(self):
form = Form(enctype='multipart/form-data')
import_title = _('Import category')
form.add(FileWidget, 'file', title=_('File'), required=True)
form.add_submit('submit', import_title)
form.add_submit('cancel', _('Cancel'))
if form.get_submit() == 'cancel':
return redirect('.')
if form.is_submitted() and not form.has_errors():
try:
return self.import_submit(form)
except ValueError:
pass
get_response().breadcrumb.append(('import', _('Import')))
get_response().set_title(import_title)
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % import_title
r += htmltext('<p>%s</p>') % _('You can install a new category by uploading a file.')
r += form.render()
return r.getvalue()
def import_submit(self, form):
fp = form.get_widget('file').parse().fp
try:
category = self.category_class.import_from_xml(fp)
get_session().message = ('info', _('This category has been successfully imported.'))
except ValueError as e:
form.set_error('file', _('Invalid File'))
raise e
try:
# check slug unicity
self.category_class.get_by_slug(category.slug)
except KeyError:
pass
else:
category.url_name = None # a new one will be set in .store()
category.store()
return redirect('%s/' % category.id)
def _q_lookup(self, component):
return self.category_page_class(component)

View File

@ -6,6 +6,7 @@
{% block sidebar-content %}
<h3>{% trans "Actions" %}</h3>
<a class="button button-paragraph" rel="popup" href="new">{% trans "New Category" %}</a>
<a class="button button-paragraph" rel="popup" href="import">{% trans "Import" %}</a>
{% include 'wcs/backoffice/includes/applications.html' %}
{% endblock %}

View File

@ -5,7 +5,11 @@
{% block appbar-actions %}
{% if not category.is_readonly %}
<a href="delete" rel="popup">{% trans "Delete" %}</a>
<a class="extra-actions-menu-opener"></a>
<ul class="extra-actions-menu">
<li><a href="export">{% trans "Export" %}</a></li>
<li><a href="delete" rel="popup">{% trans "Delete" %}</a></li>
</ul>
<a href="edit">{% trans "Edit" %}</a>
{% endif %}
{% endblock %}