Projet

Général

Profil

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

Lauréline Guérin, 30 mars 2021 16:45

Télécharger (23,8 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                                 |  17 ++-
 wcs/forms/root.py                             |  20 +++-
 wcs/qommon/form.py                            |   3 +-
 wcs/qommon/static/css/dc2/admin.scss          |   9 ++
 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    |   7 ++
 .../wcs/backoffice/popup_response.html        |  11 ++
 wcs/templates/wcs/formdata_filling.html       |   2 +-
 wcs/templates/wcs/formdata_popup_filling.html |  15 +++
 wcs/views.py                                  |  27 +++--
 17 files changed, 294 insertions(+), 41 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
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,
601
            'form_obj': form,
599 602
            'formdef': LazyFormDef(self.formdef),
600 603
            'form_side': lambda: self.form_side(0, page, data=data, magictoken=magictoken),
601 604
            'steps': lambda: self.step(0, page),
......
604 607
            context['tracking_code_box'] = lambda: self.tracking_code_box(data, magictoken)
605 608
        self.modify_filling_context(context, page, data)
606 609

  
610
        if self.is_popup:
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

  
607 617
        return template.QommonTemplateResponse(
608 618
            templates=list(self.get_formdef_template_variants(self.filling_templates)), context=context
609 619
        )
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_filling.html
44 44
{% endif %}
45 45
{% endblock %}
46 46

  
47
{{ form.render|safe }}
47
{{ form_obj.render|safe }}
48 48
{% endblock %}
49 49

  
50 50
{% endblock %}
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
        with compat.request(self.request):
35
            get_request().response.filter = {'admin_ezt': True}
36
            body = get_publisher().try_publish(get_request())
37
            if isinstance(body, template.QommonTemplateResponse):
38
                body.add_media()
39
                if body.is_django_native:
40
                    self.template_names = body.templates[0]
41
                    context.update(body.context)
42
                else:
43
                    body = template.render(body.templates, body.context)
44
                    self.template_names = None
45
                get_publisher().session_manager.finish_successful_request()
46
            self.quixote_response = get_request().response
47
            context.update(template.get_decorate_vars(body, get_response(), generate_breadcrumb=True))
34
        get_request().response.filter = {'admin_ezt': True}
35
        body = get_publisher().try_publish(get_request())
36
        if isinstance(body, template.QommonTemplateResponse):
37
            body.add_media()
38
            if body.is_django_native:
39
                self.template_names = body.templates[0]
40
                context.update(body.context)
41
            else:
42
                body = template.render(body.templates, body.context)
43
                self.template_names = None
44
            get_publisher().session_manager.finish_successful_request()
45
        self.quixote_response = get_request().response
46
        context.update(template.get_decorate_vars(body, get_response(), generate_breadcrumb=True))
48 47

  
49 48
        return context
50 49

  
51
-