Projet

Général

Profil

0001-cards-create-related-card-in-a-popup-48534.patch

Lauréline Guérin, 13 avril 2021 15:40

Télécharger (22,3 ko)

Voir les différences:

Subject: [PATCH] cards: create related card in a popup (#48534)

 tests/backoffice_pages/test_carddata.py       | 109 ++++++++++++++++++
 wcs/api.py                                    |   4 +-
 wcs/backoffice/data_management.py             |  41 ++++---
 wcs/backoffice/submission.py                  |   5 +-
 wcs/carddef.py                                |   8 ++
 wcs/fields.py                                 |  21 +++-
 wcs/forms/root.py                             |  22 +++-
 wcs/qommon/form.py                            |   3 +-
 wcs/qommon/static/css/qommon.scss             |   4 +
 wcs/qommon/static/js/popup_response.js        |   4 +
 wcs/qommon/static/js/qommon.admin.js          |  49 ++++++++
 .../qommon/forms/widgets/select_jsonp.html    |   6 +
 .../wcs/backoffice/popup_response.html        |  11 ++
 wcs/templates/wcs/formdata_popup_filling.html |  15 +++
 wcs/views.py                                  |   6 +
 15 files changed, 282 insertions(+), 26 deletions(-)
 create mode 100644 wcs/qommon/static/js/popup_response.js
 create mode 100644 wcs/templates/wcs/backoffice/popup_response.html
 create mode 100644 wcs/templates/wcs/formdata_popup_filling.html
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
280 280
        json_input = get_request().json
281 281
        formdata = self.formdef.data_class()()
282 282

  
283
        if not (user and self.can_user_add_cards()):
283
        if not (user and self.formdef.can_user_add_cards(user)):
284 284
            raise AccessForbiddenError('cannot create card')
285 285

  
286 286
        if 'data' in json_input:
......
337 337
        if get_request().get_method() != 'PUT':
338 338
            raise MethodNotAllowedError(allowed_methods=['PUT'])
339 339
        get_request()._user = get_user_from_api_query_string()
340
        if not (get_request()._user and self.can_user_add_cards()):
340
        if not (get_request()._user and self.formdef.can_user_add_cards(get_request()._user)):
341 341
            raise AccessForbiddenError('cannot import cards')
342 342

  
343 343
        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):
......
102 104
    def add(self):
103 105
        return CardFillPage(self.formdef.url_name)
104 106

  
105
    def can_user_add_cards(self):
106
        if not self.formdef.backoffice_submission_roles:
107
            return False
108
        for role in get_request().user.get_roles():
109
            if role in self.formdef.backoffice_submission_roles:
110
                return True
111
        return False
112

  
113 107
    def listing_top_actions(self):
114
        if not self.can_user_add_cards():
108
        if not self.formdef.can_user_add_cards(get_request().user):
115 109
            return ''
116 110
        return htmltext('<span class="actions"><a href="./add/">%s</a></span>') % _('Add')
117 111

  
......
135 129

  
136 130
    def get_formdata_sidebar_actions(self, qs=''):
137 131
        r = super().get_formdata_sidebar_actions(qs=qs)
138
        if self.can_user_add_cards():
132
        if self.formdef.can_user_add_cards(get_request().user):
139 133
            r += htmltext('<li><a rel="popup" href="import-csv">%s</a></li>') % _(
140 134
                'Import data from a CSV file'
141 135
            )
......
190 184
        return output.getvalue()
191 185

  
192 186
    def import_csv(self):
193
        if not self.can_user_add_cards():
187
        if not self.formdef.can_user_add_cards(get_request().user):
194 188
            raise errors.AccessForbiddenError()
195 189
        context = {'required_fields': []}
196 190

  
......
308 302
        if self.formdef.user_support == 'optional':
309 303
            self.has_user_support = True
310 304

  
311
    def submitted(self, form, *args):
312
        super().submitted(form, *args)
305
    def redirect_after_submitted(self, form, filled):
306
        if get_request().form.get('_popup'):
307
            popup_response_data = json.dumps(
308
                {
309
                    'value': str(filled.id),
310
                    'obj': str(filled.digest),
311
                }
312
            )
313
            return template.QommonTemplateResponse(
314
                templates=['wcs/backoffice/popup_response.html'],
315
                context={'popup_response_data': popup_response_data},
316
                is_django_native=True,
317
            )
318
        result = super().redirect_after_submitted(form, filled)
313 319
        if get_response().get_header('location').endswith('/backoffice/submission/'):
314 320
            return redirect('..')
321
        return result
322

  
323
    def create_form(self, *args, **kwargs):
324
        form = super().create_form(*args, **kwargs)
325
        if get_request().form.get('_popup'):
326
            form.add_hidden('_popup', 1)
327
        return form
315 328

  
316 329

  
317 330
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
1866 1866

  
1867 1867
        return self.display_mode
1868 1868

  
1869
    def get_carddef(self):
1870
        from wcs.carddef import CardDef
1871

  
1872
        try:
1873
            return CardDef.get_by_urlname(self.data_source['type'][8:])
1874
        except KeyError:
1875
            return None
1876

  
1869 1877
    def perform_more_widget_changes(self, form, kwargs, edit=True):
1870 1878
        data_source = data_sources.get_object(self.data_source)
1871 1879
        display_mode = self.get_display_mode(data_source)
1872 1880

  
1873 1881
        if display_mode == 'autocomplete' and data_source and data_source.can_jsonp():
1874 1882
            self.url = kwargs['url'] = data_source.get_jsonp_url()
1883
            carddef = self.get_carddef()
1884
            if (
1885
                get_request().is_in_backoffice()
1886
                and carddef
1887
                and carddef.can_user_add_cards(get_request().user)
1888
            ):
1889
                kwargs['add_related_url'] = carddef.get_backoffice_submission_url()
1875 1890
            self.widget_class = JsonpSingleSelectWidget
1876 1891
            return
1877 1892

  
......
1933 1948
            and self.data_source.get('type', '').startswith('carddef:')
1934 1949
        ):
1935 1950
            return value
1936
        from wcs.carddef import CardDef
1937

  
1951
        carddef = self.get_carddef()
1952
        if not carddef:
1953
            return value
1938 1954
        try:
1939
            carddef = CardDef.get_by_urlname(self.data_source['type'][8:])
1940 1955
            carddata = carddef.data_class().get(value_id)
1941 1956
        except KeyError:
1942 1957
            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/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
293 293
      }
294 294
    });
295 295
    $('[type=radio][name=display_mode]:checked').trigger('change');
296

  
297
    // IE doesn't accept periods or dashes in the window name, but the element IDs
298
    // we use to generate popup window names may contain them, therefore we map them
299
    // to allowed characters in a reversible way so that we can locate the correct
300
    // element when the popup window is dismissed.
301
    function id_to_windowname(text) {
302
        text = text.replace(/\./g, '__dot__');
303
        text = text.replace(/\-/g, '__dash__');
304
        return text;
305
    }
306

  
307
    function windowname_to_id(text) {
308
        text = text.replace(/__dot__/g, '.');
309
        text = text.replace(/__dash__/g, '-');
310
        return text;
311
    }
312

  
313
    function showAddRelatedObjectPopup(triggeringLink) {
314
        var name = triggeringLink.id.replace(/^add_/, '');
315
        name = id_to_windowname(name);
316
        console.log(name)
317
        var href = triggeringLink.href;
318
        console.log(href)
319
        var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
320
        win.focus();
321
        return false;
322
    }
323

  
324
    function dismissAddRelatedObjectPopup(win, newId, newRepr) {
325
        var name = windowname_to_id(win.name);
326
        var elem = document.getElementById(name);
327
        if (elem) {
328
            var elemName = elem.nodeName.toUpperCase();
329
            if (elemName === 'SELECT') {
330
                elem.options[elem.options.length] = new Option(newRepr, newId, true, true);
331
            }
332
            // Trigger a change event to update related links if required.
333
            $(elem).trigger('change');
334
        }
335
        win.close();
336
    }
337
    window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup;
338

  
339
    $('body').on('click', '.add-related', function(e) {
340
        e.preventDefault();
341
        if (this.href) {
342
            showAddRelatedObjectPopup(this);
343
        }
344
    });
296 345
});
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 pk-button" 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 %}">+</a>
14
{% endif %}
9 15
{% 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
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
-