Projet

Général

Profil

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

Lauréline Guérin, 26 mars 2021 09:33

Télécharger (17,7 ko)

Voir les différences:

Subject: [PATCH 2/2] 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                  |   2 +
 wcs/carddef.py                                |   8 ++
 wcs/fields.py                                 |  17 ++-
 wcs/qommon/form.py                            |   3 +-
 wcs/qommon/static/css/dc2/admin.scss          |   9 ++
 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/views.py                                  |   1 +
 13 files changed, 243 insertions(+), 22 deletions(-)
 create mode 100644 wcs/qommon/static/js/popup_response.js
 create mode 100644 wcs/templates/wcs/backoffice/popup_response.html
tests/backoffice_pages/test_carddata.py
654 654
    assert 'Associated User' in resp
655 655
    assert carddef.data_class().get(carddata.id).user_id == str(user.id)
656 656
    assert '/user-pending-forms' not in resp.text
657

  
658

  
659
def test_carddata_add_related(pub):
660
    user = create_user(pub)
661

  
662
    BlockDef.wipe()
663
    block = BlockDef()
664
    block.name = 'child'
665
    block.fields = [
666
        fields.ItemField(
667
            id='1',
668
            label='Child',
669
            type='item',
670
            data_source={'type': 'carddef:child'},
671
            display_mode='autocomplete',
672
        ),
673
    ]
674
    block.store()
675

  
676
    CardDef.wipe()
677
    family = CardDef()
678
    family.name = 'Family'
679
    family.fields = [
680
        fields.ItemField(
681
            id='1',
682
            label='RL1',
683
            type='item',
684
            data_source={'type': 'carddef:adult'},
685
            display_mode='autocomplete',
686
        ),
687
        fields.ItemField(
688
            id='2',
689
            label='RL2',
690
            type='item',
691
            data_source={'type': 'carddef:adult'},
692
            display_mode='autocomplete',
693
        ),
694
        fields.BlockField(id='3', label='Children', type='block:child', max_items=42),
695
    ]
696
    family.backoffice_submission_roles = user.roles
697
    family.workflow_roles = {'_editor': user.roles[0]}
698
    family.store()
699
    family.data_class().wipe()
700

  
701
    adult = CardDef()
702
    adult.name = 'Adult'
703
    adult.fields = [
704
        fields.ItemField(
705
            id='1',
706
            label='First name',
707
            type='string',
708
        ),
709
        fields.ItemField(
710
            id='2',
711
            label='Last name',
712
            type='string',
713
        ),
714
    ]
715
    adult.backoffice_submission_roles = user.roles
716
    adult.workflow_roles = {'_editor': user.roles[0]}
717
    adult.store()
718
    adult.data_class().wipe()
719

  
720
    child = CardDef()
721
    child.name = 'Child'
722
    child.fields = [
723
        fields.ItemField(
724
            id='1',
725
            label='First name',
726
            type='string',
727
        ),
728
        fields.ItemField(
729
            id='2',
730
            label='Last name',
731
            type='string',
732
        ),
733
    ]
734
    child.backoffice_submission_roles = user.roles
735
    child.workflow_roles = {'_editor': user.roles[0]}
736
    child.store()
737
    child.data_class().wipe()
738

  
739
    app = login(get_app(pub))
740
    resp = app.get('/backoffice/data/family/add/')
741
    assert 'Add another RL1' in resp
742
    assert 'Add another RL2' in resp
743
    assert 'Add another Child' in resp
744
    assert resp.text.count('/backoffice/data/adult/add/?_popup=1') == 2
745
    assert '/backoffice/data/child/add/?_popup=1' in resp
746

  
747
    # no autocompletion for RL1
748
    family.fields[0].display_mode = []
749
    family.store()
750
    resp = app.get('/backoffice/data/family/add/')
751
    assert 'Add another RL1' not in resp
752
    assert 'Add another RL2' in resp
753
    assert 'Add another Child' in resp
754
    assert resp.text.count('/backoffice/data/adult/add/?_popup=1') == 1
755
    assert '/backoffice/data/child/add/?_popup=1' in resp
756

  
757
    # user ha no creation rights on child
758
    child.backoffice_submission_roles = None
759
    child.store()
760
    resp = app.get('/backoffice/data/family/add/')
761
    assert 'Add another RL1' not in resp
762
    assert 'Add another RL2' in resp
763
    assert 'Add another Child' not in resp
764
    assert resp.text.count('/backoffice/data/adult/add/?_popup=1') == 1
765
    assert '/backoffice/data/child/add/?_popup=1' not in resp
wcs/api.py
286 286
        json_input = get_request().json
287 287
        formdata = self.formdef.data_class()()
288 288

  
289
        if not (user and self.can_user_add_cards()):
289
        if not (user and self.formdef.can_user_add_cards(user)):
290 290
            raise AccessForbiddenError('cannot create card')
291 291

  
292 292
        if 'data' in json_input:
......
343 343
        if get_request().get_method() != 'PUT':
344 344
            raise MethodNotAllowedError(allowed_methods=['PUT'])
345 345
        get_request()._user = get_user_from_api_query_string()
346
        if not (get_request()._user and self.can_user_add_cards()):
346
        if not (get_request()._user and self.formdef.can_user_add_cards(get_request()._user)):
347 347
            raise AccessForbiddenError('cannot import cards')
348 348

  
349 349
        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
from django.utils.encoding import force_text
22 22
from quixote import get_publisher
23 23
from quixote import get_request
24 24
from quixote import get_response
25 25
from quixote import redirect
26
from quixote.html import TemplateIO
27
from quixote.html import htmlescape
28 26
from quixote.html import htmltext
29 27

  
30 28
from wcs import fields
......
116 114
    def add(self):
117 115
        return CardFillPage(self.formdef.url_name)
118 116

  
119
    def can_user_add_cards(self):
120
        if not self.formdef.backoffice_submission_roles:
121
            return False
122
        for role in get_request().user.get_roles():
123
            if role in self.formdef.backoffice_submission_roles:
124
                return True
125
        return False
126

  
127 117
    def listing_top_actions(self):
128
        if not self.can_user_add_cards():
118
        if not self.formdef.can_user_add_cards(get_request().user):
129 119
            return ''
130 120
        return htmltext('<span class="actions"><a href="./add/">%s</a></span>') % _('Add')
131 121

  
......
149 139

  
150 140
    def get_formdata_sidebar_actions(self, qs=''):
151 141
        r = super(CardPage, self).get_formdata_sidebar_actions(qs=qs)
152
        if self.can_user_add_cards():
142
        if self.formdef.can_user_add_cards(get_request().user):
153 143
            r += htmltext('<li><a rel="popup" href="import-csv">%s</a></li>') % _(
154 144
                'Import data from a CSV file'
155 145
            )
......
204 194
        return output.getvalue()
205 195

  
206 196
    def import_csv(self):
207
        if not self.can_user_add_cards():
197
        if not self.formdef.can_user_add_cards(get_request().user):
208 198
            raise errors.AccessForbiddenError()
209 199
        context = {'required_fields': []}
210 200

  
......
322 312
        if self.formdef.user_support == 'optional':
323 313
            self.has_user_support = True
324 314

  
325
    def submitted(self, form, *args):
326
        super(CardFillPage, self).submitted(form, *args)
315
    def redirect_after_submitted(self, form, filled):
316
        if get_request().form.get('_popup'):
317
            popup_response_data = json.dumps(
318
                {
319
                    'value': str(filled.id),
320
                    'obj': str(filled.digest),
321
                }
322
            )
323
            return template.QommonTemplateResponse(
324
                templates=['wcs/backoffice/popup_response.html'],
325
                context={'popup_response_data': popup_response_data},
326
                is_django_native=True,
327
            )
328
        result = super().redirect_after_submitted(form, filled)
327 329
        if get_response().get_header('location').endswith('/backoffice/submission/'):
328 330
            return redirect('..')
331
        return result
332

  
333
    def create_form(self, *args, **kwargs):
334
        form = super().create_form(*args, **kwargs)
335
        if get_request().form.get('_popup'):
336
            form.add_hidden('_popup', 1)
337
        return form
329 338

  
330 339

  
331 340
class CardBackOfficeStatusPage(FormBackOfficeStatusPage):
wcs/backoffice/submission.py
348 348
        self.set_tracking_code(filled)
349 349
        get_session().remove_magictoken(get_request().form.get('magictoken'))
350 350
        self.clean_submission_context()
351
        return self.redirect_after_submitted(form, filled)
351 352

  
353
    def redirect_after_submitted(self, form, filled):
352 354
        url = filled.perform_workflow()
353 355
        if url:
354 356
            pass  # always redirect to an URL the workflow returned
wcs/carddef.py
150 150
        base_url = get_publisher().get_frontoffice_url()
151 151
        return '%s/api/cards/%s/' % (base_url, self.url_name)
152 152

  
153
    def can_user_add_cards(self, user):
154
        if not self.backoffice_submission_roles:
155
            return False
156
        for role in user.get_roles():
157
            if role in self.backoffice_submission_roles:
158
                return True
159
        return False
160

  
153 161
    def store(self, comment=None):
154 162
        self.roles = self.backoffice_submission_roles
155 163
        return super().store(comment=comment)
wcs/fields.py
1841 1841

  
1842 1842
        return self.display_mode
1843 1843

  
1844
    def get_carddef(self):
1845
        from wcs.carddef import CardDef
1846

  
1847
        try:
1848
            return CardDef.get_by_urlname(self.data_source['type'][8:])
1849
        except KeyError:
1850
            return None
1851

  
1844 1852
    def perform_more_widget_changes(self, form, kwargs, edit=True):
1845 1853
        data_source = data_sources.get_object(self.data_source)
1846 1854
        display_mode = self.get_display_mode(data_source)
1847 1855

  
1848 1856
        if display_mode == 'autocomplete' and data_source and data_source.can_jsonp():
1849 1857
            self.url = kwargs['url'] = data_source.get_jsonp_url()
1858
            carddef = self.get_carddef()
1859
            if carddef and carddef.can_user_add_cards(get_request().user):
1860
                kwargs['add_related_url'] = carddef.get_backoffice_submission_url()
1850 1861
            self.widget_class = JsonpSingleSelectWidget
1851 1862
            return
1852 1863

  
......
1908 1919
            and self.data_source.get('type', '').startswith('carddef:')
1909 1920
        ):
1910 1921
            return value
1911
        from wcs.carddef import CardDef
1912

  
1922
        carddef = self.get_carddef()
1923
        if not carddef:
1924
            return value
1913 1925
        try:
1914
            carddef = CardDef.get_by_urlname(self.data_source['type'][8:])
1915 1926
            carddata = carddef.data_class().get(value_id)
1916 1927
        except KeyError:
1917 1928
            return value
wcs/qommon/form.py
2322 2322
class JsonpSingleSelectWidget(Widget):
2323 2323
    template_name = 'qommon/forms/widgets/select_jsonp.html'
2324 2324

  
2325
    def __init__(self, name, value=None, url=None, **kwargs):
2325
    def __init__(self, name, value=None, url=None, add_related_url=None, **kwargs):
2326 2326
        super(JsonpSingleSelectWidget, self).__init__(name, value=value, **kwargs)
2327 2327
        self.url = url
2328
        self.add_related_url = add_related_url
2328 2329

  
2329 2330
    def add_media(self):
2330 2331
        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/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
282 282
      }
283 283
    });
284 284
    $('[type=radio][name=display_mode]:checked').trigger('change');
285

  
286
    // IE doesn't accept periods or dashes in the window name, but the element IDs
287
    // we use to generate popup window names may contain them, therefore we map them
288
    // to allowed characters in a reversible way so that we can locate the correct
289
    // element when the popup window is dismissed.
290
    function id_to_windowname(text) {
291
        text = text.replace(/\./g, '__dot__');
292
        text = text.replace(/\-/g, '__dash__');
293
        return text;
294
    }
295

  
296
    function windowname_to_id(text) {
297
        text = text.replace(/__dot__/g, '.');
298
        text = text.replace(/__dash__/g, '-');
299
        return text;
300
    }
301

  
302
    function showAddRelatedObjectPopup(triggeringLink) {
303
        var name = triggeringLink.id.replace(/^add_/, '');
304
        name = id_to_windowname(name);
305
        console.log(name)
306
        var href = triggeringLink.href;
307
        console.log(href)
308
        var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
309
        win.focus();
310
        return false;
311
    }
312

  
313
    function dismissAddRelatedObjectPopup(win, newId, newRepr) {
314
        var name = windowname_to_id(win.name);
315
        var elem = document.getElementById(name);
316
        if (elem) {
317
            var elemName = elem.nodeName.toUpperCase();
318
            if (elemName === 'SELECT') {
319
                elem.options[elem.options.length] = new Option(newRepr, newId, true, true);
320
            }
321
            // Trigger a change event to update related links if required.
322
            $(elem).trigger('change');
323
        }
324
        win.close();
325
    }
326
    window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup;
327

  
328
    $('body').on('click', '.add-related', function(e) {
329
        e.preventDefault();
330
        if (this.href) {
331
            showAddRelatedObjectPopup(this);
332
        }
333
    });
285 334
});
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/views.py
40 40
                body.add_media()
41 41
                if body.is_django_native:
42 42
                    self.template_names = body.templates[0]
43
                    context.update(body.context)
43 44
                else:
44 45
                    body = template.render(body.templates, body.context)
45 46
                    self.template_names = None
46
-