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):