diff --git a/tests/backoffice_pages/test_carddata.py b/tests/backoffice_pages/test_carddata.py index c40e7a8a4..f0eaaade4 100644 --- a/tests/backoffice_pages/test_carddata.py +++ b/tests/backoffice_pages/test_carddata.py @@ -650,3 +650,112 @@ def test_carddata_edit_user_selection(pub): assert 'Associated User' in resp assert carddef.data_class().get(carddata.id).user_id == str(user.id) assert '/user-pending-forms' not in resp.text + + +def test_carddata_add_related(pub): + user = create_user(pub) + + BlockDef.wipe() + block = BlockDef() + block.name = 'child' + block.fields = [ + fields.ItemField( + id='1', + label='Child', + type='item', + data_source={'type': 'carddef:child'}, + display_mode='autocomplete', + ), + ] + block.store() + + CardDef.wipe() + family = CardDef() + family.name = 'Family' + family.fields = [ + fields.ItemField( + id='1', + label='RL1', + type='item', + data_source={'type': 'carddef:adult'}, + display_mode='autocomplete', + ), + fields.ItemField( + id='2', + label='RL2', + type='item', + data_source={'type': 'carddef:adult'}, + display_mode='autocomplete', + ), + fields.BlockField(id='3', label='Children', type='block:child', max_items=42), + ] + family.backoffice_submission_roles = user.roles + family.workflow_roles = {'_editor': user.roles[0]} + family.store() + family.data_class().wipe() + + adult = CardDef() + adult.name = 'Adult' + adult.fields = [ + fields.ItemField( + id='1', + label='First name', + type='string', + ), + fields.ItemField( + id='2', + label='Last name', + type='string', + ), + ] + adult.backoffice_submission_roles = user.roles + adult.workflow_roles = {'_editor': user.roles[0]} + adult.store() + adult.data_class().wipe() + + child = CardDef() + child.name = 'Child' + child.fields = [ + fields.ItemField( + id='1', + label='First name', + type='string', + ), + fields.ItemField( + id='2', + label='Last name', + type='string', + ), + ] + child.backoffice_submission_roles = user.roles + child.workflow_roles = {'_editor': user.roles[0]} + child.store() + child.data_class().wipe() + + app = login(get_app(pub)) + resp = app.get('/backoffice/data/family/add/') + assert 'Add another RL1' in resp + assert 'Add another RL2' in resp + assert 'Add another Child' in resp + assert resp.text.count('/backoffice/data/adult/add/?_popup=1') == 2 + assert '/backoffice/data/child/add/?_popup=1' in resp + + # no autocompletion for RL1 + family.fields[0].display_mode = [] + family.store() + resp = app.get('/backoffice/data/family/add/') + assert 'Add another RL1' not in resp + assert 'Add another RL2' in resp + assert 'Add another Child' in resp + assert resp.text.count('/backoffice/data/adult/add/?_popup=1') == 1 + assert '/backoffice/data/child/add/?_popup=1' in resp + + # user ha no creation rights on child + child.backoffice_submission_roles = None + child.store() + resp = app.get('/backoffice/data/family/add/') + assert 'Add another RL1' not in resp + assert 'Add another RL2' in resp + assert 'Add another Child' not in resp + assert resp.text.count('/backoffice/data/adult/add/?_popup=1') == 1 + assert '/backoffice/data/child/add/?_popup=1' not in resp diff --git a/wcs/api.py b/wcs/api.py index ae1f999ca..07063e7d0 100644 --- a/wcs/api.py +++ b/wcs/api.py @@ -280,7 +280,7 @@ class ApiCardPage(ApiFormPageMixin, BackofficeCardPage): json_input = get_request().json formdata = self.formdef.data_class()() - if not (user and self.can_user_add_cards()): + if not (user and self.formdef.can_user_add_cards(user)): raise AccessForbiddenError('cannot create card') if 'data' in json_input: @@ -337,7 +337,7 @@ class ApiCardPage(ApiFormPageMixin, BackofficeCardPage): if get_request().get_method() != 'PUT': raise MethodNotAllowedError(allowed_methods=['PUT']) get_request()._user = get_user_from_api_query_string() - if not (get_request()._user and self.can_user_add_cards()): + if not (get_request()._user and self.formdef.can_user_add_cards(get_request()._user)): raise AccessForbiddenError('cannot import cards') afterjob = bool(get_request().form.get('async') == 'on') diff --git a/wcs/backoffice/data_management.py b/wcs/backoffice/data_management.py index 62623aa78..746b62d96 100644 --- a/wcs/backoffice/data_management.py +++ b/wcs/backoffice/data_management.py @@ -17,6 +17,7 @@ import csv import datetime import io +import json from quixote import get_publisher, get_request, get_response, redirect from quixote.html import htmltext @@ -29,7 +30,8 @@ from ..qommon import N_, _, errors, template from ..qommon.afterjobs import AfterJob from ..qommon.backoffice.menu import html_top from ..qommon.form import FileWidget, Form -from .management import FormBackOfficeStatusPage, FormFillPage, FormPage, ManagementDirectory +from .management import FormBackOfficeStatusPage, FormPage, ManagementDirectory +from .submission import FormFillPage class DataManagementDirectory(ManagementDirectory): @@ -102,16 +104,8 @@ class CardPage(FormPage): def add(self): return CardFillPage(self.formdef.url_name) - def can_user_add_cards(self): - if not self.formdef.backoffice_submission_roles: - return False - for role in get_request().user.get_roles(): - if role in self.formdef.backoffice_submission_roles: - return True - return False - def listing_top_actions(self): - if not self.can_user_add_cards(): + if not self.formdef.can_user_add_cards(get_request().user): return '' return htmltext('%s') % _('Add') @@ -135,7 +129,7 @@ class CardPage(FormPage): def get_formdata_sidebar_actions(self, qs=''): r = super().get_formdata_sidebar_actions(qs=qs) - if self.can_user_add_cards(): + if self.formdef.can_user_add_cards(get_request().user): r += htmltext('
  • %s
  • ') % _( 'Import data from a CSV file' ) @@ -190,7 +184,7 @@ class CardPage(FormPage): return output.getvalue() def import_csv(self): - if not self.can_user_add_cards(): + if not self.formdef.can_user_add_cards(get_request().user): raise errors.AccessForbiddenError() context = {'required_fields': []} @@ -308,10 +302,29 @@ class CardFillPage(FormFillPage): if self.formdef.user_support == 'optional': self.has_user_support = True - def submitted(self, form, *args): - super().submitted(form, *args) + def redirect_after_submitted(self, form, filled): + if get_request().form.get('_popup'): + popup_response_data = json.dumps( + { + 'value': str(filled.id), + 'obj': str(filled.digest), + } + ) + return template.QommonTemplateResponse( + templates=['wcs/backoffice/popup_response.html'], + context={'popup_response_data': popup_response_data}, + is_django_native=True, + ) + result = super().redirect_after_submitted(form, filled) if get_response().get_header('location').endswith('/backoffice/submission/'): return redirect('..') + return result + + def create_form(self, *args, **kwargs): + form = super().create_form(*args, **kwargs) + if get_request().form.get('_popup'): + form.add_hidden('_popup', 1) + return form class CardBackOfficeStatusPage(FormBackOfficeStatusPage): diff --git a/wcs/backoffice/submission.py b/wcs/backoffice/submission.py index f7206ba96..41e71ae29 100644 --- a/wcs/backoffice/submission.py +++ b/wcs/backoffice/submission.py @@ -97,6 +97,7 @@ class FormFillPage(PublicFormFillPage): ] filling_templates = ['wcs/formdata_filling.html'] + popup_filling_templates = ['wcs/formdata_popup_filling.html'] validation_templates = ['wcs/formdata_validation.html'] steps_templates = ['wcs/formdata_steps.html'] has_channel_support = True @@ -307,7 +308,7 @@ class FormFillPage(PublicFormFillPage): get_response().filter['sidebar'] = self.get_sidebar(data) r += htmltext('
    ') r += htmltext('

    %s

    ') % self.formdef.name - if not self.edit_mode: + if not self.edit_mode and not getattr(self, 'is_popup', False): draft_formdata_id = data.get('draft_formdata_id') if draft_formdata_id: r += htmltext('%s') % ( @@ -340,7 +341,9 @@ class FormFillPage(PublicFormFillPage): self.set_tracking_code(filled) get_session().remove_magictoken(get_request().form.get('magictoken')) self.clean_submission_context() + return self.redirect_after_submitted(form, filled) + def redirect_after_submitted(self, form, filled): url = filled.perform_workflow() if url: pass # always redirect to an URL the workflow returned diff --git a/wcs/carddef.py b/wcs/carddef.py index 18776051e..5cd8e90a8 100644 --- a/wcs/carddef.py +++ b/wcs/carddef.py @@ -142,6 +142,14 @@ class CardDef(FormDef): base_url = get_publisher().get_frontoffice_url() return '%s/api/cards/%s/' % (base_url, self.url_name) + def can_user_add_cards(self, user): + if not self.backoffice_submission_roles: + return False + for role in user.get_roles(): + if role in self.backoffice_submission_roles: + return True + return False + def store(self, comment=None): self.roles = self.backoffice_submission_roles return super().store(comment=comment) diff --git a/wcs/fields.py b/wcs/fields.py index eff12e737..dbe7e72f0 100644 --- a/wcs/fields.py +++ b/wcs/fields.py @@ -1866,12 +1866,27 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin): return self.display_mode + def get_carddef(self): + from wcs.carddef import CardDef + + try: + return CardDef.get_by_urlname(self.data_source['type'][8:]) + except KeyError: + return None + def perform_more_widget_changes(self, form, kwargs, edit=True): data_source = data_sources.get_object(self.data_source) display_mode = self.get_display_mode(data_source) if display_mode == 'autocomplete' and data_source and data_source.can_jsonp(): self.url = kwargs['url'] = data_source.get_jsonp_url() + carddef = self.get_carddef() + if ( + get_request().is_in_backoffice() + and carddef + and carddef.can_user_add_cards(get_request().user) + ): + kwargs['add_related_url'] = carddef.get_backoffice_submission_url() self.widget_class = JsonpSingleSelectWidget return @@ -1933,10 +1948,10 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin): and self.data_source.get('type', '').startswith('carddef:') ): return value - from wcs.carddef import CardDef - + carddef = self.get_carddef() + if not carddef: + return value try: - carddef = CardDef.get_by_urlname(self.data_source['type'][8:]) carddata = carddef.data_class().get(value_id) except KeyError: return value diff --git a/wcs/forms/root.py b/wcs/forms/root.py index 41a8099c7..3ad3cd743 100644 --- a/wcs/forms/root.py +++ b/wcs/forms/root.py @@ -554,6 +554,8 @@ class FormPage(Directory, FormTemplateMixin): self.formdef.set_live_condition_sources(form, displayed_fields) + self.is_popup = form._names.get('_popup') + if had_prefill: # pass over prefilled fields that are used as live source of item # fields @@ -576,10 +578,11 @@ class FormPage(Directory, FormTemplateMixin): if page: form.add_hidden('page_id', page.id) - cancel_label = _('Cancel') - if self.has_draft_support() and not (data and data.get('is_recalled_draft')): - cancel_label = _('Discard') - form.add_submit('cancel', cancel_label, css_class='cancel') + if not self.is_popup: + cancel_label = _('Cancel') + if self.has_draft_support() and not (data and data.get('is_recalled_draft')): + cancel_label = _('Discard') + form.add_submit('cancel', cancel_label, css_class='cancel') if self.has_draft_support(): form.add_submit( @@ -595,7 +598,6 @@ class FormPage(Directory, FormTemplateMixin): context = { 'view': self, 'page_no': lambda: self.get_current_page_no(page), - 'form': form, 'formdef': LazyFormDef(self.formdef), 'form_side': lambda: self.form_side(0, page, data=data, magictoken=magictoken), 'steps': lambda: self.step(0, page), @@ -604,6 +606,16 @@ class FormPage(Directory, FormTemplateMixin): context['tracking_code_box'] = lambda: self.tracking_code_box(data, magictoken) self.modify_filling_context(context, page, data) + if self.is_popup: + context['form_obj'] = form + return template.QommonTemplateResponse( + templates=list(self.get_formdef_template_variants(self.popup_filling_templates)), + context=context, + is_django_native=True, + ) + else: + context['form'] = form + return template.QommonTemplateResponse( templates=list(self.get_formdef_template_variants(self.filling_templates)), context=context ) diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py index ea52a57f3..7617514a9 100644 --- a/wcs/qommon/form.py +++ b/wcs/qommon/form.py @@ -2289,9 +2289,10 @@ class RankedItemsWidget(CompositeWidget): class JsonpSingleSelectWidget(Widget): template_name = 'qommon/forms/widgets/select_jsonp.html' - def __init__(self, name, value=None, url=None, **kwargs): + def __init__(self, name, value=None, url=None, add_related_url=None, **kwargs): super().__init__(name, value=value, **kwargs) self.url = url + self.add_related_url = add_related_url def add_media(self): get_response().add_javascript(['select2.js']) diff --git a/wcs/qommon/static/css/qommon.scss b/wcs/qommon/static/css/qommon.scss index 68dbb6e0c..4ab457962 100644 --- a/wcs/qommon/static/css/qommon.scss +++ b/wcs/qommon/static/css/qommon.scss @@ -1,3 +1,10 @@ +body.no-header #header { + display: none; +} +body.no-footer #footer { + display: none; +} + a { color: #028; } diff --git a/wcs/qommon/static/js/popup_response.js b/wcs/qommon/static/js/popup_response.js new file mode 100644 index 000000000..b3f78649d --- /dev/null +++ b/wcs/qommon/static/js/popup_response.js @@ -0,0 +1,4 @@ +(function() { + var initData = JSON.parse(document.getElementById('popup-response-constants').dataset.popupResponse); + opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj); +})(); diff --git a/wcs/qommon/static/js/qommon.admin.js b/wcs/qommon/static/js/qommon.admin.js index 4da8cd215..bcde6674f 100644 --- a/wcs/qommon/static/js/qommon.admin.js +++ b/wcs/qommon/static/js/qommon.admin.js @@ -293,4 +293,53 @@ $(function() { } }); $('[type=radio][name=display_mode]:checked').trigger('change'); + + // IE doesn't accept periods or dashes in the window name, but the element IDs + // we use to generate popup window names may contain them, therefore we map them + // to allowed characters in a reversible way so that we can locate the correct + // element when the popup window is dismissed. + function id_to_windowname(text) { + text = text.replace(/\./g, '__dot__'); + text = text.replace(/\-/g, '__dash__'); + return text; + } + + function windowname_to_id(text) { + text = text.replace(/__dot__/g, '.'); + text = text.replace(/__dash__/g, '-'); + return text; + } + + function showAddRelatedObjectPopup(triggeringLink) { + var name = triggeringLink.id.replace(/^add_/, ''); + name = id_to_windowname(name); + console.log(name) + var href = triggeringLink.href; + console.log(href) + var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); + win.focus(); + return false; + } + + function dismissAddRelatedObjectPopup(win, newId, newRepr) { + var name = windowname_to_id(win.name); + var elem = document.getElementById(name); + if (elem) { + var elemName = elem.nodeName.toUpperCase(); + if (elemName === 'SELECT') { + elem.options[elem.options.length] = new Option(newRepr, newId, true, true); + } + // Trigger a change event to update related links if required. + $(elem).trigger('change'); + } + win.close(); + } + window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup; + + $('body').on('click', '.add-related', function(e) { + e.preventDefault(); + if (this.href) { + showAddRelatedObjectPopup(this); + } + }); }); diff --git a/wcs/qommon/templates/qommon/forms/widgets/select_jsonp.html b/wcs/qommon/templates/qommon/forms/widgets/select_jsonp.html index 3913a0c6b..d28b75f9c 100644 --- a/wcs/qommon/templates/qommon/forms/widgets/select_jsonp.html +++ b/wcs/qommon/templates/qommon/forms/widgets/select_jsonp.html @@ -1,4 +1,5 @@ {% extends "qommon/forms/widget.html" %} +{% load i18n %} {% block widget-control %} +{% if widget.add_related_url %} ++ +{% endif %} {% endblock %} diff --git a/wcs/templates/wcs/backoffice/popup_response.html b/wcs/templates/wcs/backoffice/popup_response.html new file mode 100644 index 000000000..114057a7e --- /dev/null +++ b/wcs/templates/wcs/backoffice/popup_response.html @@ -0,0 +1,11 @@ +{% load i18n static %} + + {% trans 'Popup closing...' %} + + + + diff --git a/wcs/templates/wcs/formdata_popup_filling.html b/wcs/templates/wcs/formdata_popup_filling.html new file mode 100644 index 000000000..b16c410a7 --- /dev/null +++ b/wcs/templates/wcs/formdata_popup_filling.html @@ -0,0 +1,17 @@ +{% extends "wcs/backoffice.html" %} + +{% block bodyargs %}class="no-header no-footer"{% endblock %} +{% block site-header %}{% endblock %} +{% block user-links %}{% endblock %} +{% block sidepage %}{% endblock %} + +{% block main-content %} +{% block form-side %} +{{ form_side|default:"" }} +{{ publisher.get_request.session.display_message|safe }} +{% endblock %} + +{{ form_obj.render|safe }} +{% endblock %} + +{% block footer %}{% endblock %} diff --git a/wcs/views.py b/wcs/views.py index efac6bc30..87c293fb2 100644 --- a/wcs/views.py +++ b/wcs/views.py @@ -31,12 +31,14 @@ class Backoffice(compat.TemplateWithFallbackView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + _request = None with compat.request(self.request): get_request().response.filter = {'admin_ezt': True} body = get_publisher().try_publish(get_request()) if isinstance(body, template.QommonTemplateResponse): body.add_media() if body.is_django_native: + _request = get_request() self.template_names = body.templates context.update(body.context) else: @@ -46,6 +48,10 @@ class Backoffice(compat.TemplateWithFallbackView): self.quixote_response = get_request().response context.update(template.get_decorate_vars(body, get_response(), generate_breadcrumb=True)) + # restore request for django mode + if _request is not None: + get_publisher()._set_request(_request) + return context def get_template_names(self):