Projet

Général

Profil

0001-workflows-add-comment-template-management-39178.patch

Lauréline Guérin, 07 décembre 2022 15:28

Télécharger (74,1 ko)

Voir les différences:

Subject: [PATCH 1/2] workflows: add comment template management (#39178)

 tests/admin_pages/test_settings.py            |  51 +-
 tests/admin_pages/test_studio.py              |  61 ++-
 tests/test_comment_template.py                | 510 ++++++++++++++++++
 tests/test_snapshots.py                       |  24 +
 wcs/admin/categories.py                       |  23 +
 wcs/admin/comment_templates.py                | 360 +++++++++++++
 wcs/admin/settings.py                         |   9 +
 wcs/admin/workflows.py                        |   4 +
 wcs/backoffice/studio.py                      |   6 +-
 wcs/categories.py                             |  21 +
 wcs/comment_templates.py                      | 138 +++++
 wcs/publisher.py                              |   2 +
 wcs/snapshots.py                              |   2 +
 .../wcs/backoffice/comment-template.html      |  39 ++
 .../wcs/backoffice/comment-templates.html     |  37 ++
 .../wcs/backoffice/settings/import.html       |  15 +
 wcs/wf/register_comment.py                    |  56 +-
 17 files changed, 1329 insertions(+), 29 deletions(-)
 create mode 100644 tests/test_comment_template.py
 create mode 100644 wcs/admin/comment_templates.py
 create mode 100644 wcs/comment_templates.py
 create mode 100644 wcs/templates/wcs/backoffice/comment-template.html
 create mode 100644 wcs/templates/wcs/backoffice/comment-templates.html
tests/admin_pages/test_settings.py
15 15

  
16 16
from wcs import fields
17 17
from wcs.api_access import ApiAccess
18
from wcs.blocks import BlockDef
18 19
from wcs.carddef import CardDef
19 20
from wcs.categories import (
20 21
    BlockCategory,
21 22
    CardDefCategory,
22 23
    Category,
24
    CommentTemplateCategory,
23 25
    DataSourceCategory,
24 26
    MailTemplateCategory,
25 27
    WorkflowCategory,
26 28
)
29
from wcs.comment_templates import CommentTemplate
27 30
from wcs.data_sources import NamedDataSource
28 31
from wcs.formdef import FormDef
32
from wcs.mail_templates import MailTemplate
29 33
from wcs.qommon.form import UploadedFile
30 34
from wcs.qommon.http_request import HTTPRequest
31 35
from wcs.wf.export_to_model import ExportToModel
......
97 101
        ApiAccess.wipe()
98 102
        BlockCategory.wipe()
99 103
        MailTemplateCategory.wipe()
104
        CommentTemplateCategory.wipe()
100 105
        DataSourceCategory.wipe()
106
        MailTemplate.wipe()
107
        CommentTemplate.wipe()
108
        BlockDef.wipe()
101 109

  
102 110
    wipe()
103 111
    create_superuser(pub)
......
132 140
    WorkflowCategory(name='foobaz').store()
133 141
    BlockCategory(name='category for blocks').store()
134 142
    MailTemplateCategory(name='category for mail templates').store()
143
    CommentTemplateCategory(name='category for mail templates').store()
135 144
    DataSourceCategory(name='category for data sources').store()
145
    MailTemplate(name='Mail templates').store()
146
    CommentTemplate(name='Comment templates').store()
136 147
    pub.role_class(name='qux').store()
137 148
    NamedDataSource(name='quux').store()
149
    BlockDef(name='blockdef').store()
138 150
    ds = NamedDataSource(name='agenda')
139 151
    ds.external = 'agenda'
140 152
    ds.store()
......
179 191
    assert 'carddef_categories/1' in filelist
180 192
    assert 'workflow_categories/1' in filelist
181 193
    assert 'block_categories/1' in filelist
182
    assert 'data_source_categories/1' in filelist
183 194
    assert 'mail_template_categories/1' in filelist
195
    assert 'comment_template_categories/1' in filelist
196
    assert 'data_source_categories/1' in filelist
184 197
    assert 'datasources/1' in filelist
185 198
    assert 'datasources/2' not in filelist  # agenda datasource, not exported
199
    assert 'mail-templates/1' in filelist
200
    assert 'comment-templates/1' in filelist
186 201
    assert 'wscalls/corge' in filelist
187 202
    assert 'apiaccess/1' in filelist
188 203
    for filename in filelist:
......
204 219
    resp.form['file'] = Upload('export.wcs', zip_content.getvalue())
205 220
    resp = resp.form.submit('submit')
206 221
    assert 'Imported successfully' in resp.text
207
    assert '1 form' in resp.text
208
    assert '1 card' in resp.text
209
    assert '2 categories' in resp.text
210
    assert '1 card category' in resp.text
211
    assert '1 workflow category' in resp.text
222
    assert '1 form</li>' in resp.text
223
    assert '1 card</li>' in resp.text
224
    assert '1 fields block</li>' in resp.text
225
    assert '1 workflow</li>' in resp.text
226
    assert '1 role</li>' in resp.text
227
    assert '2 categories</li>' in resp.text
228
    assert '1 card category</li>' in resp.text
229
    assert '1 workflow category</li>' in resp.text
230
    assert '1 block category</li>' in resp.text
231
    assert '1 mail template category</li>' in resp.text
232
    assert '1 comment template category</li>' in resp.text
233
    assert '1 data source category</li>' in resp.text
234
    assert '1 data source</li>' in resp.text
235
    assert '1 mail template</li>' in resp.text
236
    assert '1 comment template</li>' in resp.text
237
    assert '1 webservice call</li>' in resp.text
238
    assert '1 API access</li>' in resp.text
212 239
    assert FormDef.count() == 1
213 240
    assert FormDef.select()[0].url_name == 'foo'
214 241
    assert CardDef.count() == 1
215 242
    assert CardDef.select()[0].url_name == 'bar'
243
    assert BlockDef.count() == 1
244
    assert Workflow.count() == 1
245
    assert pub.role_class.count() == 1
246
    assert Category.count() == 2
247
    assert CardDefCategory.count() == 1
248
    assert WorkflowCategory.count() == 1
216 249
    assert BlockCategory.count() == 1
217 250
    assert MailTemplateCategory.count() == 1
251
    assert CommentTemplateCategory.count() == 1
218 252
    assert DataSourceCategory.count() == 1
253
    assert NamedDataSource.count() == 1
254
    assert MailTemplate.count() == 1
255
    assert CommentTemplate.count() == 1
256
    assert NamedWsCall.count() == 1
219 257
    assert ApiAccess.count() == 1
220
    assert pub.role_class.count() == 1
221 258

  
222 259
    # check roles are found by name
223 260
    wipe()
tests/admin_pages/test_studio.py
5 5

  
6 6
from wcs.blocks import BlockDef
7 7
from wcs.carddef import CardDef
8
from wcs.comment_templates import CommentTemplate
8 9
from wcs.data_sources import NamedDataSource
9 10
from wcs.formdef import FormDef
10 11
from wcs.mail_templates import MailTemplate
......
48 49
    assert '../settings/data-sources/' not in resp.text
49 50
    assert '../forms/blocks/' in resp.text
50 51
    assert '../workflows/mail-templates/' in resp.text
52
    assert '../workflows/comment-templates/' in resp.text
51 53
    assert '../settings/wscalls/' in resp.text
52 54
    assert 'Recent errors' in resp.text
53 55

  
......
136 138
    NamedDataSource.wipe()
137 139
    FormDef.wipe()
138 140
    MailTemplate.wipe()
141
    CommentTemplate.wipe()
139 142
    Workflow.wipe()
140 143
    NamedWsCall.wipe()
141 144

  
142 145
    objects = defaultdict(list)
143 146
    for i in range(6):
144
        for klass in [BlockDef, CardDef, NamedDataSource, FormDef, MailTemplate, Workflow, NamedWsCall]:
147
        for klass in [
148
            BlockDef,
149
            CardDef,
150
            NamedDataSource,
151
            FormDef,
152
            MailTemplate,
153
            CommentTemplate,
154
            Workflow,
155
            NamedWsCall,
156
        ]:
145 157
            obj = klass()
146 158
            obj.name = 'foo %s' % i
147 159
            obj.store()
148 160
            objects[klass.xml_root_node].append(obj)
149
    for klass in [BlockDef, CardDef, NamedDataSource, FormDef, MailTemplate, Workflow, NamedWsCall]:
161
    for klass in [
162
        BlockDef,
163
        CardDef,
164
        NamedDataSource,
165
        FormDef,
166
        MailTemplate,
167
        CommentTemplate,
168
        Workflow,
169
        NamedWsCall,
170
    ]:
150 171
        assert pub.snapshot_class.count(clause=[Equal('object_type', klass.xml_root_node)]) == 6
151 172
        # 2 snapshots for this one, but will be displayed only once
152 173
        objects[klass.xml_root_node][-1].name += ' bar'
......
184 205
        assert (
185 206
            'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp
186 207
        )
208
        assert (
209
            'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][i].id
210
            not in resp
211
        )
187 212
        assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp
188 213
        assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][i].id not in resp
189 214

  
190 215
    # too old
191 216
    assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][5].id not in resp
192 217
    assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id not in resp
218
    assert 'backoffice/settings/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id not in resp
193 219
    # only 5 elements
194
    assert 'backoffice/settings/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id in resp
195 220
    assert (
196 221
        'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id not in resp
197 222
    )  # not this url
......
201 226
    )
202 227
    assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][5].id in resp
203 228
    assert 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][5].id in resp
229
    assert 'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][5].id in resp
204 230
    assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][5].id in resp
205 231
    assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][5].id in resp
206 232

  
......
227 253
        assert (
228 254
            'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp
229 255
        )
256
        assert (
257
            'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][i].id
258
            not in resp
259
        )
230 260
        assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp
231 261
    # too old
232 262
    assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][5].id not in resp
263
    assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id not in resp
233 264
    # only 5 elements
234
    assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id in resp
235 265
    assert 'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id in resp
236 266
    assert (
237 267
        'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id
......
239 269
    )
240 270
    assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][5].id in resp
241 271
    assert 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][5].id in resp
272
    assert 'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][5].id in resp
242 273
    assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][5].id in resp
243 274

  
244 275
    pub.cfg['admin-permissions'] = {}
......
256 287
        assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][i].id not in resp
257 288
        assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][i].id not in resp
258 289
    # too old
259
    for i in range(4):
290
    for i in range(5):
260 291
        assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][i].id not in resp
261 292
        assert (
262 293
            'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
......
264 295
        assert (
265 296
            'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp
266 297
        )
298
        assert (
299
            'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][i].id
300
            not in resp
301
        )
267 302
        assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp
268
    # too old
269
    assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][4].id not in resp
270
    assert 'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][4].id not in resp
271
    assert 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][4].id not in resp
272 303
    # only 5 elements
273
    assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][4].id in resp
274 304
    assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id in resp
275 305
    assert 'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id in resp
276 306
    assert 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][5].id in resp
307
    assert 'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][5].id in resp
277 308
    assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][5].id in resp
278 309

  
279 310
    pub.cfg['admin-permissions'] = {}
......
296 327
        assert (
297 328
            'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp
298 329
        )
330
        assert (
331
            'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][i].id
332
            not in resp
333
        )
299 334
        assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp
300 335
    # too old
301 336
    assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][0].id not in resp
......
324 359
    pub.cfg['admin-permissions'] = {}
325 360
    pub.write_cfg()
326 361
    resp = app.get('/backoffice/studio/all-changes/')
327
    assert '(1-20/42)' in resp
362
    assert '(1-20/48)' in resp
328 363
    resp = resp.click('<!--Next Page-->')
329
    assert '21-40/42' in resp.text
364
    assert '21-40/48' in resp.text
330 365
    resp = resp.click('<!--Next Page-->')
331
    assert '41-42/42' in resp.text
366
    assert '41-48/48' in resp.text
332 367

  
333 368
    user.is_admin = False
334 369
    user.store()
tests/test_comment_template.py
1
import io
2
import os
3
import re
4
import xml.etree.ElementTree as ET
5

  
6
import pytest
7
from quixote import cleanup
8
from webtest import Upload
9

  
10
from wcs.categories import CommentTemplateCategory
11
from wcs.comment_templates import CommentTemplate
12
from wcs.fields import FileField
13
from wcs.formdef import FormDef
14
from wcs.qommon.http_request import HTTPRequest
15
from wcs.qommon.ident.password_accounts import PasswordAccount
16
from wcs.qommon.misc import indent_xml as indent
17
from wcs.qommon.upload_storage import PicklableUpload
18
from wcs.workflows import Workflow
19

  
20
from .utilities import clean_temporary_pub, create_temporary_pub, get_app, login
21

  
22

  
23
def setup_module(module):
24
    cleanup()
25

  
26

  
27
def teardown_module(module):
28
    clean_temporary_pub()
29

  
30

  
31
@pytest.fixture
32
def pub(request):
33
    pub = create_temporary_pub()
34
    req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
35
    pub.set_app_dir(req)
36
    pub._set_request(req)
37
    pub.cfg['identification'] = {'methods': ['password']}
38
    pub.write_cfg()
39
    return pub
40

  
41

  
42
@pytest.fixture
43
def superuser(pub):
44
    if pub.user_class.select(lambda x: x.name == 'admin'):
45
        user1 = pub.user_class.select(lambda x: x.name == 'admin')[0]
46
        user1.is_admin = True
47
        user1.store()
48
        return user1
49

  
50
    user1 = pub.user_class(name='admin')
51
    user1.is_admin = True
52
    user1.store()
53

  
54
    account1 = PasswordAccount(id='admin')
55
    account1.set_password('admin')
56
    account1.user_id = user1.id
57
    account1.store()
58

  
59
    return user1
60

  
61

  
62
@pytest.fixture
63
def comment_template():
64
    CommentTemplate.wipe()
65
    comment_template = CommentTemplate(name='test CT')
66
    comment_template.comment = 'test comment'
67
    comment_template.store()
68
    return comment_template
69

  
70

  
71
def test_comment_templates_basics(pub, superuser):
72
    CommentTemplateCategory.wipe()
73
    CommentTemplate.wipe()
74

  
75
    app = login(get_app(pub))
76
    resp = app.get('/backoffice/workflows/')
77
    assert 'Comment Templates' in resp
78
    resp = resp.click('Comment Templates')
79
    assert 'There are no comment templates defined.' in resp
80

  
81
    resp = resp.click('New')
82
    resp.form['name'] = 'first comment template'
83
    resp = resp.form.submit('cancel').follow()
84
    assert 'There are no comment templates defined.' in resp
85

  
86
    resp = resp.click('New')
87
    resp.form['name'] = 'first comment template'
88
    resp = resp.form.submit('submit').follow()
89
    resp.form['comment'] = 'comment body'
90
    resp = resp.form.submit('submit').follow()
91

  
92
    resp = resp.click('Edit')
93
    resp.form['comment'] = 'edited comment body'
94
    resp.form['attachments$element0'] = 'plop'
95
    resp = resp.form.submit('submit').follow()
96

  
97
    resp = resp.click('Edit')
98
    assert resp.form['comment'].value == 'edited comment body'
99
    assert resp.form['attachments$element0'].value == 'plop'
100
    resp = resp.form.submit('submit').follow()
101

  
102
    resp = resp.click('Delete')
103
    resp = resp.form.submit('cancel').follow()
104
    assert 'first comment template' in resp
105

  
106
    resp = resp.click('Delete')
107
    resp = resp.form.submit('submit').follow()
108
    assert 'first comment template' not in resp
109
    assert 'There are no comment templates defined.' in resp
110

  
111
    resp = resp.click('New')
112
    resp.form['name'] = 'first comment template'
113
    resp = resp.form.submit('submit').follow()
114
    resp.form['comment'] = 'comment body'
115
    resp = resp.form.submit('submit').follow()
116

  
117
    resp = app.get('/backoffice/workflows/')
118
    resp = resp.click('Comment Templates')
119
    assert 'first comment template' in resp
120

  
121

  
122
def test_comment_template_in_use(pub, superuser):
123
    Workflow.wipe()
124
    CommentTemplate.wipe()
125
    workflow = Workflow(name='test workflow')
126
    st1 = workflow.add_status('Status1')
127
    item = st1.add_action('register-comment')
128
    item.comment = 'Hello'
129
    workflow.store()
130

  
131
    comment_template = CommentTemplate(name='test comment template')
132
    comment_template.comment = 'test comment'
133
    comment_template.store()
134
    assert comment_template.is_in_use() is False
135

  
136
    item.comment_template = comment_template.slug
137
    workflow.store()
138
    assert comment_template.is_in_use() is True
139

  
140
    # check workflow usage is displayed
141
    app = login(get_app(pub))
142
    resp = app.get('/backoffice/workflows/comment-templates/%s/' % comment_template.id)
143
    assert 'Usage in workflows' in resp.text
144
    assert 'test workflow' in resp.text
145
    resp.click('test workflow')  # make sure the link is ok
146

  
147
    resp = resp.click('Delete')
148
    assert 'still used' in resp.text
149

  
150

  
151
def test_admin_workflow_edit(pub, superuser):
152
    CommentTemplateCategory.wipe()
153
    Workflow.wipe()
154
    CommentTemplate.wipe()
155
    comment_template = CommentTemplate(name='test comment template')
156
    comment_template.comment = 'test comment'
157
    comment_template.store()
158

  
159
    workflow = Workflow(name='test comment template')
160
    st1 = workflow.add_status('Status1')
161
    item = st1.add_action('register-comment')
162
    item.comment = 'Hello'
163
    workflow.store()
164

  
165
    app = login(get_app(pub))
166
    resp = app.get('/backoffice/workflows/%s/status/%s/items/1/' % (workflow.id, st1.id))
167
    assert [o[0] for o in resp.form['comment_template'].options] == ['', 'test-comment-template']
168

  
169
    cat_b = CommentTemplateCategory(name='Cat B')
170
    cat_b.store()
171
    comment_template = CommentTemplate(name='foo bar')
172
    comment_template.category_id = cat_b.id
173
    comment_template.store()
174
    comment_template = CommentTemplate(name='bar foo')
175
    comment_template.category_id = cat_b.id
176
    comment_template.store()
177
    cat_a = CommentTemplateCategory(name='Cat A')
178
    cat_a.store()
179
    comment_template = CommentTemplate(name='foo baz')
180
    comment_template.category_id = cat_a.id
181
    comment_template.store()
182

  
183
    resp = app.get('/backoffice/workflows/%s/status/%s/items/1/' % (workflow.id, st1.id))
184
    assert [o[0] for o in resp.form['comment_template'].options] == [
185
        '',
186
        'foo-baz',
187
        'bar-foo',
188
        'foo-bar',
189
        'test-comment-template',
190
    ]
191
    resp.form['comment_template'] = 'test-comment-template'
192
    resp = resp.form.submit('submit')
193

  
194
    workflow = Workflow.get(workflow.id)
195
    assert workflow.possible_status[0].items[0].comment_template == 'test-comment-template'
196

  
197

  
198
def test_comment_templates_category(pub, superuser):
199
    CommentTemplateCategory.wipe()
200
    CommentTemplate.wipe()
201

  
202
    app = login(get_app(pub))
203
    resp = app.get('/backoffice/workflows/comment-templates/new')
204
    assert 'category_id' not in resp.form.fields
205

  
206
    comment_template = CommentTemplate(name='foo')
207
    comment_template.store()
208

  
209
    resp = app.get('/backoffice/workflows/comment-templates/categories/')
210
    resp = resp.click('New Category')
211
    resp.form['name'] = 'a new category'
212
    resp.form['description'] = 'description of the category'
213
    resp = resp.form.submit('submit')
214
    assert CommentTemplateCategory.count() == 1
215
    category = CommentTemplateCategory.select()[0]
216
    assert category.name == 'a new category'
217

  
218
    resp = app.get('/backoffice/workflows/comment-templates/new')
219
    resp.form['name'] = 'template 2'
220
    resp = resp.form.submit('submit').follow()
221
    assert CommentTemplate.count() == 2
222
    assert CommentTemplate.get(2).category_id is None
223
    resp = app.get('/backoffice/workflows/comment-templates/new')
224
    resp.form['name'] = 'template 3'
225
    resp.form['category_id'] = str(category.id)
226
    resp = resp.form.submit('submit').follow()
227
    assert CommentTemplate.count() == 3
228
    assert CommentTemplate.get(3).category_id == str(category.id)
229

  
230
    resp = app.get('/backoffice/workflows/comment-templates/%s/' % comment_template.id)
231
    resp = resp.click(href=re.compile('^edit$'))
232
    resp.form['category_id'] = str(category.id)
233
    resp = resp.form.submit('cancel').follow()
234
    comment_template.refresh_from_storage()
235
    assert comment_template.category_id is None
236

  
237
    resp = app.get('/backoffice/workflows/comment-templates/%s/' % comment_template.id)
238
    resp = resp.click(href=re.compile('^edit$'))
239
    resp.form['category_id'] = str(category.id)
240
    resp.form['comment'] = 'comment body'
241
    resp = resp.form.submit('submit').follow()
242
    comment_template.refresh_from_storage()
243
    assert str(comment_template.category_id) == str(category.id)
244

  
245
    resp = app.get('/backoffice/workflows/comment-templates/%s/' % comment_template.id)
246
    resp = resp.click(href=re.compile('^edit$'))
247
    assert resp.form['category_id'].value == str(category.id)
248

  
249
    resp = app.get('/backoffice/workflows/comment-templates/categories/')
250
    resp = resp.click('New Category')
251
    resp.form['name'] = 'a second category'
252
    resp.form['description'] = 'description of the category'
253
    resp = resp.form.submit('submit')
254
    assert CommentTemplateCategory.count() == 2
255
    category2 = [x for x in CommentTemplateCategory.select() if x.id != category.id][0]
256
    assert category2.name == 'a second category'
257

  
258
    app.get(
259
        '/backoffice/workflows/comment-templates/categories/update_order?order=%s;%s;'
260
        % (category2.id, category.id)
261
    )
262
    categories = CommentTemplateCategory.select()
263
    CommentTemplateCategory.sort_by_position(categories)
264
    assert [x.id for x in categories] == [str(category2.id), str(category.id)]
265

  
266
    app.get(
267
        '/backoffice/workflows/comment-templates/categories/update_order?order=%s;%s;0'
268
        % (category.id, category2.id)
269
    )
270
    categories = CommentTemplateCategory.select()
271
    CommentTemplateCategory.sort_by_position(categories)
272
    assert [x.id for x in categories] == [str(category.id), str(category2.id)]
273

  
274
    resp = app.get('/backoffice/workflows/comment-templates/categories/')
275
    resp = resp.click('a new category')
276
    resp = resp.click('Delete')
277
    resp = resp.form.submit()
278
    comment_template.refresh_from_storage()
279
    assert not comment_template.category_id
280

  
281

  
282
def test_workflow_register_comment_template(pub):
283
    Workflow.wipe()
284
    CommentTemplate.wipe()
285

  
286
    comment_template = CommentTemplate(name='test comment template')
287
    comment_template.comment = 'test comment'
288
    comment_template.store()
289

  
290
    workflow = Workflow(name='test comment template')
291
    st1 = workflow.add_status('Status1')
292
    item = st1.add_action('register-comment')
293
    item.comment = 'Hello'
294
    item.comment_template = comment_template.slug
295
    workflow.store()
296

  
297
    formdef = FormDef()
298
    formdef.name = 'baz'
299
    formdef.fields = []
300
    formdef.store()
301

  
302
    formdata = formdef.data_class()()
303
    formdata.just_created()
304
    formdata.store()
305

  
306
    item.perform(formdata)
307
    assert len(formdata.evolution) == 1
308
    assert len(formdata.evolution[0].parts) == 2
309
    assert formdata.evolution[-1].parts[1].content == 'test comment'
310

  
311
    # check nothing is registered and an error is logged if the comment template is missing
312
    CommentTemplate.wipe()
313
    item.perform(formdata)
314
    assert len(formdata.evolution) == 1
315
    assert len(formdata.evolution[0].parts) == 2
316
    assert pub.loggederror_class.count() == 1
317
    logged_error = pub.loggederror_class.select()[0]
318
    assert (
319
        logged_error.summary
320
        == 'reference to invalid comment template test-comment-template in status Status1'
321
    )
322

  
323

  
324
def test_workflow_register_comment_template_attachments(pub):
325
    Workflow.wipe()
326
    CommentTemplate.wipe()
327

  
328
    comment_template = CommentTemplate(name='test comment template')
329
    comment_template.comment = 'test comment'
330
    comment_template.attachments = ['form_var_file1_raw']
331
    comment_template.store()
332

  
333
    workflow = Workflow(name='test comment template')
334
    st1 = workflow.add_status('Status1')
335
    item = st1.add_action('register-comment')
336
    item.comment = 'Hello'
337
    item.comment_template = comment_template.slug
338
    workflow.store()
339

  
340
    formdef = FormDef()
341
    formdef.name = 'baz'
342
    formdef.fields = [
343
        FileField(id='1', label='File', type='file', varname='file1'),
344
    ]
345
    formdef.store()
346

  
347
    upload = PicklableUpload('test.jpeg', 'image/jpeg')
348
    with open(os.path.join(os.path.dirname(__file__), 'image-with-gps-data.jpeg'), 'rb') as fd:
349
        upload.receive([fd.read()])
350
    formdata = formdef.data_class()()
351
    formdata.data = {'1': upload}
352
    formdata.just_created()
353
    formdata.store()
354
    pub.substitutions.feed(formdata)
355

  
356
    item.perform(formdata)
357
    assert len(formdata.evolution) == 1
358
    assert len(formdata.evolution[0].parts) == 3
359
    assert formdata.evolution[-1].parts[2].content == 'test comment'
360
    assert formdata.evolution[-1].parts[1].base_filename == 'test.jpeg'
361

  
362
    # check two files are sent if attachments are also defined on the action itself.
363
    item.attachments = ['form_var_file1_raw']
364
    item.perform(formdata)
365
    assert len(formdata.evolution) == 1
366
    assert len(formdata.evolution[0].parts) == 6
367
    assert formdata.evolution[-1].parts[5].content == 'test comment'
368
    assert formdata.evolution[-1].parts[4].base_filename == 'test.jpeg'
369
    assert formdata.evolution[-1].parts[3].base_filename == 'test.jpeg'
370

  
371

  
372
def test_workflow_register_comment_template_empty(pub):
373
    Workflow.wipe()
374
    CommentTemplate.wipe()
375

  
376
    comment_template = CommentTemplate(name='test comment template')
377
    comment_template.comment = None
378
    comment_template.store()
379

  
380
    workflow = Workflow(name='test comment template')
381
    st1 = workflow.add_status('Status1')
382
    item = st1.add_action('register-comment')
383
    item.comment = 'Hello'
384
    item.comment_template = comment_template.slug
385
    workflow.store()
386

  
387
    formdef = FormDef()
388
    formdef.name = 'baz'
389
    formdef.store()
390

  
391
    formdata = formdef.data_class()()
392
    formdata.data = {}
393
    formdata.just_created()
394
    formdata.store()
395
    pub.substitutions.feed(formdata)
396

  
397
    item.perform(formdata)
398
    assert len(formdata.evolution) == 1
399
    assert len(formdata.evolution[0].parts) == 1
400

  
401

  
402
def test_comment_templates_export(pub, superuser, comment_template):
403
    app = login(get_app(pub))
404
    resp = app.get('/backoffice/workflows/comment-templates/1/')
405

  
406
    resp = resp.click(href='export')
407
    xml_export = resp.text
408

  
409
    ds = io.StringIO(xml_export)
410
    comment_template2 = CommentTemplate.import_from_xml(ds)
411
    assert comment_template2.name == 'test CT'
412

  
413

  
414
def test_comment_templates_import(pub, superuser, comment_template):
415
    comment_template.slug = 'foobar'
416
    comment_template.store()
417
    comment_template_xml = ET.tostring(comment_template.export_to_xml(include_id=True))
418
    CommentTemplate.wipe()
419
    assert CommentTemplate.count() == 0
420

  
421
    app = login(get_app(pub))
422
    resp = app.get('/backoffice/workflows/comment-templates/')
423
    resp = resp.click(href='import')
424
    resp.forms[0]['file'] = Upload('comment_template.wcs', comment_template_xml)
425
    resp = resp.forms[0].submit()
426
    assert CommentTemplate.count() == 1
427
    assert {wc.slug for wc in CommentTemplate.select()} == {'foobar'}
428

  
429
    # check slug
430
    resp = app.get('/backoffice/workflows/comment-templates/')
431
    resp = resp.click(href='import')
432
    resp.forms[0]['file'] = Upload('comment_template.wcs', comment_template_xml)
433
    resp = resp.forms[0].submit()
434
    assert CommentTemplate.count() == 2
435
    assert {wc.slug for wc in CommentTemplate.select()} == {'foobar', 'test-ct'}
436
    resp = app.get('/backoffice/workflows/comment-templates/')
437
    resp = resp.click(href='import')
438
    resp.forms[0]['file'] = Upload('comment_template.wcs', comment_template_xml)
439
    resp = resp.forms[0].submit()
440
    assert CommentTemplate.count() == 3
441
    assert {wc.slug for wc in CommentTemplate.select()} == {'foobar', 'test-ct', 'test-ct-1'}
442

  
443
    # import an invalid file
444
    resp = app.get('/backoffice/workflows/comment-templates/')
445
    resp = resp.click(href='import')
446
    resp.form['file'] = Upload('comment_template.wcs', b'garbage')
447
    resp = resp.form.submit()
448
    assert 'Invalid File' in resp.text
449

  
450

  
451
def test_comment_templates_duplicate(pub, superuser, comment_template):
452
    app = login(get_app(pub))
453
    resp = app.get('/backoffice/workflows/comment-templates/1/')
454

  
455
    resp = resp.click(href='duplicate')
456
    assert resp.form['name'].value == 'test CT (copy)'
457
    resp = resp.form.submit('cancel').follow()
458
    assert CommentTemplate.count() == 1
459

  
460
    resp = resp.click(href='duplicate')
461
    assert resp.form['name'].value == 'test CT (copy)'
462
    resp = resp.form.submit('submit').follow()
463
    assert CommentTemplate.count() == 2
464

  
465
    resp = app.get('/backoffice/workflows/comment-templates/1/')
466
    resp = resp.click(href='duplicate')
467
    assert resp.form['name'].value == 'test CT (copy 2)'
468
    resp.form['name'].value = 'other copy'
469
    resp = resp.form.submit('submit').follow()
470
    assert CommentTemplate.count() == 3
471
    assert {x.name for x in CommentTemplate.select()} == {'test CT', 'test CT (copy)', 'other copy'}
472
    assert {x.slug for x in CommentTemplate.select()} == {'test-ct', 'test-ct-copy', 'other-copy'}
473

  
474

  
475
def export_to_indented_xml(comment_template, include_id=False):
476
    comment_template_xml = comment_template.export_to_xml(include_id=include_id)
477
    indent(comment_template_xml)
478
    return comment_template_xml
479

  
480

  
481
def assert_import_export_works(comment_template, include_id=False):
482
    comment_template2 = CommentTemplate.import_from_xml_tree(
483
        ET.fromstring(ET.tostring(comment_template.export_to_xml(include_id))), include_id
484
    )
485
    assert ET.tostring(export_to_indented_xml(comment_template)) == ET.tostring(
486
        export_to_indented_xml(comment_template2)
487
    )
488
    return comment_template2
489

  
490

  
491
def test_comment_template(pub):
492
    comment_template = CommentTemplate(name='test')
493
    assert_import_export_works(comment_template, include_id=True)
494

  
495

  
496
def test_comment_template_with_category(pub):
497
    category = CommentTemplateCategory(name='test category')
498
    category.store()
499

  
500
    comment_template = CommentTemplate(name='test category')
501
    comment_template.category_id = category.id
502
    comment_template.store()
503
    comment_template2 = assert_import_export_works(comment_template, include_id=True)
504
    assert comment_template2.category_id == comment_template.category_id
505

  
506
    # import with non existing category
507
    CommentTemplateCategory.wipe()
508
    export = ET.tostring(comment_template.export_to_xml(include_id=True))
509
    comment_template3 = CommentTemplate.import_from_xml_tree(ET.fromstring(export), include_id=True)
510
    assert comment_template3.category_id is None
tests/test_snapshots.py
9 9
from wcs.blocks import BlockDef
10 10
from wcs.carddef import CardDef
11 11
from wcs.categories import Category
12
from wcs.comment_templates import CommentTemplate
12 13
from wcs.data_sources import NamedDataSource
13 14
from wcs.fields import CommentField, ItemField, PageField, StringField
14 15
from wcs.formdef import FormDef
......
1133 1134
        resp = resp.click('Edit')
1134 1135

  
1135 1136

  
1137
def test_comment_template_snapshot_browse(pub):
1138
    create_superuser(pub)
1139
    create_role(pub)
1140

  
1141
    CommentTemplate.wipe()
1142
    comment_template = CommentTemplate(name='test')
1143
    comment_template.store()
1144
    assert pub.snapshot_class.count() == 1
1145
    # check calling .store() without changes doesn't create snapshots
1146
    comment_template.store()
1147
    assert pub.snapshot_class.count() == 1
1148

  
1149
    app = login(get_app(pub))
1150

  
1151
    resp = app.get('/backoffice/workflows/comment-templates/%s/history/' % comment_template.id)
1152
    snapshot = pub.snapshot_class.select_object_history(comment_template)[0]
1153
    resp = resp.click(href='%s/view/' % snapshot.id)
1154
    assert 'This comment template is readonly' in resp.text
1155
    assert '<p>%s</p>' % localstrftime(snapshot.timestamp) in resp.text
1156
    with pytest.raises(IndexError):
1157
        resp = resp.click('Edit')
1158

  
1159

  
1136 1160
def test_category_snapshot_browse(pub):
1137 1161
    create_superuser(pub)
1138 1162
    create_role(pub)
wcs/admin/categories.py
26 26
    BlockCategory,
27 27
    CardDefCategory,
28 28
    Category,
29
    CommentTemplateCategory,
29 30
    DataSourceCategory,
30 31
    MailTemplateCategory,
31 32
    WorkflowCategory,
32 33
)
34
from wcs.comment_templates import CommentTemplate
33 35
from wcs.data_sources import NamedDataSource
34 36
from wcs.formdef import FormDef
35 37
from wcs.mail_templates import MailTemplate
......
177 179
    management_roles_hint_text = None
178 180

  
179 181

  
182
class CommentTemplateCategoryUI(CategoryUI):
183
    category_class = CommentTemplateCategory
184
    management_roles_hint_text = None
185

  
186

  
180 187
class DataSourceCategoryUI(CategoryUI):
181 188
    category_class = DataSourceCategory
182 189
    management_roles_hint_text = None
......
334 341
    empty_message = _('No mail template associated to this category.')
335 342

  
336 343

  
344
class CommentTemplateCategoryPage(CategoryPage):
345
    category_class = CommentTemplateCategory
346
    category_ui_class = CommentTemplateCategoryUI
347
    object_class = CommentTemplate
348
    usage_title = _('Comment templates in this category')
349
    empty_message = _('No comment template associated to this category.')
350

  
351

  
337 352
class DataSourceCategoryPage(CategoryPage):
338 353
    category_class = DataSourceCategory
339 354
    category_ui_class = DataSourceCategoryUI
......
452 467
    category_explanation = _('Categories are used to sort the different mail templates.')
453 468

  
454 469

  
470
class CommentTemplateCategoriesDirectory(CategoriesDirectory):
471
    base_section = 'workflows'
472
    category_class = CommentTemplateCategory
473
    category_ui_class = CommentTemplateCategoryUI
474
    category_page_class = CommentTemplateCategoryPage
475
    category_explanation = _('Categories are used to sort the different comment templates.')
476

  
477

  
455 478
class DataSourceCategoriesDirectory(CategoriesDirectory):
456 479
    base_section = 'workflows'
457 480
    category_class = DataSourceCategory
wcs/admin/comment_templates.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2022  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_response, redirect
18
from quixote.directory import Directory
19
from quixote.html import TemplateIO, htmltext
20

  
21
from wcs.admin import utils
22
from wcs.admin.categories import CommentTemplateCategoriesDirectory, get_categories
23
from wcs.backoffice.snapshots import SnapshotsDirectory
24
from wcs.categories import CommentTemplateCategory
25
from wcs.comment_templates import CommentTemplate
26
from wcs.qommon import _, errors, misc, template
27
from wcs.qommon.backoffice.menu import html_top
28
from wcs.qommon.form import (
29
    ComputedExpressionWidget,
30
    FileWidget,
31
    Form,
32
    HtmlWidget,
33
    SingleSelectWidget,
34
    SlugWidget,
35
    StringWidget,
36
    TextWidget,
37
    WidgetList,
38
    get_session,
39
)
40

  
41

  
42
class CommentTemplatesDirectory(Directory):
43
    _q_exports = ['', 'new', 'categories', ('import', 'p_import')]
44
    do_not_call_in_templates = True
45
    categories = CommentTemplateCategoriesDirectory()
46

  
47
    def _q_traverse(self, path):
48
        if not get_publisher().get_backoffice_root().is_global_accessible('workflows'):
49
            raise errors.AccessForbiddenError()
50
        get_response().breadcrumb.append(('comment-templates/', _('Comment Templates')))
51
        return super()._q_traverse(path)
52

  
53
    def _q_lookup(self, component):
54
        return CommentTemplatePage(component)
55

  
56
    def _q_index(self):
57
        html_top('comment_templates', title=_('Comment Templates'))
58
        categories = CommentTemplateCategory.select()
59
        CommentTemplateCategory.sort_by_position(categories)
60
        comment_templates = CommentTemplate.select(order_by='name')
61
        if categories:
62
            categories.append(CommentTemplateCategory(_('Misc')))
63
            for category in categories:
64
                category.comment_templates = [x for x in comment_templates if x.category_id == category.id]
65
        return template.QommonTemplateResponse(
66
            templates=['wcs/backoffice/comment-templates.html'],
67
            context={'view': self, 'comment_templates': comment_templates, 'categories': categories},
68
        )
69

  
70
    def new(self):
71
        form = Form(enctype='multipart/form-data')
72
        form.add(StringWidget, 'name', title=_('Name'), required=True, size=50)
73
        category_options = get_categories(CommentTemplateCategory)
74
        if category_options:
75
            category_options = [(None, '---', '')] + list(category_options)
76
            form.add(
77
                SingleSelectWidget,
78
                'category_id',
79
                title=_('Category'),
80
                options=category_options,
81
            )
82
        form.add_submit('submit', _('Add'))
83
        form.add_submit('cancel', _('Cancel'))
84
        if form.get_widget('cancel').parse():
85
            return redirect('.')
86

  
87
        if form.is_submitted() and not form.has_errors():
88
            comment_template = CommentTemplate(name=form.get_widget('name').parse())
89
            if form.get_widget('category_id'):
90
                comment_template.category_id = form.get_widget('category_id').parse()
91
            comment_template.store()
92
            return redirect('%s/edit' % comment_template.id)
93

  
94
        get_response().breadcrumb.append(('new', _('New Comment Template')))
95
        html_top('comment_templates', title=_('New Comment Template'))
96
        r = TemplateIO(html=True)
97
        r += htmltext('<h2>%s</h2>') % _('New Comment Template')
98
        r += form.render()
99
        return r.getvalue()
100

  
101
    def p_import(self):
102
        form = Form(enctype='multipart/form-data')
103
        import_title = _('Import Comment Template')
104

  
105
        form.add(FileWidget, 'file', title=_('File'), required=True)
106
        form.add_submit('submit', import_title)
107
        form.add_submit('cancel', _('Cancel'))
108

  
109
        if form.get_submit() == 'cancel':
110
            return redirect('.')
111

  
112
        if form.is_submitted() and not form.has_errors():
113
            try:
114
                return self.import_submit(form)
115
            except ValueError:
116
                pass
117

  
118
        get_response().breadcrumb.append(('import', _('Import')))
119
        html_top('comment_templates', title=import_title)
120
        r = TemplateIO(html=True)
121
        r += htmltext('<h2>%s</h2>') % import_title
122
        r += htmltext('<p>%s</p>') % _('You can install a new comment template by uploading a file.')
123
        r += form.render()
124
        return r.getvalue()
125

  
126
    def import_submit(self, form):
127
        fp = form.get_widget('file').parse().fp
128

  
129
        error = False
130
        try:
131
            comment_template = CommentTemplate.import_from_xml(fp)
132
            get_session().message = ('info', _('This comment template has been successfully imported.'))
133
        except ValueError:
134
            error = True
135

  
136
        if error:
137
            form.set_error('file', _('Invalid File'))
138
            raise ValueError()
139

  
140
        # check slug unicity
141
        known_slugs = {
142
            x.slug: x.id for x in CommentTemplate.select(ignore_migration=True, ignore_errors=True)
143
        }
144
        if comment_template.slug in known_slugs:
145
            comment_template.slug = None  # a new one will be set in .store()
146
        comment_template.store()
147
        return redirect('%s/' % comment_template.id)
148

  
149

  
150
class CommentTemplatePage(Directory):
151
    _q_exports = [
152
        '',
153
        'edit',
154
        'delete',
155
        'duplicate',
156
        'export',
157
        ('history', 'snapshots_dir'),
158
    ]
159
    do_not_call_in_templates = True
160

  
161
    def __init__(self, component, instance=None):
162
        try:
163
            self.comment_template = instance or CommentTemplate.get(component)
164
        except KeyError:
165
            raise errors.TraversalError()
166
        get_response().breadcrumb.append((component + '/', self.comment_template.name))
167
        self.snapshots_dir = SnapshotsDirectory(self.comment_template)
168

  
169
    def get_sidebar(self):
170
        r = TemplateIO(html=True)
171
        if self.comment_template.is_readonly():
172
            r += htmltext('<div class="infonotice"><p>%s</p></div>') % _('This comment template is readonly.')
173
            r += utils.snapshot_info_block(snapshot=self.comment_template.snapshot_object)
174
        r += htmltext('<ul id="sidebar-actions">')
175
        if not self.comment_template.is_readonly():
176
            r += htmltext('<li><a href="export">%s</a></li>') % _('Export')
177
            r += htmltext('<li><a href="duplicate" rel="popup">%s</a></li>') % _('Duplicate')
178
            r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
179
            if get_publisher().snapshot_class:
180
                r += htmltext('<li><a rel="popup" href="history/save">%s</a></li>') % _('Save snapshot')
181
                r += htmltext('<li><a href="history/">%s</a></li>') % _('History')
182
        r += htmltext('</ul>')
183
        return r.getvalue()
184

  
185
    def _q_index(self):
186
        html_top('comment_templates', title=self.comment_template.name)
187
        get_response().filter['sidebar'] = self.get_sidebar()
188
        return template.QommonTemplateResponse(
189
            templates=['wcs/backoffice/comment-template.html'],
190
            context={'view': self, 'comment_template': self.comment_template},
191
        )
192

  
193
    def get_form(self):
194
        form = Form(enctype='multipart/form-data', use_tabs=True)
195
        form.add(
196
            StringWidget, 'name', title=_('Name'), required=True, size=30, value=self.comment_template.name
197
        )
198
        category_options = get_categories(CommentTemplateCategory)
199
        if category_options:
200
            category_options = [(None, '---', '')] + list(category_options)
201
            form.add(
202
                SingleSelectWidget,
203
                'category_id',
204
                title=_('Category'),
205
                options=category_options,
206
                value=self.comment_template.category_id,
207
            )
208

  
209
        form.add(
210
            TextWidget,
211
            'description',
212
            title=_('Description'),
213
            cols=80,
214
            rows=3,
215
            value=self.comment_template.description,
216
        )
217
        form.add(
218
            TextWidget,
219
            'comment',
220
            title=_('Comment'),
221
            value=self.comment_template.comment,
222
            cols=80,
223
            rows=15,
224
            require=True,
225
            validation_function=ComputedExpressionWidget.validate_template,
226
        )
227

  
228
        if self.comment_template.slug and not self.comment_template.is_in_use():
229
            form.add(
230
                SlugWidget,
231
                'slug',
232
                value=self.comment_template.slug,
233
                advanced=True,
234
            )
235

  
236
        form.add(
237
            WidgetList,
238
            'attachments',
239
            title=_('Attachments (templates or Python expressions)'),
240
            element_type=StringWidget,
241
            value=self.comment_template.attachments,
242
            add_element_label=_('Add attachment'),
243
            element_kwargs={'render_br': False, 'size': 50},
244
            advanced=True,
245
        )
246

  
247
        if not self.comment_template.is_readonly():
248
            form.add_submit('submit', _('Submit'))
249
        form.add_submit('cancel', _('Cancel'))
250
        return form
251

  
252
    def submit_form(self, form):
253
        name = form.get_widget('name').parse()
254
        slug_widget = form.get_widget('slug')
255
        if slug_widget:
256
            slug = form.get_widget('slug').parse()
257

  
258
        for comment_template in CommentTemplate.select():
259
            if comment_template.id == self.comment_template.id:
260
                continue
261
            if slug_widget and slug == comment_template.slug:
262
                slug_widget.set_error(_('This value is already used.'))
263
        if form.has_errors():
264
            raise ValueError()
265

  
266
        self.comment_template.name = name
267
        if form.get_widget('category_id'):
268
            self.comment_template.category_id = form.get_widget('category_id').parse()
269
        self.comment_template.description = form.get_widget('description').parse()
270
        self.comment_template.comment = form.get_widget('comment').parse()
271
        self.comment_template.attachments = form.get_widget('attachments').parse()
272
        if slug_widget:
273
            self.comment_template.slug = slug
274
        self.comment_template.store()
275

  
276
    def edit(self):
277
        form = self.get_form()
278
        if form.get_submit() == 'cancel':
279
            return redirect('.')
280

  
281
        if form.get_submit() == 'submit' and not form.has_errors():
282
            try:
283
                self.submit_form(form)
284
            except ValueError:
285
                pass
286
            else:
287
                return redirect('.')
288

  
289
        get_response().breadcrumb.append(('edit', _('Edit')))
290
        html_top('comment_templates', title=_('Edit Comment Template'))
291
        r = TemplateIO(html=True)
292
        r += htmltext('<h2>%s</h2>') % _('Edit Comment Template')
293
        r += form.render()
294
        r += get_publisher().substitutions.get_substitution_html_table()
295

  
296
        return r.getvalue()
297

  
298
    def delete(self):
299
        form = Form(enctype='multipart/form-data')
300
        if not self.comment_template.is_in_use():
301
            form.widgets.append(
302
                HtmlWidget('<p>%s</p>' % _('You are about to irrevocably delete this comment template.'))
303
            )
304
            form.add_submit('delete', _('Submit'))
305
        else:
306
            form.widgets.append(
307
                HtmlWidget('<p>%s</p>' % _('This comment template is still used, it cannot be deleted.'))
308
            )
309
        form.add_submit('cancel', _('Cancel'))
310
        if form.get_widget('cancel').parse():
311
            return redirect('.')
312
        if not form.is_submitted() or form.has_errors():
313
            get_response().breadcrumb.append(('delete', _('Delete')))
314
            html_top('comment_templates', title=_('Delete Comment Template'))
315
            r = TemplateIO(html=True)
316
            r += htmltext('<h2>%s %s</h2>') % (_('Deleting Comment Template:'), self.comment_template.name)
317
            r += form.render()
318
            return r.getvalue()
319
        else:
320
            self.comment_template.remove_self()
321
            return redirect('..')
322

  
323
    def export(self):
324
        return misc.xml_response(
325
            self.comment_template,
326
            filename='comment-template-%s.wcs' % self.comment_template.slug,
327
            content_type='application/x-wcs-comment-template',
328
        )
329

  
330
    def duplicate(self):
331
        form = Form(enctype='multipart/form-data')
332
        name_widget = form.add(StringWidget, 'name', title=_('Name'), required=True, size=30)
333
        form.add_submit('submit', _('Submit'))
334
        form.add_submit('cancel', _('Cancel'))
335
        if form.get_widget('cancel').parse():
336
            return redirect('.')
337

  
338
        if not form.is_submitted():
339
            original_name = self.comment_template.name
340
            new_name = '%s %s' % (original_name, _('(copy)'))
341
            names = [x.name for x in CommentTemplate.select()]
342
            no = 2
343
            while new_name in names:
344
                new_name = _('%(name)s (copy %(no)d)') % {'name': original_name, 'no': no}
345
                no += 1
346
            name_widget.set_value(new_name)
347

  
348
        if not form.is_submitted() or form.has_errors():
349
            html_top('comment_templates', title=_('Duplicate Comment Template'))
350
            r = TemplateIO(html=True)
351
            get_response().breadcrumb.append(('duplicate', _('Duplicate')))
352
            r += htmltext('<h2>%s</h2>') % _('Duplicate Comment Template')
353
            r += form.render()
354
            return r.getvalue()
355

  
356
        self.comment_template.id = None
357
        self.comment_template.slug = None
358
        self.comment_template.name = form.get_widget('name').parse()
359
        self.comment_template.store()
360
        return redirect('../%s/' % self.comment_template.id)
wcs/admin/settings.py
731 731
        form.add(CheckboxWidget, 'workflow_categories', title=_('Workflow Categories'), value=True)
732 732
        form.add(CheckboxWidget, 'block_categories', title=_('Fields Blocks Categories'), value=True)
733 733
        form.add(CheckboxWidget, 'mail_template_categories', title=_('Mail Templates Categories'), value=True)
734
        form.add(
735
            CheckboxWidget, 'comment_template_categories', title=_('Comment Templates Categories'), value=True
736
        )
734 737
        form.add(CheckboxWidget, 'data_source_categories', title=_('Data Sources Categories'), value=True)
735 738
        form.add(CheckboxWidget, 'settings', title=_('Settings'), value=False)
736 739
        form.add(CheckboxWidget, 'datasources', title=_('Data sources'), value=True)
737 740
        form.add(CheckboxWidget, 'mail-templates', title=_('Mail templates'), value=True)
741
        form.add(CheckboxWidget, 'comment-templates', title=_('Comment templates'), value=True)
738 742
        form.add(CheckboxWidget, 'wscalls', title=_('Webservice calls'), value=True)
739 743
        form.add(CheckboxWidget, 'apiaccess', title=_('API access'), value=True)
740 744
        form.add_submit('submit', _('Submit'))
......
766 770
                            'workflow_categories',
767 771
                            'block_categories',
768 772
                            'mail_template_categories',
773
                            'comment_template_categories',
769 774
                            'data_source_categories',
770 775
                            'wscalls',
771 776
                            'mail-templates',
777
                            'comment-templates',
772 778
                            'apiaccess',
773 779
                        ):
774 780
                            continue
......
864 870
            'workflow_categories',
865 871
            'block_categories',
866 872
            'mail_template_categories',
873
            'comment_template_categories',
867 874
            'data_source_categories',
868 875
            'datasources',
869 876
            'wscalls',
870 877
            'mail-templates',
878
            'comment-templates',
871 879
            'blockdefs',
872 880
            'apiaccess',
873 881
        ):
......
927 935
            try:
928 936
                results = self.import_submit(form)
929 937
                results['mail_templates'] = results['mail-templates']
938
                results['comment_templates'] = results['comment-templates']
930 939
            except zipfile.BadZipfile:
931 940
                results = None
932 941
                reason = _('Not a valid export file')
wcs/admin/workflows.py
63 63
)
64 64

  
65 65
from . import utils
66
from .comment_templates import CommentTemplatesDirectory
66 67
from .data_sources import NamedDataSourcesDirectory
67 68
from .fields import FieldDefPage, FieldsDirectory
68 69
from .logged_errors import LoggedErrorsDirectory
......
1944 1945
        ('import', 'p_import'),
1945 1946
        ('data-sources', 'data_sources'),
1946 1947
        ('mail-templates', 'mail_templates'),
1948
        ('comment-templates', 'comment_templates'),
1947 1949
    ]
1948 1950

  
1949 1951
    data_sources = NamedDataSourcesDirectoryInWorkflows()
1950 1952
    mail_templates = MailTemplatesDirectory()
1953
    comment_templates = CommentTemplatesDirectory()
1951 1954
    category_class = WorkflowCategory
1952 1955
    categories = WorkflowCategoriesDirectory()
1953 1956

  
......
1979 1982
        r += htmltext('<h2>%s</h2>') % _('Workflows')
1980 1983
        r += htmltext('<span class="actions">')
1981 1984
        if is_global_accessible():
1985
            r += htmltext('<a href="comment-templates/">%s</a>') % _('Comment Templates')
1982 1986
            r += htmltext('<a href="mail-templates/">%s</a>') % _('Mail Templates')
1983 1987
            r += htmltext('<a href="data-sources/">%s</a>') % _('Data sources')
1984 1988
            r += htmltext('<a href="categories/">%s</a>') % _('Categories')
wcs/backoffice/studio.py
21 21
from wcs.backoffice.deprecations import DeprecationsDirectory
22 22
from wcs.blocks import BlockDef
23 23
from wcs.carddef import CardDef
24
from wcs.comment_templates import CommentTemplate
24 25
from wcs.data_sources import NamedDataSource
25 26
from wcs.formdef import FormDef
26 27
from wcs.mail_templates import MailTemplate
......
46 47
        backoffice_root = get_publisher().get_backoffice_root()
47 48
        object_types = []
48 49
        if backoffice_root.is_accessible('workflows'):
49
            object_types += [Workflow, MailTemplate]
50
            object_types += [Workflow, MailTemplate, CommentTemplate]
50 51
        if backoffice_root.is_accessible('forms'):
51 52
            object_types += [NamedDataSource, BlockDef, FormDef]
52 53
        if backoffice_root.is_accessible('workflows'):
......
103 104
            extra_links.append(('../forms/blocks/', pgettext('studio', 'Field blocks')))
104 105
        if backoffice_root.is_accessible('workflows'):
105 106
            extra_links.append(('../workflows/mail-templates/', pgettext('studio', 'Mail templates')))
106
            object_types += [Workflow, MailTemplate]
107
            extra_links.append(('../workflows/comment-templates/', pgettext('studio', 'Comment templates')))
108
            object_types += [Workflow, MailTemplate, CommentTemplate]
107 109
        if backoffice_root.is_accessible('forms'):
108 110
            extra_links.append(('../forms/data-sources/', pgettext('studio', 'Data sources')))
109 111
            object_types += [NamedDataSource, BlockDef, FormDef]
wcs/categories.py
299 299
        return MailTemplate
300 300

  
301 301

  
302
class CommentTemplateCategory(Category):
303
    _names = 'comment_template_categories'
304
    xml_root_node = 'comment_template_category'
305
    backoffice_class = 'wcs.admin.categories.CommentTemplateCategoryPage'
306
    backoffice_base_url = 'workflows/comment-templates/categories/'
307

  
308
    # declarations for serialization
309
    XML_NODES = [
310
        ('name', 'str'),
311
        ('url_name', 'str'),
312
        ('description', 'str'),
313
        ('position', 'int'),
314
    ]
315

  
316
    @classmethod
317
    def get_object_class(cls):
318
        from .comment_templates import CommentTemplate
319

  
320
        return CommentTemplate
321

  
322

  
302 323
class DataSourceCategory(Category):
303 324
    _names = 'data_source_categories'
304 325
    xml_root_node = 'data_source_category'
wcs/comment_templates.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2022  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 collections import defaultdict
18

  
19
from quixote import get_publisher
20

  
21
from wcs.categories import CommentTemplateCategory
22
from wcs.qommon import _, get_logger
23
from wcs.qommon.form import OptGroup
24
from wcs.qommon.xml_storage import XmlStorableObject
25

  
26

  
27
class CommentTemplate(XmlStorableObject):
28
    _names = 'comment-templates'
29
    xml_root_node = 'comment-template'
30
    backoffice_class = 'wcs.admin.comment_templates.CommentTemplatePage'
31
    verbose_name = _('Comment template')
32
    verbose_name_plural = _('Comment templates')
33

  
34
    name = None
35
    slug = None
36
    description = None
37
    comment = None
38
    attachments = []
39
    category_id = None
40

  
41
    # declarations for serialization
42
    XML_NODES = [
43
        ('name', 'str'),
44
        ('slug', 'str'),
45
        ('description', 'str'),
46
        ('comment', 'str'),
47
        ('attachments', 'str_list'),
48
    ]
49

  
50
    def __init__(self, name=None):
51
        XmlStorableObject.__init__(self)
52
        self.name = name
53

  
54
    @property
55
    def category(self):
56
        return CommentTemplateCategory.get(self.category_id, ignore_errors=True)
57

  
58
    @category.setter
59
    def category(self, category):
60
        if category:
61
            self.category_id = category.id
62
        elif self.category_id:
63
            self.category_id = None
64

  
65
    def get_admin_url(self):
66
        base_url = get_publisher().get_backoffice_url()
67
        return '%s/workflows/comment-templates/%s/' % (base_url, self.id)
68

  
69
    def store(self, comment=None, *args, **kwargs):
70
        assert not self.is_readonly()
71
        if self.slug is None:
72
            # set slug if it's not yet there
73
            self.slug = self.get_new_slug()
74
        super().store(*args, **kwargs)
75
        if get_publisher().snapshot_class:
76
            get_publisher().snapshot_class.snap(instance=self, comment=comment)
77

  
78
    def get_places_of_use(self):
79
        from wcs.workflows import Workflow
80

  
81
        for workflow in Workflow.select(ignore_errors=True, ignore_migration=True):
82
            for item in workflow.get_all_items():
83
                if item.key != 'register-comment':
84
                    continue
85
                if item.comment_template == self.slug:
86
                    yield workflow
87
                    break
88

  
89
    def is_in_use(self):
90
        return any(self.get_places_of_use())
91

  
92
    @classmethod
93
    def get_as_options_list(cls):
94
        def get_option(mt):
95
            option = [mt.slug, mt.name, mt.slug]
96
            if get_publisher().get_backoffice_root().is_accessible('workflows'):
97
                option.append({'data-goto-url': mt.get_admin_url()})
98
            return option
99

  
100
        comment_templates_by_category_names = defaultdict(list)
101
        for comment_template in cls.select(order_by='name'):
102
            name = ''
103
            if comment_template.category:
104
                name = comment_template.category.name
105
            comment_templates_by_category_names[name].append(comment_template)
106
        category_names = list(comment_templates_by_category_names.keys())
107
        if len(category_names) == 1 and category_names[0] == '':
108
            # no category found
109
            return [get_option(mt) for mt in comment_templates_by_category_names['']]
110
        options = []
111
        # sort categories
112
        category_names = sorted(category_names)
113
        # comment template without categories at the end
114
        if category_names[0] == '':
115
            category_names = category_names[1:] + ['']
116
        # group by category name
117
        for name in category_names:
118
            options.append(OptGroup(name or _('Without category')))
119
            options.extend([get_option(mt) for mt in comment_templates_by_category_names[name]])
120
        return options
121

  
122
    @classmethod
123
    def get_by_slug(cls, slug, ignore_errors=True):
124
        comment_template = super().get_by_slug(slug, ignore_errors=ignore_errors)
125
        if comment_template is None:
126
            get_logger().warning("comment template '%s' does not exist" % slug)
127
        return comment_template
128

  
129
    def export_to_xml(self, include_id=False):
130
        root = super().export_to_xml(include_id=include_id)
131
        CommentTemplateCategory.object_category_xml_export(self, root, include_id=include_id)
132
        return root
133

  
134
    @classmethod
135
    def import_from_xml_tree(cls, tree, include_id=False, **kwargs):
136
        comment_template = super().import_from_xml_tree(tree, include_id=include_id, **kwargs)
137
        CommentTemplateCategory.object_category_xml_import(comment_template, tree, include_id=include_id)
138
        return comment_template
wcs/publisher.py
193 193
            'workflow_categories': 0,
194 194
            'block_categories': 0,
195 195
            'mail_template_categories': 0,
196
            'comment_template_categories': 0,
196 197
            'data_source_categories': 0,
197 198
            'roles': 0,
198 199
            'settings': 0,
199 200
            'datasources': 0,
200 201
            'wscalls': 0,
201 202
            'mail-templates': 0,
203
            'comment-templates': 0,
202 204
            'blockdefs': 0,
203 205
            'apiaccess': 0,
204 206
        }
wcs/snapshots.py
203 203
        from wcs.blocks import BlockDef
204 204
        from wcs.carddef import CardDef
205 205
        from wcs.categories import BlockCategory, CardDefCategory, Category, WorkflowCategory
206
        from wcs.comment_templates import CommentTemplate
206 207
        from wcs.data_sources import NamedDataSource
207 208
        from wcs.formdef import FormDef
208 209
        from wcs.mail_templates import MailTemplate
......
217 218
            Workflow,
218 219
            NamedWsCall,
219 220
            MailTemplate,
221
            CommentTemplate,
220 222
            Category,
221 223
            CardDefCategory,
222 224
            WorkflowCategory,
wcs/templates/wcs/backoffice/comment-template.html
1
{% extends "wcs/backoffice/base.html" %}
2
{% load i18n %}
3

  
4
{% block appbar-title %}{% trans "Comment Template" %} - {{ comment_template.name }}{% endblock %}
5

  
6
{% block appbar-actions %}
7
  {% if not comment_template.is_readonly %}
8
    <a href="edit">{% trans "Edit" %}</a>
9
  {% endif %}
10
{% endblock %}
11

  
12
{% block content %}
13
  {% if comment_template.description %}
14
    <div class="bo-block">{{ comment_template.description }}</div>
15
  {% endif %}
16

  
17
  {% if comment_template.comment %}
18
    <div class="section">
19
      <div class="comment-comment">{{ comment_template.comment }}</div>
20
    </div>
21

  
22
    {% for workflow in comment_template.get_places_of_use %}
23
      {% if forloop.first %}
24
        <div class="section">
25
          <h3>{% trans "Usage in workflows" %}</h3>
26
          <ul class="objects-list single-links">
27
      {% endif %}
28
      <li><a href="{{ workflow.get_admin_url }}">{{ workflow.name }}</a></li>
29
      {% if forloop.last %}
30
        </ul>
31
        </div>
32
      {% endif %}
33
    {% endfor %}
34

  
35
  {% else %}
36
    <div class="infonotice">{% trans "This comment template still needs to be configured." %}</div>
37
  {% endif %}
38

  
39
{% endblock %}
wcs/templates/wcs/backoffice/comment-templates.html
1
{% extends "wcs/backoffice/base.html" %}
2
{% load i18n %}
3

  
4
{% block appbar-title %}{% trans "Comment Templates" %}{% endblock %}
5

  
6
{% block appbar-actions %}
7
  <a href="categories/">{% trans "Categories" %}</a>
8
  <a rel="popup" href="import">{% trans "Import" %}</a>
9
  <a rel="popup" href="new">{% trans "New comment template" %}</a>
10
{% endblock %}
11

  
12
{% block content %}
13
  {% if categories %}
14
    {% for category in categories %}
15
      {% if category.comment_templates %}
16
        <div class="section">
17
          <h2>{{ category.name }}</h2>
18
          <ul class="objects-list single-links">
19
            {% for comment_template in category.comment_templates %}
20
              <li><a href="{{ comment_template.id }}/">{{ comment_template.name }}</a></li>
21
            {% endfor %}
22
          </ul>
23
        </div>
24
      {% endif %}
25
    {% endfor %}
26
  {% elif comment_templates %}
27
    <ul class="objects-list single-links">
28
      {% for comment_template in comment_templates %}
29
        <li><a href="{{ comment_template.id }}/">{{ comment_template.name }}</a></li>
30
      {% endfor %}
31
    </ul>
32
  {% else %}
33
    <div class="infonotice">
34
      {% trans "There are no comment templates defined." %}
35
    </div>
36
  {% endif %}
37
{% endblock %}
wcs/templates/wcs/backoffice/settings/import.html
33 33
      {% if results.workflow_categories %}
34 34
        <li>{% blocktrans count counter=results.workflow_categories %}1 workflow category{% plural %}{{ counter }} workflow categories{% endblocktrans %}</li>
35 35
      {% endif %}
36
      {% if results.block_categories %}
37
        <li>{% blocktrans count counter=results.block_categories %}1 block category{% plural %}{{ counter }} block categories{% endblocktrans %}</li>
38
      {% endif %}
39
      {% if results.mail_template_categories %}
40
        <li>{% blocktrans count counter=results.mail_template_categories %}1 mail template category{% plural %}{{ counter }} mail template categories{% endblocktrans %}</li>
41
      {% endif %}
42
      {% if results.comment_template_categories %}
43
        <li>{% blocktrans count counter=results.comment_template_categories %}1 comment template category{% plural %}{{ counter }} comment template categories{% endblocktrans %}</li>
44
      {% endif %}
45
      {% if results.data_source_categories %}
46
        <li>{% blocktrans count counter=results.data_source_categories %}1 data source category{% plural %}{{ counter }} data source categories{% endblocktrans %}</li>
47
      {% endif %}
36 48
      {% if results.datasources %}
37 49
        <li>{% blocktrans count counter=results.datasources %}1 data source{% plural %}{{ counter }} data sources{% endblocktrans %}</li>
38 50
      {% endif %}
39 51
      {% if results.mail_templates %}
40 52
        <li>{% blocktrans count counter=results.mail_templates %}1 mail template{% plural %}{{ counter }} mail templates{% endblocktrans %}</li>
41 53
      {% endif %}
54
      {% if results.comment_templates %}
55
        <li>{% blocktrans count counter=results.comment_templates %}1 comment template{% plural %}{{ counter }} comment templates{% endblocktrans %}</li>
56
      {% endif %}
42 57
      {% if results.wscalls %}
43 58
        <li>{% blocktrans count counter=results.wscalls %}1 webservice call{% plural %}{{ counter }} webservice calls{% endblocktrans %}</li>
44 59
      {% endif %}
wcs/wf/register_comment.py
17 17
from quixote import get_publisher
18 18
from quixote.html import htmltext
19 19

  
20
from wcs.comment_templates import CommentTemplate
20 21
from wcs.workflows import (
21 22
    AttachmentEvolutionPart,
22 23
    EvolutionPart,
......
26 27
)
27 28

  
28 29
from ..qommon import _, ezt
29
from ..qommon.form import TextWidget, WidgetListOfRoles
30
from ..qommon.form import SingleSelectWidget, TextWidget, WidgetListOfRoles
30 31
from ..qommon.template import TemplateError
31 32

  
32 33

  
......
84 85
    category = 'interaction'
85 86

  
86 87
    comment = None
88
    comment_template = None
87 89
    to = None
88 90
    attachments = None
89 91

  
90 92
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs):
91 93
        super().add_parameters_widgets(form, parameters, prefix=prefix, formdef=formdef, **kwargs)
94
        subject_body_attrs = {}
92 95
        if 'comment' in parameters:
96
            if CommentTemplate.count():
97
                subject_body_attrs = {
98
                    'data-dynamic-display-value': '',
99
                    'data-dynamic-display-child-of': '%scomment_template' % prefix,
100
                }
101
        if 'comment' in parameters:
102
            form.add(
103
                TextWidget,
104
                '%scomment' % prefix,
105
                title=_('Message'),
106
                value=self.comment,
107
                cols=80,
108
                rows=10,
109
                attrs=subject_body_attrs,
110
            )
111
        if 'comment_template' in parameters and CommentTemplate.count():
93 112
            form.add(
94
                TextWidget, '%scomment' % prefix, title=_('Message'), value=self.comment, cols=80, rows=10
113
                SingleSelectWidget,
114
                '%scomment_template' % prefix,
115
                title=_('Comment Template'),
116
                value=self.comment_template,
117
                options=[(None, '', '')] + CommentTemplate.get_as_options_list(),
118
                attrs={'data-dynamic-display-parent': 'true'},
95 119
            )
96 120
        if 'to' in parameters:
97 121
            form.add(
......
105 129
            )
106 130

  
107 131
    def get_parameters(self):
108
        return ('comment', 'to', 'attachments', 'condition')
132
        return ('comment_template', 'comment', 'to', 'attachments', 'condition')
109 133

  
110 134
    def attach_uploads_to_formdata(self, formdata, uploads, to):
111 135
        if not formdata.evolution[-1].parts:
......
124 148
        if not formdata.evolution:
125 149
            return
126 150

  
151
        if self.comment_template:
152
            comment_template = CommentTemplate.get_by_slug(self.comment_template)
153
            if comment_template:
154
                comment = comment_template.comment
155
                extra_attachments = comment_template.attachments
156
            else:
157
                message = _(
158
                    'reference to invalid comment template %(comment_template)s in status %(status)s'
159
                ) % {
160
                    'status': self.parent.name,
161
                    'comment_template': self.comment_template,
162
                }
163
                get_publisher().record_error(message, formdata=formdata, status_item=self)
164
                return
165
        else:
166
            comment = self.comment
167
            extra_attachments = None
168

  
127 169
        # process attachments first, they might be used in the comment
128 170
        # (with substitution vars)
129
        if self.attachments:
130
            uploads = self.convert_attachments_to_uploads()
171
        if self.attachments or extra_attachments:
172
            uploads = self.convert_attachments_to_uploads(extra_attachments)
131 173
            self.attach_uploads_to_formdata(formdata, uploads, self.to)
132 174
            formdata.store()  # store and invalidate cache, so references can be used in the comment message.
133 175

  
134 176
        # the comment can use attachments done above
135
        if self.comment:
177
        if comment:
136 178
            try:
137 179
                formdata.evolution[-1].add_part(
138
                    JournalEvolutionPart(formdata, get_publisher().translate(self.comment), self.to)
180
                    JournalEvolutionPart(formdata, get_publisher().translate(comment), self.to)
139 181
                )
140 182
                formdata.store()
141 183
            except TemplateError as e:
142
-