0001-add-possibility-to-send-action-links-in-emails-2554.patch
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 |
- |