cards: create related card in a popup (#48534)
This commit is contained in:
parent
3dbec9d823
commit
13923c5023
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
body.no-header #header {
|
||||
display: none;
|
||||
}
|
||||
body.no-footer #footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #028;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
(function() {
|
||||
var initData = JSON.parse(document.getElementById('popup-response-constants').dataset.popupResponse);
|
||||
opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj);
|
||||
})();
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue