cards: create related card in a popup (#48534)

This commit is contained in:
Lauréline Guérin 2021-03-26 09:08:21 +01:00
parent 3dbec9d823
commit 13923c5023
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
15 changed files with 287 additions and 26 deletions

View File

@ -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

View File

@ -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')

View File

@ -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('<span class="actions"><a href="./add/">%s</a></span>') % _('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('<li><a rel="popup" href="import-csv">%s</a></li>') % _(
'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):

View File

@ -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('<div id="appbar">')
r += htmltext('<h2>%s</h2>') % 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('<a rel="popup" href="remove/%s">%s</a>') % (
@ -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

View File

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

View File

@ -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

View File

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

View File

@ -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'])

View File

@ -1,3 +1,10 @@
body.no-header #header {
display: none;
}
body.no-footer #footer {
display: none;
}
a {
color: #028;
}

View File

@ -0,0 +1,4 @@
(function() {
var initData = JSON.parse(document.getElementById('popup-response-constants').dataset.popupResponse);
opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj);
})();

View File

@ -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);
}
});
});

View File

@ -1,4 +1,5 @@
{% extends "qommon/forms/widget.html" %}
{% load i18n %}
{% block widget-control %}
<select id="form_{{widget.name}}" name="{{widget.name}}"
data-select2-url="{{widget.get_select2_url}}"
@ -6,4 +7,9 @@
data-required="{% if widget.is_required %}true{% endif %}"
data-initial-display-value="{{widget.get_display_value|default_if_none:''}}">
</select>
{% if widget.add_related_url %}
<a class="add-related pk-button" id="add_form_{{ widget.name }}"
href="{{ widget.add_related_url }}?_popup=1"
title="{% blocktrans with card=widget.get_title %}Add another {{ card }}{% endblocktrans %}">+</a>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,11 @@
{% load i18n static %}<!DOCTYPE html>
<html>
<head><title>{% trans 'Popup closing...' %}</title></head>
<body>
<script type="text/javascript"
id="popup-response-constants"
src="{% static "/js/popup_response.js" %}"
data-popup-response="{{ popup_response_data }}">
</script>
</body>
</html>

View File

@ -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 %}

View File

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