Projet

Général

Profil

0001-add-possibility-to-send-action-links-in-emails-2554.patch

Frédéric Péters, 08 août 2018 15:00

Télécharger (15,4 ko)

Voir les différences:

Subject: [PATCH] add possibility to send "action links" in emails (#2554)

 tests/test_form_pages.py                      | 47 ++++++++++-
 tests/utilities.py                            |  5 +-
 wcs/forms/actions.py                          | 84 +++++++++++++++++++
 wcs/qommon/emails.py                          | 21 +++++
 wcs/qommon/form.py                            |  7 +-
 .../templates/qommon/email_button_link.html   |  1 +
 wcs/qommon/templatetags/qommon.py             | 21 +++++
 wcs/root.py                                   |  4 +-
 wcs/templates/wcs/action.html                 | 15 ++++
 wcs/workflows.py                              | 12 ++-
 10 files changed, 208 insertions(+), 9 deletions(-)
 create mode 100644 wcs/forms/actions.py
 create mode 100644 wcs/qommon/templates/qommon/email_button_link.html
 create mode 100644 wcs/templates/wcs/action.html
tests/test_form_pages.py
20 20
    Image = None
21 21

  
22 22
from quixote.http_request import Upload as QuixoteUpload
23
from wcs.qommon.emails import docutils
23 24
from wcs.qommon.form import UploadedFile
24 25
from wcs.qommon.ident.password_accounts import PasswordAccount
25 26
from wcs.formdef import FormDef
26 27
from wcs.workflows import (Workflow, EditableWorkflowStatusItem,
27 28
        DisplayMessageWorkflowStatusItem, WorkflowBackofficeFieldsFormDef,
28
        ChoiceWorkflowStatusItem, JumpOnSubmitWorkflowStatusItem)
29
        ChoiceWorkflowStatusItem, JumpOnSubmitWorkflowStatusItem,
30
        SendmailWorkflowStatusItem)
29 31
from wcs.wf.export_to_model import ExportToModel, transform_to_pdf
30 32
from wcs.wf.jump import JumpWorkflowStatusItem
31 33
from wcs.wf.attachment import AddAttachmentWorkflowStatusItem
......
4921 4923
    resp = resp.form.submit('submit')
4922 4924
    assert emails.get('New form2 (test condition on action)')
4923 4925

  
4926
def test_email_actions(pub, emails):
4927
    user = create_user(pub)
4928

  
4929
    workflow = Workflow.get_default_workflow()
4930
    workflow.id = '2'
4931
    # change email subjects to differentiate them
4932
    workflow.possible_status[0].items[0].subject = 'New form ([name])'
4933
    workflow.possible_status[0].items[1].subject = 'New form2 ([name])'
4934
    workflow.possible_status[0].items[1].body = 'Hello; {% action_button "do-accept" label="Accept!" %} Bye.'
4935
    workflow.possible_status[1].items[1].identifier = 'do-accept'
4936
    workflow.store()
4937

  
4938
    formdef = FormDef()
4939
    formdef.name = 'test email action'
4940
    formdef.fields = []
4941
    formdef.workflow_id = workflow.id
4942
    formdef.workflow_roles = {'_receiver': 1}
4943
    formdef.store()
4944
    formdef.data_class().wipe()
4945

  
4946
    app = login(get_app(pub), username='foo', password='foo')
4947
    resp = app.get(formdef.get_url())
4948
    resp = resp.form.submit('submit')
4949
    resp = resp.form.submit('submit')
4950
    email_data = emails.get('New form2 (test email action)')
4951
    action_url = re.findall(r'http.* ', email_data['payload'])[0].strip()
4952
    assert '/actions/' in action_url
4953
    if docutils:
4954
        assert len(email_data['payloads']) == 2
4955
        assert action_url in email_data['payloads'][1]
4956

  
4957
    app = get_app(pub)
4958
    resp = app.get(action_url)
4959
    assert 'Accept!' in resp.body
4960
    resp = resp.form.submit()
4961
    assert 'The action has been confirmed.' in resp.body
4962
    assert formdef.data_class().count() == 1
4963
    formdata = formdef.data_class().select()[0]
4964
    assert formdata.status == 'wf-accepted'
4965

  
4966
    # no longer on a correct status, action url will now return a 404
4967
    app.get(action_url, status=404)
4968

  
4924 4969
def test_manager_public_access(pub):
4925 4970
    user, manager = create_user_and_admin(pub)
4926 4971

  
tests/utilities.py
233 233
                msg = email.parser.Parser().parsestr(msg)
234 234
                subject = email.header.decode_header(msg['Subject'])[0][0]
235 235
                if msg.is_multipart():
236
                    payload = msg.get_payload()[0].get_payload(decode=True)
236
                    payloads = [x.get_payload(decode=True) for x in msg.get_payload()]
237
                    payload = payloads[0]
237 238
                else:
238 239
                    payload = msg.get_payload(decode=True)
240
                    payloads = [payload]
239 241
                self.emails[subject] = {
240 242
                        'from': msg_from,
241 243
                        'to': email.header.decode_header(msg['To'])[0][0],
242 244
                        'payload': payload,
245
                        'payloads': payloads,
243 246
                        'msg': msg,
244 247
                        }
245 248
                self.emails[subject]['email_rcpt'] = rcpts
wcs/forms/actions.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2018  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 redirect
18
from quixote.directory import Directory
19

  
20
from qommon import errors
21
from qommon import misc
22
from qommon import template
23
from qommon import tokens
24
from qommon.form import Form
25

  
26
from wcs.formdef import FormDef
27
from wcs.forms.common import FormTemplateMixin
28
from wcs.wf.jump import jump_and_perform
29

  
30

  
31
class ActionsDirectory(Directory):
32
    def _q_lookup(self, component):
33
        try:
34
            token = tokens.Token.get(component)
35
        except KeyError:
36
            raise errors.TraversalError()
37
        if token.type != 'action':
38
            raise errors.TraversalError()
39
        return ActionDirectory(token)
40

  
41

  
42
class ActionDirectory(Directory, FormTemplateMixin):
43
    _q_exports = ['']
44
    templates = ['wcs/action.html']
45

  
46
    def __init__(self, token):
47
        self.token = token
48
        self.formdef = FormDef.get_by_urlname(self.token.context['form_slug'])
49
        self.formdata = self.formdef.data_class().get(self.token.context['form_number_raw'])
50
        self.action = None
51
        status = self.formdata.get_status()
52
        for item in status.items:
53
            if getattr(item, 'identifier', None) == self.token.context['action_id']:
54
                self.action = item
55
                break
56
        else:
57
            raise errors.TraversalError()
58

  
59
    def _q_index(self):
60
        template.html_top(title=self.formdef.name)
61
        form = Form()
62
        form.add_submit('submit', misc.site_encode(self.token.context['label']))
63
        if form.is_submitted() and not form.has_errors():
64
            return self.submit()
65
        context = {
66
            'view': self,
67
            'form': form,
68
        }
69
        return template.QommonTemplateResponse(
70
                templates=list(self.get_formdef_template_variants(self.templates)),
71
                context=context)
72

  
73
    def submit(self):
74
        url = jump_and_perform(self.formdata, self.action.status)
75
        self.token.remove_self()
76
        if url:
77
            return redirect(url)
78
        context = {
79
            'view': self,
80
            'done': True,
81
        }
82
        return template.QommonTemplateResponse(
83
                templates=list(self.get_formdef_template_variants(self.templates)),
84
                context=context)
wcs/qommon/emails.py
172 172
    context = get_publisher().get_substitution_variables()
173 173
    context['email_signature'] = footer
174 174

  
175
    # handle action links/buttons
176
    button_re = re.compile(r'---===BUTTON:(?P<token>[a-zA-Z0-9]*):(?P<label>.*)===---')
177

  
178
    def get_action_url(match):
179
        return '%s/actions/%s/' % (
180
            get_publisher().get_frontoffice_url(),
181
            match.group('token'))
182

  
183
    def text_button(match):
184
        return '[%s] %s' % (match.group('label'), get_action_url(match))
185

  
186
    def html_button(match):
187
        context = {
188
            'label': match.group('label'),
189
            'url': get_action_url(match),
190
        }
191
        return render_to_string('qommon/email_button_link.html', context)
192

  
193
    text_body = button_re.sub(text_button, text_body) if text_body else None
194
    html_body = button_re.sub(html_button, html_body) if html_body else None
195

  
175 196
    if text_body:
176 197
        context['content'] = mark_safe(text_body)
177 198
        text_body = render_to_string('qommon/email_body.txt', context)
wcs/qommon/form.py
285 285
            self.captcha = CaptchaWidget('captcha', hint=hint)
286 286

  
287 287
    def add_submit(self, name, value=None, **kwargs):
288
        self.add(SubmitWidget, name, value, **kwargs)
288
        return self.add(SubmitWidget, name, value, **kwargs)
289 289

  
290 290
    def add(self, widget_class, name, *args, **kwargs):
291 291
        if kwargs and not kwargs.has_key('render_br'):
......
323 323

  
324 324
    def render_button(self, button):
325 325
        r = TemplateIO(html=True)
326
        classnames = '%s widget %s-button' % (
327
                button.__class__.__name__, button.name)
326
        classnames = '%s widget %s-button %s' % (
327
                button.__class__.__name__, button.name,
328
                getattr(button, 'extra_css_class', ''))
328 329
        r += htmltext('<div class="%s">') % classnames
329 330
        r += htmltext('<div class="content">')
330 331
        r += button.render_content()
wcs/qommon/templates/qommon/email_button_link.html
1
<a href="{{url}}">{{label}}</a>
wcs/qommon/templatetags/qommon.py
18 18
from django.utils import dateparse
19 19
from django.utils.safestring import mark_safe
20 20
from wcs.qommon import evalutils
21
from wcs.qommon import tokens
21 22
from wcs.qommon.admin.texts import TextsDirectory
22 23

  
23 24
register = template.Library()
......
60 61
@register.simple_tag
61 62
def standard_text(text_id):
62 63
    return mark_safe(TextsDirectory.get_html_text(str(text_id)))
64

  
65
@register.simple_tag(takes_context=True)
66
def action_button(context, action_id, label, delay=3):
67
    from wcs.formdef import FormDef
68
    formdata_id = context.get('form_number_raw')
69
    formdef_urlname = context.get('form_slug')
70
    if not (formdef_urlname and formdata_id):
71
        return ''
72
    formdef = FormDef.get_by_urlname(formdef_urlname)
73
    formdata = formdef.data_class().get(formdata_id, ignore_errors=True)
74
    token = tokens.Token(expiration_delay=delay*86400, size=64)
75
    token.type = 'action'
76
    token.context = {
77
        'form_slug': formdef_urlname,
78
        'form_number_raw': formdata_id,
79
        'action_id': action_id,
80
        'label': label,
81
    }
82
    token.store()
83
    return '---===BUTTON:%s:%s===---' % (token.id, label)
wcs/root.py
45 45
from wcs.api import ApiDirectory
46 46
from myspace import MyspaceDirectory
47 47
from forms.preview import PreviewDirectory
48
from wcs.forms.actions import ActionsDirectory
48 49
from wcs.scripts import Script
49 50

  
50 51
from wcs import portfolio
......
241 242
            'ident', 'register', 'afterjobs', 'themes', 'myspace', 'user', 'roles',
242 243
            'pages', ('tmp-upload', 'tmp_upload'), 'api', '__version__',
243 244
            'tryauth', 'auth', 'preview', ('reload-top', 'reload_top'),
244
            'fargo', ('i18n.js', 'i18n_js'), 'static']
245
            'fargo', ('i18n.js', 'i18n_js'), 'static', 'actions']
245 246

  
246 247
    api = ApiDirectory()
247 248
    themes = template.ThemesDirectory()
......
249 250
    pages = qommon.pages.PagesDirectory()
250 251
    fargo = portfolio.FargoDirectory()
251 252
    static = StaticsDirectory()
253
    actions = ActionsDirectory()
252 254

  
253 255
    def tryauth(self):
254 256
        return forms.root.tryauth(get_publisher().get_root_url())
wcs/templates/wcs/action.html
1
{% extends template_base %}
2
{% load i18n %}
3

  
4
{% block body %}
5
{% if done %}
6
<p>
7
{% trans "The action has been confirmed.  You can now close this window." %}
8
</p>
9
{% else %}
10
<p>
11
{% trans "Please confirm action." %}
12
</p>
13
{{ form.render|safe }}
14
{% endif %}
15
{% endblock %}
wcs/workflows.py
2046 2046
    ok_in_global_action = False
2047 2047

  
2048 2048
    label = None
2049
    identifier = None
2049 2050
    by = []
2050 2051
    backoffice_info_text = None
2051 2052
    require_confirmation = False
......
2079 2080
        label = self.compute(self.label)
2080 2081
        if not label:
2081 2082
            return
2082
        form.add_submit('button%s' % self.id, label)
2083
        widget = form.add_submit('button%s' % self.id, label)
2084
        if self.identifier:
2085
            widget.extra_css_class = 'button-%s' % self.identifier
2083 2086
        if self.require_confirmation:
2084 2087
            get_response().add_javascript(['jquery.js', '../../i18n.js', 'qommon.js'])
2085
            widget = form.get_widget('button%s' % self.id)
2086 2088
            widget.attrs = {'data-ask-for-confirmation': 'true'}
2087 2089
        form.get_widget('button%s' % self.id).backoffice_info_text = self.backoffice_info_text
2088 2090

  
......
2115 2117
            form.add(WysiwygTextWidget, '%sbackoffice_info_text' % prefix,
2116 2118
                     title=_('Information Text for Backoffice'),
2117 2119
                     value=self.backoffice_info_text)
2120
        if 'identifier' in parameters:
2121
            form.add(VarnameWidget, '%sidentifier' % prefix,
2122
                     title=_('Identifier'), value=self.identifier,
2123
                     advanced=True)
2118 2124

  
2119 2125
    def get_parameters(self):
2120 2126
        return ('label', 'by', 'status',
2121 2127
                'require_confirmation',
2122 2128
                'backoffice_info_text',
2123 2129
                'set_marker_on_status',
2124
                'condition')
2130
                'condition', 'identifier',)
2125 2131

  
2126 2132
register_item_class(ChoiceWorkflowStatusItem)
2127 2133

  
2128
-