Projet

Général

Profil

0001-backoffice-make-it-possible-to-submit-forms-from-the.patch

Frédéric Péters, 28 août 2015 16:48

Télécharger (22,1 ko)

Voir les différences:

Subject: [PATCH] backoffice: make it possible to submit forms from the
 backoffice (#7052)

 tests/test_admin_pages.py           |   6 +-
 tests/test_backoffice_pages.py      | 103 ++++++++++++++++++++++++
 wcs/admin/forms.py                  |  56 ++++++++-----
 wcs/backoffice/root.py              |   7 +-
 wcs/backoffice/submission.py        | 155 ++++++++++++++++++++++++++++++++++++
 wcs/formdef.py                      |   1 +
 wcs/forms/root.py                   |  26 ++++--
 wcs/qommon/static/css/dc2/admin.css |   3 +
 8 files changed, 327 insertions(+), 30 deletions(-)
 create mode 100644 wcs/backoffice/submission.py
tests/test_admin_pages.py
553 553

  
554 554
    app = login(get_app(pub))
555 555
    resp = app.get('/backoffice/forms/1/')
556
    resp = resp.click('change', href='roles')
556
    resp = resp.click('change', href='roles', index=0)
557 557
    resp = resp.forms[0].submit('cancel')
558 558

  
559 559
    resp = app.get('/backoffice/forms/1/')
560
    resp = resp.click('change', href='roles')
560
    resp = resp.click('change', href='roles', index=0)
561 561
    resp.forms[0]['roles$element0'].value = role.name
562 562
    resp = resp.forms[0].submit('submit')
563 563
    assert FormDef.get(1).roles == [role.id]
......
921 921
    assert resp.forms[0]['workflow_id'].value
922 922

  
923 923
    resp = app.get('/backoffice/forms/1/')
924
    resp = resp.click('change', href='roles')
924
    resp = resp.click('change', href='roles', index=0)
925 925
    assert resp.forms[0]['roles$element0'].value == 'Logged Users'
926 926
    assert resp.forms[0]['roles$element1'].value == 'ZAB'
927 927

  
tests/test_backoffice_pages.py
38 38
    if pub.user_class.select(lambda x: x.name == 'admin'):
39 39
        user1 = pub.user_class.select(lambda x: x.name == 'admin')[0]
40 40
        user1.is_admin = is_admin
41
        user1.roles = [x.id for x in Role.select() if x.name == 'foobar']
41 42
        user1.store()
42 43
        return user1
43 44
    user1 = pub.user_class(name='admin')
......
453 454
    resp.forms[0]['end'] = '2014-12-31'
454 455
    resp = resp.forms[0].submit()
455 456
    assert 'Total count: 20' in resp.body
457

  
458
def test_backoffice_submission(pub):
459
    user = create_user(pub)
460
    create_environment(pub)
461

  
462
    app = login(get_app(pub))
463
    resp = app.get('/backoffice/')
464
    assert 'Submission' in resp.body
465

  
466
    resp = resp.click('Submission', index=0)
467
    formdef = FormDef.select()[0]
468
    assert not formdef.url_name in resp.body
469

  
470
    formdef.backoffice_submission_roles = user.roles[:]
471
    formdef.store()
472
    resp = app.get('/backoffice/submission/')
473
    assert formdef.url_name in resp.body
474

  
475
    resp = resp.click(formdef.name)
476
    resp.form['f1'] = 'test submission'
477
    resp.form['f2'] = 'baz'
478
    resp.form['f3'] = 'C'
479
    resp = resp.form.submit('submit')
480
    assert 'Check values then click submit.' in resp.body
481

  
482
    # going back to first page, to check
483
    resp = resp.form.submit('previous')
484
    assert resp.form['f1'].value == 'test submission'
485
    resp = resp.form.submit('submit')
486

  
487
    # final submit
488
    resp = resp.form.submit('submit')
489

  
490
    formdata_no = resp.location.split('/')[-2]
491
    data_class = formdef.data_class()
492
    assert data_class.get(formdata_no).data['1'] == 'test submission'
493
    assert data_class.get(formdata_no).data['2'] == 'baz'
494
    assert data_class.get(formdata_no).status == 'wf-new'
495
    assert data_class.get(formdata_no).user is None
496

  
497
    resp = resp.follow() # get to the formdata page
498

  
499
    formdata_count = data_class.count()
500

  
501
    # test submission when agent is not receiver
502
    formdef.workflow_roles = {}
503
    formdef.store()
504
    resp = app.get('/backoffice/submission/')
505
    resp = resp.click(formdef.name)
506
    resp.form['f1'] = 'test submission'
507
    resp.form['f2'] = 'baz'
508
    resp.form['f3'] = 'C'
509
    resp = resp.form.submit('submit') # to validation screen
510
    resp = resp.form.submit('submit') # final submit
511
    assert resp.location == 'http://example.net/backoffice/submission/'
512
    resp = resp.follow() # should go back to submission screen
513

  
514
    assert data_class.count() == formdata_count + 1
515

  
516
def test_backoffice_submission_tracking_code(pub):
517
    user = create_user(pub)
518
    create_environment(pub)
519

  
520
    app = login(get_app(pub))
521
    resp = app.get('/backoffice/')
522
    assert 'Submission' in resp.body
523

  
524
    resp = resp.click('Submission', index=0)
525
    formdef = FormDef.select()[0]
526
    assert not formdef.url_name in resp.body
527

  
528
    formdef.enable_tracking_codes = True
529
    formdef.backoffice_submission_roles = user.roles[:]
530
    formdef.store()
531
    data_class = formdef.data_class()
532
    data_class.wipe()
533
    resp = app.get('/backoffice/submission/')
534
    assert formdef.url_name in resp.body
535

  
536
    resp = resp.click(formdef.name)
537
    resp.form['f1'] = 'test submission'
538
    resp.form['f2'] = 'baz'
539
    resp.form['f3'] = 'C'
540
    resp = resp.form.submit('submit')
541
    assert 'Check values then click submit.' in resp.body
542

  
543
    # stop here, don't validate, let user finish it.
544
    assert data_class.count() == 1
545
    formdata_no = data_class.select()[0].id
546
    tracking_code = data_class.select()[0].tracking_code
547

  
548
    assert data_class.get(formdata_no).data['1'] == 'test submission'
549
    assert data_class.get(formdata_no).data['2'] == 'baz'
550
    assert data_class.get(formdata_no).status == 'draft'
551
    assert data_class.get(formdata_no).user is None
552

  
553
    resp = get_app(pub).get('/code/%s/load' % data_class.select()[0].tracking_code)
554
    resp = resp.follow()
555
    assert resp.location.startswith('http://example.net/form-title/?mt=')
556
    resp = resp.follow()
557
    assert 'Check values then click submit.' in resp.body
558
    assert 'test submission' in resp.body
wcs/admin/forms.py
290 290
                  ('workflow-status-remapping', 'workflow_status_remapping'),
291 291
                  'roles', 'title', 'options', ('acl-read', 'acl_read'),
292 292
                  'overwrite', 'qrcode', 'information',
293
                  ('public-url', 'public_url')]
293
                  ('public-url', 'public_url'),
294
                  ('backoffice-submission-roles', 'backoffice_submission_roles'),]
294 295

  
295 296
    def __init__(self, component):
296 297
        try:
......
415 416
            r += htmltext('</ul>')
416 417
            r += htmltext('</li>')
417 418

  
418
        r += htmltext('<li>%s ') % _('User Roles:')
419
        if self.formdef.roles:
420
            roles = []
421
            for x in self.formdef.roles:
422
                if x == logged_users_role().id:
423
                    roles.append(logged_users_role().name)
424
                else:
425
                    try:
426
                        roles.append(htmltext('<a href="../../roles/%s/">%s</a>') % (x, Role.get(x).name))
427
                    except KeyError:
428
                        # removed role ?
429
                        roles.append(htmltext('<em>%s</em>') % _('Unknown role (%s)') % role_id)
430
            r += htmltext(', ').join(roles)
431
        else:
432
            r += '-'
433
        r += ' '
434
        r += htmltext('(<a href="roles" rel="popup">%s</a>)') % _('change')
435
        r += htmltext('</li>')
419
        r += self._display_roles(_('User Roles:'), 'roles', 'roles')
420
        r += self._display_roles(_('Backoffice Submission Roles:'),
421
                'backoffice_submission_roles',
422
                'backoffice-submission-roles')
436 423

  
437 424
        r += htmltext('<li>%s ') % _('Read Access:')
438 425
        r += '%s' % {'none': _('None'),
......
498 485
        r += htmltext('</div>')
499 486
        return r.getvalue()
500 487

  
488
    def _display_roles(self, title, attribute, change_url):
489
        r = TemplateIO(html=True)
490
        r += htmltext('<li>%s ') % title
491
        if getattr(self.formdef, attribute):
492
            roles = []
493
            for x in getattr(self.formdef, attribute):
494
                if x == logged_users_role().id:
495
                    roles.append(logged_users_role().name)
496
                else:
497
                    try:
498
                        roles.append(htmltext('<a href="../../roles/%s/">%s</a>') % (x, Role.get(x).name))
499
                    except KeyError:
500
                        # removed role ?
501
                        roles.append(htmltext('<em>%s</em>') % _('Unknown role (%s)') % role_id)
502
            r += htmltext(', ').join(roles)
503
        else:
504
            r += '-'
505
        r += ' '
506
        r += htmltext('(<a href="%s" rel="popup">%s</a>)') % (
507
                change_url, _('change'))
508
        r += htmltext('</li>')
509
        return r.getvalue()
510

  
501 511
    def get_sidebar(self):
502 512
        r = TemplateIO(html=True)
503 513
        r += htmltext('<ul id="sidebar-actions">')
......
613 623
                attribute='roles',
614 624
                description=_('Select the roles that can access this form.'))
615 625

  
626
    def backoffice_submission_roles(self):
627
        return self._roles_selection(
628
                title=_('Backoffice Submission Roles'),
629
                attribute='backoffice_submission_roles',
630
                include_logged_users_role=False,
631
                description=_('Select the roles that will be allowed to '
632
                              'fill out forms of this kind in the backoffice.'))
633

  
616 634
    def title(self):
617 635
        form = Form(enctype='multipart/form-data')
618 636
        form.add(StringWidget, 'name', title=_('Form Title'), required=True,
wcs/backoffice/root.py
39 39
import wcs.admin.users
40 40
import wcs.admin.workflows
41 41

  
42
from . import submission
42 43
from . import management
43 44

  
44 45

  
......
54 55
    users = wcs.admin.users.UsersDirectory()
55 56
    workflows = wcs.admin.workflows.WorkflowsDirectory()
56 57
    management = management.ManagementDirectory()
58
    submission = submission.SubmissionDirectory()
57 59

  
58 60
    menu_items = [
59 61
        ('management/', N_('Management')),
62
        ('submission/', N_('Submission')),
60 63
        ('forms/', N_('Forms Workshop')),
61 64
        ('workflows/', N_('Workflows Workshop')),
62 65
        ('users/', N_('Users')),
......
104 107

  
105 108
        # for some subdirectories, the user needs to be part of a role allowed
106 109
        # to go in the backoffice
107
        if subdirectory in ('management',):
110
        if subdirectory in ('management', 'submission'):
108 111
            return get_request().user.can_go_in_backoffice()
109 112

  
110 113
        # for the other directories, an extra level is required, the user needs
......
272 275
                'slug': slug,
273 276
                'url': backoffice_url + k})
274 277
            if slug in ('home', 'forms', 'workflows', 'users', 'roles',
275
                    'categories', 'settings', 'management'):
278
                    'categories', 'settings', 'management', 'submission'):
276 279
                menu_items[-1]['icon'] = k.strip('/')
277 280
        return menu_items
wcs/backoffice/submission.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2015  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
from quixote import get_publisher, get_request, get_response, get_session, redirect
18
from quixote.directory import Directory
19
from quixote.html import TemplateIO, htmltext
20

  
21
from qommon.backoffice.menu import html_top
22
from qommon import errors
23

  
24
from wcs.formdef import FormDef
25
from wcs.categories import Category
26
from wcs.forms.root import FormPage as PublicFormFillPage
27

  
28

  
29
class FormFillPage(PublicFormFillPage):
30
    def html_top(self, *args, **kwargs):
31
        return html_top('submission', *args, **kwargs)
32

  
33
    def check_role(self):
34
        if not self.formdef.backoffice_submission_roles:
35
            raise errors.AccessUnauthorizedError()
36
        for role in get_request().user.roles or []:
37
            if role in self.formdef.backoffice_submission_roles:
38
                break
39
        else:
40
            raise errors.AccessUnauthorizedError()
41

  
42
    def get_sidebar(self, data):
43
        r = TemplateIO(html=True)
44

  
45
        if self.formdef.enable_tracking_codes:
46
            draft_formdata_id = data.get('draft_formdata_id')
47
            r += htmltext('<h3>%s</h3>') % _('Tracking Code')
48
            tracking_code = None
49
            if draft_formdata_id:
50
                formdata = self.formdef.data_class().get(draft_formdata_id)
51
                if formdata.tracking_code:
52
                    tracking_code = formdata.tracking_code
53
            if tracking_code:
54
                r += htmltext('<p>%s</p>') % tracking_code
55
            else:
56
                r += htmltext('<p>-</p>')
57

  
58
        return r.getvalue()
59

  
60
    def form_side(self, step_no, page_no=0, log_detail=None, data=None, editing=None):
61
        r = TemplateIO(html=True)
62
        get_response().filter['sidebar'] = self.get_sidebar(data)
63
        r += htmltext('<div id="side">')
64
        r += self.step(step_no, page_no, log_detail, data=data, editing=editing)
65
        r += htmltext('</div> <!-- #side -->')
66
        return r.getvalue()
67

  
68
    def submitted(self, form, *args):
69
        filled = self.formdef.data_class()()
70
        filled.just_created()
71
        filled.data = self.formdef.get_data(form)
72
        filled.store()
73

  
74
        self.keep_tracking_code(filled)
75
        get_session().remove_magictoken(get_request().form.get('magictoken'))
76

  
77
        url = filled.perform_workflow()
78
        if not url:
79
            url = filled.get_url(backoffice=True)
80

  
81
        if not self.formdef.is_of_concern_for_user(self.user, filled):
82
            # if the agent is not allowed to see the submitted formdef,
83
            # redirect to the submission homepage
84
            return redirect(get_publisher().get_backoffice_url() + '/submission/')
85

  
86
        return redirect(url)
87

  
88
    def save_draft(self, data, page_no):
89
        formdata = super(FormFillPage, self).save_draft(data, page_no)
90
        formdata.user_id = None
91
        formdata.store()
92
        return formdata
93

  
94

  
95
class SubmissionDirectory(Directory):
96
    _q_exports = ['']
97

  
98
    def _q_index(self):
99
        get_response().breadcrumb.append(('submission/', _('Submission')))
100
        html_top('submission', _('Submission'))
101
        user = get_request().user
102

  
103
        list_forms = []
104
        for formdef in FormDef.select(order_by='name', ignore_errors=True):
105
            if formdef.is_disabled():
106
                continue
107
            if not formdef.backoffice_submission_roles:
108
                continue
109
            for role in user.roles or []:
110
                if role in formdef.backoffice_submission_roles:
111
                    break
112
            else:
113
                continue
114
            list_forms.append(formdef)
115

  
116
        r = TemplateIO(html=True)
117

  
118
        cats = Category.select()
119
        Category.sort_by_position(cats)
120
        one = False
121
        for c in cats:
122
            l2 = [x for x in list_forms if str(x.category_id) == str(c.id)]
123
            if l2:
124
                r += self.form_list(l2, title=c.name)
125
                one = True
126

  
127
        l2 = [x for x in list_forms if not x.category]
128
        if l2:
129
            if one:
130
                title = _('Misc')
131
            else:
132
                title = None
133
            r += self.form_list(l2, title=title)
134

  
135
        return r.getvalue()
136

  
137
    def form_list(self, formdefs, title=None):
138
        r = TemplateIO(html=True)
139
        r += htmltext('<div class="bo-block">')
140
        if title:
141
            r += htmltext('<h2>%s</h2>') % title
142

  
143
        r += htmltext('<ul class="biglist">')
144
        for formdef in formdefs:
145
            r += htmltext('<li>')
146
            r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (
147
                    formdef.url_name, formdef.name)
148
            r += htmltext('</li>')
149
        r += htmltext('</ul>')
150
        r += htmltext('</div>')
151
        return r.getvalue()
152

  
153
    def _q_lookup(self, component):
154
        get_response().breadcrumb.append(('submission/', _('Submission')))
155
        return FormFillPage(component)
wcs/formdef.py
67 67
    workflow_options = None
68 68
    workflow_roles = None
69 69
    roles = None
70
    backoffice_submission_roles = None
70 71
    discussion = False
71 72
    confirmation = True
72 73
    detailed_emails = True
wcs/forms/root.py
222 222
        self.user = get_request().user
223 223
        get_response().breadcrumb.append( (component + '/', self.formdef.name) )
224 224

  
225
    def html_top(self, *args, **kwargs):
226
        html_top(*args, **kwargs)
227

  
225 228
    def get_substitution_variables(self):
226 229
        return self.substvars
227 230

  
......
375 378
                    v = data[k]
376 379
                elif field.prefill:
377 380
                    v = field.get_prefill_value()
381
                    if get_request().get_path().startswith('/backoffice/') and (
382
                            field.prefill and field.prefill.get('type') in ('user', 'geoloc')):
383
                        # turn off prefilling from user and geolocation
384
                        # attributes if the form is filled from the backoffice
385
                        v = None
378 386
                    if v:
379 387
                        prefilled = True
380 388
                        form.get_widget('f%s' % k).set_message(
......
395 403
            if not one:
396 404
                req.form = {}
397 405

  
398
        html_top(self.formdef.name)
406
        self.html_top(self.formdef.name)
399 407
        r += self.form_side(0, page_no, log_detail=log_detail, data=data, editing=editing)
400 408

  
401 409
        form.add_hidden('step', '0')
......
505 513
                    if data.has_key('page_no'):
506 514
                        page_no = int(data['page_no'])
507 515
                        del data['page_no']
508
                        if page_no == -1:
516
                        if page_no == -1 or page_no >= self.page_number:
509 517
                            req = get_request()
510 518
                            for k, v in data.items():
511 519
                                req.form['f%s' % k] = v
......
799 807
            except AttributeError:
800 808
                filled.user_id = get_request().user.id
801 809

  
810
        if get_request().get_path().startswith('/backoffice/'):
811
            filled.user_id = None
812

  
802 813
        if self.formdef.only_allow_one:
803 814
            # this is already checked in _q_index but it's done a second time
804 815
            # just before a new form is to be stored.
......
819 830
            get_session().mark_anonymous_formdata(filled)
820 831

  
821 832
        if not url:
822
            url = filled.get_url()
833
            if get_request().get_path().startswith('/backoffice/'):
834
                url = filled.get_url(backoffice=True)
835
            else:
836
                url = filled.get_url()
823 837
        return redirect(url)
824 838

  
825 839
    def keep_tracking_code(self, formdata):
......
869 883
        return get_session().get_tempfile_content(t).get_file_pointer().read()
870 884

  
871 885
    def validating(self, data):
872
        html_top(self.formdef.name)
886
        self.html_top(self.formdef.name)
873 887
        r = TemplateIO(html=True)
874 888
        r += htmltext('<div class="form-validation">')
875 889
        r += self.form_side(step_no=1, data=data)
......
892 906
        return r.getvalue()
893 907

  
894 908
    def error(self, msg):
895
        html_top(self.formdef.name)
909
        self.html_top(self.formdef.name)
896 910
        homepage = get_publisher().get_root_url()
897 911
        r = TemplateIO(html=True)
898 912
        r += htmltext('<div class="errornotice">%s</div>') % msg
......
903 917
        if not self.formdef.is_user_allowed_read(get_request().user):
904 918
            raise errors.AccessForbiddenError()
905 919
        get_response().breadcrumb.append( ('listing', _('Listing')) )
906
        html_top('%s - %s' % (_('Listing'), self.formdef.name))
920
        self.html_top('%s - %s' % (_('Listing'), self.formdef.name))
907 921
        r = TemplateIO(html=True)
908 922

  
909 923
        fields = []
wcs/qommon/static/css/dc2/admin.css
864 864
li.zone-home a { background-image: url(icons/home.large.png); }
865 865
li.zone-home a:hover { background-image: url(icons/home.large-hover.png); }
866 866

  
867
li.zone-submission a { background-image: url(icons/submission.large.png); }
868
li.zone-submission a:hover { background-image: url(icons/submission.large-hover.png); }
869

  
867 870
ul.apps li.zone-no-icon a {
868 871
	height: 29px;
869 872
	padding-top: 24px;
870
-