0001-cards-create-related-card-in-a-popup-48534.patch
tests/backoffice_pages/test_carddata.py | ||
---|---|---|
650 | 650 |
assert 'Associated User' in resp |
651 | 651 |
assert carddef.data_class().get(carddata.id).user_id == str(user.id) |
652 | 652 |
assert '/user-pending-forms' not in resp.text |
653 | ||
654 | ||
655 |
def test_carddata_add_related(pub): |
|
656 |
user = create_user(pub) |
|
657 | ||
658 |
BlockDef.wipe() |
|
659 |
block = BlockDef() |
|
660 |
block.name = 'child' |
|
661 |
block.fields = [ |
|
662 |
fields.ItemField( |
|
663 |
id='1', |
|
664 |
label='Child', |
|
665 |
type='item', |
|
666 |
data_source={'type': 'carddef:child'}, |
|
667 |
display_mode='autocomplete', |
|
668 |
), |
|
669 |
] |
|
670 |
block.store() |
|
671 | ||
672 |
CardDef.wipe() |
|
673 |
family = CardDef() |
|
674 |
family.name = 'Family' |
|
675 |
family.fields = [ |
|
676 |
fields.ItemField( |
|
677 |
id='1', |
|
678 |
label='RL1', |
|
679 |
type='item', |
|
680 |
data_source={'type': 'carddef:adult'}, |
|
681 |
display_mode='autocomplete', |
|
682 |
), |
|
683 |
fields.ItemField( |
|
684 |
id='2', |
|
685 |
label='RL2', |
|
686 |
type='item', |
|
687 |
data_source={'type': 'carddef:adult'}, |
|
688 |
display_mode='autocomplete', |
|
689 |
), |
|
690 |
fields.BlockField(id='3', label='Children', type='block:child', max_items=42), |
|
691 |
] |
|
692 |
family.backoffice_submission_roles = user.roles |
|
693 |
family.workflow_roles = {'_editor': user.roles[0]} |
|
694 |
family.store() |
|
695 |
family.data_class().wipe() |
|
696 | ||
697 |
adult = CardDef() |
|
698 |
adult.name = 'Adult' |
|
699 |
adult.fields = [ |
|
700 |
fields.ItemField( |
|
701 |
id='1', |
|
702 |
label='First name', |
|
703 |
type='string', |
|
704 |
), |
|
705 |
fields.ItemField( |
|
706 |
id='2', |
|
707 |
label='Last name', |
|
708 |
type='string', |
|
709 |
), |
|
710 |
] |
|
711 |
adult.backoffice_submission_roles = user.roles |
|
712 |
adult.workflow_roles = {'_editor': user.roles[0]} |
|
713 |
adult.store() |
|
714 |
adult.data_class().wipe() |
|
715 | ||
716 |
child = CardDef() |
|
717 |
child.name = 'Child' |
|
718 |
child.fields = [ |
|
719 |
fields.ItemField( |
|
720 |
id='1', |
|
721 |
label='First name', |
|
722 |
type='string', |
|
723 |
), |
|
724 |
fields.ItemField( |
|
725 |
id='2', |
|
726 |
label='Last name', |
|
727 |
type='string', |
|
728 |
), |
|
729 |
] |
|
730 |
child.backoffice_submission_roles = user.roles |
|
731 |
child.workflow_roles = {'_editor': user.roles[0]} |
|
732 |
child.store() |
|
733 |
child.data_class().wipe() |
|
734 | ||
735 |
app = login(get_app(pub)) |
|
736 |
resp = app.get('/backoffice/data/family/add/') |
|
737 |
assert 'Add another RL1' in resp |
|
738 |
assert 'Add another RL2' in resp |
|
739 |
assert 'Add another Child' in resp |
|
740 |
assert resp.text.count('/backoffice/data/adult/add/?_popup=1') == 2 |
|
741 |
assert '/backoffice/data/child/add/?_popup=1' in resp |
|
742 | ||
743 |
# no autocompletion for RL1 |
|
744 |
family.fields[0].display_mode = [] |
|
745 |
family.store() |
|
746 |
resp = app.get('/backoffice/data/family/add/') |
|
747 |
assert 'Add another RL1' not in resp |
|
748 |
assert 'Add another RL2' in resp |
|
749 |
assert 'Add another Child' in resp |
|
750 |
assert resp.text.count('/backoffice/data/adult/add/?_popup=1') == 1 |
|
751 |
assert '/backoffice/data/child/add/?_popup=1' in resp |
|
752 | ||
753 |
# user ha no creation rights on child |
|
754 |
child.backoffice_submission_roles = None |
|
755 |
child.store() |
|
756 |
resp = app.get('/backoffice/data/family/add/') |
|
757 |
assert 'Add another RL1' not in resp |
|
758 |
assert 'Add another RL2' in resp |
|
759 |
assert 'Add another Child' not in resp |
|
760 |
assert resp.text.count('/backoffice/data/adult/add/?_popup=1') == 1 |
|
761 |
assert '/backoffice/data/child/add/?_popup=1' not in resp |
wcs/api.py | ||
---|---|---|
278 | 278 |
json_input = get_request().json |
279 | 279 |
formdata = self.formdef.data_class()() |
280 | 280 | |
281 |
if not (user and self.can_user_add_cards()):
|
|
281 |
if not (user and self.formdef.can_user_add_cards(user)):
|
|
282 | 282 |
raise AccessForbiddenError('cannot create card') |
283 | 283 | |
284 | 284 |
if 'data' in json_input: |
... | ... | |
335 | 335 |
if get_request().get_method() != 'PUT': |
336 | 336 |
raise MethodNotAllowedError(allowed_methods=['PUT']) |
337 | 337 |
get_request()._user = get_user_from_api_query_string() |
338 |
if not (get_request()._user and self.can_user_add_cards()):
|
|
338 |
if not (get_request()._user and self.formdef.can_user_add_cards(get_request()._user)):
|
|
339 | 339 |
raise AccessForbiddenError('cannot import cards') |
340 | 340 | |
341 | 341 |
afterjob = bool(get_request().form.get('async') == 'on') |
wcs/backoffice/data_management.py | ||
---|---|---|
17 | 17 |
import csv |
18 | 18 |
import datetime |
19 | 19 |
import io |
20 |
import json |
|
20 | 21 | |
21 | 22 |
from quixote import get_publisher, get_request, get_response, redirect |
22 | 23 |
from quixote.html import htmltext |
... | ... | |
29 | 30 |
from ..qommon.afterjobs import AfterJob |
30 | 31 |
from ..qommon.backoffice.menu import html_top |
31 | 32 |
from ..qommon.form import FileWidget, Form |
32 |
from .management import FormBackOfficeStatusPage, FormFillPage, FormPage, ManagementDirectory |
|
33 |
from .management import FormBackOfficeStatusPage, FormPage, ManagementDirectory |
|
34 |
from .submission import FormFillPage |
|
33 | 35 | |
34 | 36 | |
35 | 37 |
class DataManagementDirectory(ManagementDirectory): |
... | ... | |
103 | 105 |
def add(self): |
104 | 106 |
return CardFillPage(self.formdef.url_name) |
105 | 107 | |
106 |
def can_user_add_cards(self): |
|
107 |
if not self.formdef.backoffice_submission_roles: |
|
108 |
return False |
|
109 |
for role in get_request().user.get_roles(): |
|
110 |
if role in self.formdef.backoffice_submission_roles: |
|
111 |
return True |
|
112 |
return False |
|
113 | ||
114 | 108 |
def listing_top_actions(self): |
115 |
if not self.can_user_add_cards():
|
|
109 |
if not self.formdef.can_user_add_cards(get_request().user):
|
|
116 | 110 |
return '' |
117 | 111 |
return htmltext('<span class="actions"><a href="./add/">%s</a></span>') % _('Add') |
118 | 112 | |
... | ... | |
136 | 130 | |
137 | 131 |
def get_formdata_sidebar_actions(self, qs=''): |
138 | 132 |
r = super().get_formdata_sidebar_actions(qs=qs) |
139 |
if self.can_user_add_cards():
|
|
133 |
if self.formdef.can_user_add_cards(get_request().user):
|
|
140 | 134 |
r += htmltext('<li><a rel="popup" href="import-csv">%s</a></li>') % _( |
141 | 135 |
'Import data from a CSV file' |
142 | 136 |
) |
... | ... | |
191 | 185 |
return output.getvalue() |
192 | 186 | |
193 | 187 |
def import_csv(self): |
194 |
if not self.can_user_add_cards():
|
|
188 |
if not self.formdef.can_user_add_cards(get_request().user):
|
|
195 | 189 |
raise errors.AccessForbiddenError() |
196 | 190 |
context = {'required_fields': []} |
197 | 191 | |
... | ... | |
309 | 303 |
if self.formdef.user_support == 'optional': |
310 | 304 |
self.has_user_support = True |
311 | 305 | |
312 |
def submitted(self, form, *args): |
|
313 |
super().submitted(form, *args) |
|
306 |
def redirect_after_submitted(self, form, filled): |
|
307 |
if get_request().form.get('_popup'): |
|
308 |
popup_response_data = json.dumps( |
|
309 |
{ |
|
310 |
'value': str(filled.id), |
|
311 |
'obj': str(filled.digest), |
|
312 |
} |
|
313 |
) |
|
314 |
return template.QommonTemplateResponse( |
|
315 |
templates=['wcs/backoffice/popup_response.html'], |
|
316 |
context={'popup_response_data': popup_response_data}, |
|
317 |
is_django_native=True, |
|
318 |
) |
|
319 |
result = super().redirect_after_submitted(form, filled) |
|
314 | 320 |
if get_response().get_header('location').endswith('/backoffice/submission/'): |
315 | 321 |
return redirect('..') |
322 |
return result |
|
323 | ||
324 |
def create_form(self, *args, **kwargs): |
|
325 |
form = super().create_form(*args, **kwargs) |
|
326 |
if get_request().form.get('_popup'): |
|
327 |
form.add_hidden('_popup', 1) |
|
328 |
return form |
|
316 | 329 | |
317 | 330 | |
318 | 331 |
class CardBackOfficeStatusPage(FormBackOfficeStatusPage): |
wcs/backoffice/submission.py | ||
---|---|---|
97 | 97 |
] |
98 | 98 | |
99 | 99 |
filling_templates = ['wcs/formdata_filling.html'] |
100 |
popup_filling_templates = ['wcs/formdata_popup_filling.html'] |
|
100 | 101 |
validation_templates = ['wcs/formdata_validation.html'] |
101 | 102 |
steps_templates = ['wcs/formdata_steps.html'] |
102 | 103 |
has_channel_support = True |
... | ... | |
307 | 308 |
get_response().filter['sidebar'] = self.get_sidebar(data) |
308 | 309 |
r += htmltext('<div id="appbar">') |
309 | 310 |
r += htmltext('<h2>%s</h2>') % self.formdef.name |
310 |
if not self.edit_mode: |
|
311 |
if not self.edit_mode and not getattr(self, 'is_popup', False):
|
|
311 | 312 |
draft_formdata_id = data.get('draft_formdata_id') |
312 | 313 |
if draft_formdata_id: |
313 | 314 |
r += htmltext('<a rel="popup" href="remove/%s">%s</a>') % ( |
... | ... | |
340 | 341 |
self.set_tracking_code(filled) |
341 | 342 |
get_session().remove_magictoken(get_request().form.get('magictoken')) |
342 | 343 |
self.clean_submission_context() |
344 |
return self.redirect_after_submitted(form, filled) |
|
343 | 345 | |
346 |
def redirect_after_submitted(self, form, filled): |
|
344 | 347 |
url = filled.perform_workflow() |
345 | 348 |
if url: |
346 | 349 |
pass # always redirect to an URL the workflow returned |
wcs/carddef.py | ||
---|---|---|
142 | 142 |
base_url = get_publisher().get_frontoffice_url() |
143 | 143 |
return '%s/api/cards/%s/' % (base_url, self.url_name) |
144 | 144 | |
145 |
def can_user_add_cards(self, user): |
|
146 |
if not self.backoffice_submission_roles: |
|
147 |
return False |
|
148 |
for role in user.get_roles(): |
|
149 |
if role in self.backoffice_submission_roles: |
|
150 |
return True |
|
151 |
return False |
|
152 | ||
145 | 153 |
def store(self, comment=None): |
146 | 154 |
self.roles = self.backoffice_submission_roles |
147 | 155 |
return super().store(comment=comment) |
wcs/fields.py | ||
---|---|---|
1865 | 1865 | |
1866 | 1866 |
return self.display_mode |
1867 | 1867 | |
1868 |
def get_carddef(self): |
|
1869 |
from wcs.carddef import CardDef |
|
1870 | ||
1871 |
try: |
|
1872 |
return CardDef.get_by_urlname(self.data_source['type'][8:]) |
|
1873 |
except KeyError: |
|
1874 |
return None |
|
1875 | ||
1868 | 1876 |
def perform_more_widget_changes(self, form, kwargs, edit=True): |
1869 | 1877 |
data_source = data_sources.get_object(self.data_source) |
1870 | 1878 |
display_mode = self.get_display_mode(data_source) |
1871 | 1879 | |
1872 | 1880 |
if display_mode == 'autocomplete' and data_source and data_source.can_jsonp(): |
1873 | 1881 |
self.url = kwargs['url'] = data_source.get_jsonp_url() |
1882 |
carddef = self.get_carddef() |
|
1883 |
if carddef and carddef.can_user_add_cards(get_request().user): |
|
1884 |
kwargs['add_related_url'] = carddef.get_backoffice_submission_url() |
|
1874 | 1885 |
self.widget_class = JsonpSingleSelectWidget |
1875 | 1886 |
return |
1876 | 1887 | |
... | ... | |
1932 | 1943 |
and self.data_source.get('type', '').startswith('carddef:') |
1933 | 1944 |
): |
1934 | 1945 |
return value |
1935 |
from wcs.carddef import CardDef |
|
1936 | ||
1946 |
carddef = self.get_carddef() |
|
1947 |
if not carddef: |
|
1948 |
return value |
|
1937 | 1949 |
try: |
1938 |
carddef = CardDef.get_by_urlname(self.data_source['type'][8:]) |
|
1939 | 1950 |
carddata = carddef.data_class().get(value_id) |
1940 | 1951 |
except KeyError: |
1941 | 1952 |
return value |
wcs/forms/root.py | ||
---|---|---|
554 | 554 | |
555 | 555 |
self.formdef.set_live_condition_sources(form, displayed_fields) |
556 | 556 | |
557 |
self.is_popup = form._names.get('_popup') |
|
558 | ||
557 | 559 |
if had_prefill: |
558 | 560 |
# pass over prefilled fields that are used as live source of item |
559 | 561 |
# fields |
... | ... | |
576 | 578 |
if page: |
577 | 579 |
form.add_hidden('page_id', page.id) |
578 | 580 | |
579 |
cancel_label = _('Cancel') |
|
580 |
if self.has_draft_support() and not (data and data.get('is_recalled_draft')): |
|
581 |
cancel_label = _('Discard') |
|
582 |
form.add_submit('cancel', cancel_label, css_class='cancel') |
|
581 |
if not self.is_popup: |
|
582 |
cancel_label = _('Cancel') |
|
583 |
if self.has_draft_support() and not (data and data.get('is_recalled_draft')): |
|
584 |
cancel_label = _('Discard') |
|
585 |
form.add_submit('cancel', cancel_label, css_class='cancel') |
|
583 | 586 | |
584 | 587 |
if self.has_draft_support(): |
585 | 588 |
form.add_submit( |
... | ... | |
595 | 598 |
context = { |
596 | 599 |
'view': self, |
597 | 600 |
'page_no': lambda: self.get_current_page_no(page), |
598 |
'form': form, |
|
599 | 601 |
'formdef': LazyFormDef(self.formdef), |
600 | 602 |
'form_side': lambda: self.form_side(0, page, data=data, magictoken=magictoken), |
601 | 603 |
'steps': lambda: self.step(0, page), |
... | ... | |
604 | 606 |
context['tracking_code_box'] = lambda: self.tracking_code_box(data, magictoken) |
605 | 607 |
self.modify_filling_context(context, page, data) |
606 | 608 | |
609 |
if self.is_popup: |
|
610 |
context['form_obj'] = form |
|
611 |
return template.QommonTemplateResponse( |
|
612 |
templates=list(self.get_formdef_template_variants(self.popup_filling_templates)), |
|
613 |
context=context, |
|
614 |
is_django_native=True, |
|
615 |
) |
|
616 |
else: |
|
617 |
context['form'] = form |
|
618 | ||
607 | 619 |
return template.QommonTemplateResponse( |
608 | 620 |
templates=list(self.get_formdef_template_variants(self.filling_templates)), context=context |
609 | 621 |
) |
wcs/qommon/form.py | ||
---|---|---|
2289 | 2289 |
class JsonpSingleSelectWidget(Widget): |
2290 | 2290 |
template_name = 'qommon/forms/widgets/select_jsonp.html' |
2291 | 2291 | |
2292 |
def __init__(self, name, value=None, url=None, **kwargs): |
|
2292 |
def __init__(self, name, value=None, url=None, add_related_url=None, **kwargs):
|
|
2293 | 2293 |
super().__init__(name, value=value, **kwargs) |
2294 | 2294 |
self.url = url |
2295 |
self.add_related_url = add_related_url |
|
2295 | 2296 | |
2296 | 2297 |
def add_media(self): |
2297 | 2298 |
get_response().add_javascript(['select2.js']) |
wcs/qommon/static/css/dc2/admin.scss | ||
---|---|---|
684 | 684 |
display: inline-block; |
685 | 685 |
} |
686 | 686 | |
687 |
div.JsonpSingleSelectWidget a.add-related { |
|
688 |
border-bottom: none; |
|
689 |
&::before { |
|
690 |
content: "\f067"; /* plus */ |
|
691 |
font-family: FontAwesome; |
|
692 |
padding-right: 1ex; |
|
693 |
} |
|
694 |
} |
|
695 | ||
687 | 696 |
aside#sidebar input.inline-input { |
688 | 697 |
margin-right: 1em; |
689 | 698 |
} |
wcs/qommon/static/css/qommon.scss | ||
---|---|---|
1 |
body.no-header #header { |
|
2 |
display: none; |
|
3 |
} |
|
4 | ||
1 | 5 |
a { |
2 | 6 |
color: #028; |
3 | 7 |
} |
wcs/qommon/static/js/popup_response.js | ||
---|---|---|
1 |
(function() { |
|
2 |
var initData = JSON.parse(document.getElementById('popup-response-constants').dataset.popupResponse); |
|
3 |
opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj); |
|
4 |
})(); |
wcs/qommon/static/js/qommon.admin.js | ||
---|---|---|
311 | 311 |
} |
312 | 312 |
}); |
313 | 313 |
$('[type=radio][name=display_mode]:checked').trigger('change'); |
314 | ||
315 |
// IE doesn't accept periods or dashes in the window name, but the element IDs |
|
316 |
// we use to generate popup window names may contain them, therefore we map them |
|
317 |
// to allowed characters in a reversible way so that we can locate the correct |
|
318 |
// element when the popup window is dismissed. |
|
319 |
function id_to_windowname(text) { |
|
320 |
text = text.replace(/\./g, '__dot__'); |
|
321 |
text = text.replace(/\-/g, '__dash__'); |
|
322 |
return text; |
|
323 |
} |
|
324 | ||
325 |
function windowname_to_id(text) { |
|
326 |
text = text.replace(/__dot__/g, '.'); |
|
327 |
text = text.replace(/__dash__/g, '-'); |
|
328 |
return text; |
|
329 |
} |
|
330 | ||
331 |
function showAddRelatedObjectPopup(triggeringLink) { |
|
332 |
var name = triggeringLink.id.replace(/^add_/, ''); |
|
333 |
name = id_to_windowname(name); |
|
334 |
console.log(name) |
|
335 |
var href = triggeringLink.href; |
|
336 |
console.log(href) |
|
337 |
var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); |
|
338 |
win.focus(); |
|
339 |
return false; |
|
340 |
} |
|
341 | ||
342 |
function dismissAddRelatedObjectPopup(win, newId, newRepr) { |
|
343 |
var name = windowname_to_id(win.name); |
|
344 |
var elem = document.getElementById(name); |
|
345 |
if (elem) { |
|
346 |
var elemName = elem.nodeName.toUpperCase(); |
|
347 |
if (elemName === 'SELECT') { |
|
348 |
elem.options[elem.options.length] = new Option(newRepr, newId, true, true); |
|
349 |
} |
|
350 |
// Trigger a change event to update related links if required. |
|
351 |
$(elem).trigger('change'); |
|
352 |
} |
|
353 |
win.close(); |
|
354 |
} |
|
355 |
window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup; |
|
356 | ||
357 |
$('body').on('click', '.add-related', function(e) { |
|
358 |
e.preventDefault(); |
|
359 |
if (this.href) { |
|
360 |
showAddRelatedObjectPopup(this); |
|
361 |
} |
|
362 |
}); |
|
314 | 363 |
}); |
wcs/qommon/templates/qommon/forms/widgets/select_jsonp.html | ||
---|---|---|
1 | 1 |
{% extends "qommon/forms/widget.html" %} |
2 |
{% load i18n %} |
|
2 | 3 |
{% block widget-control %} |
3 | 4 |
<select id="form_{{widget.name}}" name="{{widget.name}}" |
4 | 5 |
data-select2-url="{{widget.get_select2_url}}" |
... | ... | |
6 | 7 |
data-required="{% if widget.is_required %}true{% endif %}" |
7 | 8 |
data-initial-display-value="{{widget.get_display_value|default_if_none:''}}"> |
8 | 9 |
</select> |
10 |
{% if widget.add_related_url %} |
|
11 |
<a class="add-related" id="add_form_{{ widget.name }}" |
|
12 |
href="{{ widget.add_related_url }}?_popup=1" |
|
13 |
title="{% blocktrans with card=widget.get_title %}Add another {{ card }}{% endblocktrans %}"> |
|
14 |
</a> |
|
15 |
{% endif %} |
|
9 | 16 |
{% endblock %} |
wcs/templates/wcs/backoffice/popup_response.html | ||
---|---|---|
1 |
{% load i18n static %}<!DOCTYPE html> |
|
2 |
<html> |
|
3 |
<head><title>{% trans 'Popup closing...' %}</title></head> |
|
4 |
<body> |
|
5 |
<script type="text/javascript" |
|
6 |
id="popup-response-constants" |
|
7 |
src="{% static "/js/popup_response.js" %}" |
|
8 |
data-popup-response="{{ popup_response_data }}"> |
|
9 |
</script> |
|
10 |
</body> |
|
11 |
</html> |
wcs/templates/wcs/formdata_popup_filling.html | ||
---|---|---|
1 |
{% extends "wcs/backoffice.html" %} |
|
2 | ||
3 |
{% block bodyargs %}class="no-header"{% endblock %} |
|
4 |
{% block site-header %}{% endblock %} |
|
5 |
{% block user-links %}{% endblock %} |
|
6 |
{% block sidepage %}{% endblock %} |
|
7 | ||
8 |
{% block main-content %} |
|
9 |
{% block form-side %} |
|
10 |
{{ form_side|default:"" }} |
|
11 |
{{ publisher.get_request.session.display_message|safe }} |
|
12 |
{% endblock %} |
|
13 | ||
14 |
{{ form_obj.render|safe }} |
|
15 |
{% endblock %} |
wcs/views.py | ||
---|---|---|
31 | 31 |
def get_context_data(self, **kwargs): |
32 | 32 |
context = super().get_context_data(**kwargs) |
33 | 33 | |
34 |
_request = None |
|
34 | 35 |
with compat.request(self.request): |
35 | 36 |
get_request().response.filter = {'admin_ezt': True} |
36 | 37 |
body = get_publisher().try_publish(get_request()) |
37 | 38 |
if isinstance(body, template.QommonTemplateResponse): |
38 | 39 |
body.add_media() |
39 | 40 |
if body.is_django_native: |
41 |
_request = get_request() |
|
40 | 42 |
self.template_names = body.templates[0] |
41 | 43 |
context.update(body.context) |
42 | 44 |
else: |
... | ... | |
46 | 48 |
self.quixote_response = get_request().response |
47 | 49 |
context.update(template.get_decorate_vars(body, get_response(), generate_breadcrumb=True)) |
48 | 50 | |
51 |
# restore request for django mode |
|
52 |
if _request is not None: |
|
53 |
get_publisher()._set_request(_request) |
|
54 | ||
49 | 55 |
return context |
50 | 56 | |
51 | 57 |
def get_template_names(self): |
52 |
- |