diff --git a/tests/admin_pages/test_settings.py b/tests/admin_pages/test_settings.py index e6a62e144..9998321e0 100644 --- a/tests/admin_pages/test_settings.py +++ b/tests/admin_pages/test_settings.py @@ -15,17 +15,21 @@ from webtest import Upload from wcs import fields from wcs.api_access import ApiAccess +from wcs.blocks import BlockDef from wcs.carddef import CardDef from wcs.categories import ( BlockCategory, CardDefCategory, Category, + CommentTemplateCategory, DataSourceCategory, MailTemplateCategory, WorkflowCategory, ) +from wcs.comment_templates import CommentTemplate from wcs.data_sources import NamedDataSource from wcs.formdef import FormDef +from wcs.mail_templates import MailTemplate from wcs.qommon.form import UploadedFile from wcs.qommon.http_request import HTTPRequest from wcs.wf.export_to_model import ExportToModel @@ -97,7 +101,11 @@ def test_settings_export_import(pub): ApiAccess.wipe() BlockCategory.wipe() MailTemplateCategory.wipe() + CommentTemplateCategory.wipe() DataSourceCategory.wipe() + MailTemplate.wipe() + CommentTemplate.wipe() + BlockDef.wipe() wipe() create_superuser(pub) @@ -132,9 +140,13 @@ def test_settings_export_import(pub): WorkflowCategory(name='foobaz').store() BlockCategory(name='category for blocks').store() MailTemplateCategory(name='category for mail templates').store() + CommentTemplateCategory(name='category for mail templates').store() DataSourceCategory(name='category for data sources').store() + MailTemplate(name='Mail templates').store() + CommentTemplate(name='Comment templates').store() pub.role_class(name='qux').store() NamedDataSource(name='quux').store() + BlockDef(name='blockdef').store() ds = NamedDataSource(name='agenda') ds.external = 'agenda' ds.store() @@ -179,10 +191,13 @@ def test_settings_export_import(pub): assert 'carddef_categories/1' in filelist assert 'workflow_categories/1' in filelist assert 'block_categories/1' in filelist - assert 'data_source_categories/1' in filelist assert 'mail_template_categories/1' in filelist + assert 'comment_template_categories/1' in filelist + assert 'data_source_categories/1' in filelist assert 'datasources/1' in filelist assert 'datasources/2' not in filelist # agenda datasource, not exported + assert 'mail-templates/1' in filelist + assert 'comment-templates/1' in filelist assert 'wscalls/corge' in filelist assert 'apiaccess/1' in filelist for filename in filelist: @@ -204,20 +219,42 @@ def test_settings_export_import(pub): resp.form['file'] = Upload('export.wcs', zip_content.getvalue()) resp = resp.form.submit('submit') assert 'Imported successfully' in resp.text - assert '1 form' in resp.text - assert '1 card' in resp.text - assert '2 categories' in resp.text - assert '1 card category' in resp.text - assert '1 workflow category' in resp.text + assert '1 form' in resp.text + assert '1 card' in resp.text + assert '1 fields block' in resp.text + assert '1 workflow' in resp.text + assert '1 role' in resp.text + assert '2 categories' in resp.text + assert '1 card category' in resp.text + assert '1 workflow category' in resp.text + assert '1 block category' in resp.text + assert '1 mail template category' in resp.text + assert '1 comment template category' in resp.text + assert '1 data source category' in resp.text + assert '1 data source' in resp.text + assert '1 mail template' in resp.text + assert '1 comment template' in resp.text + assert '1 webservice call' in resp.text + assert '1 API access' in resp.text assert FormDef.count() == 1 assert FormDef.select()[0].url_name == 'foo' assert CardDef.count() == 1 assert CardDef.select()[0].url_name == 'bar' + assert BlockDef.count() == 1 + assert Workflow.count() == 1 + assert pub.role_class.count() == 1 + assert Category.count() == 2 + assert CardDefCategory.count() == 1 + assert WorkflowCategory.count() == 1 assert BlockCategory.count() == 1 assert MailTemplateCategory.count() == 1 + assert CommentTemplateCategory.count() == 1 assert DataSourceCategory.count() == 1 + assert NamedDataSource.count() == 1 + assert MailTemplate.count() == 1 + assert CommentTemplate.count() == 1 + assert NamedWsCall.count() == 1 assert ApiAccess.count() == 1 - assert pub.role_class.count() == 1 # check roles are found by name wipe() diff --git a/tests/admin_pages/test_studio.py b/tests/admin_pages/test_studio.py index f0a158b2f..9d6da4948 100644 --- a/tests/admin_pages/test_studio.py +++ b/tests/admin_pages/test_studio.py @@ -5,6 +5,7 @@ import pytest from wcs.blocks import BlockDef from wcs.carddef import CardDef +from wcs.comment_templates import CommentTemplate from wcs.data_sources import NamedDataSource from wcs.formdef import FormDef from wcs.mail_templates import MailTemplate @@ -48,6 +49,7 @@ def test_studio_home(pub): assert '../settings/data-sources/' not in resp.text assert '../forms/blocks/' in resp.text assert '../workflows/mail-templates/' in resp.text + assert '../workflows/comment-templates/' in resp.text assert '../settings/wscalls/' in resp.text assert 'Recent errors' in resp.text @@ -136,17 +138,36 @@ def test_studio_home_recent_changes(pub): NamedDataSource.wipe() FormDef.wipe() MailTemplate.wipe() + CommentTemplate.wipe() Workflow.wipe() NamedWsCall.wipe() objects = defaultdict(list) for i in range(6): - for klass in [BlockDef, CardDef, NamedDataSource, FormDef, MailTemplate, Workflow, NamedWsCall]: + for klass in [ + BlockDef, + CardDef, + NamedDataSource, + FormDef, + MailTemplate, + CommentTemplate, + Workflow, + NamedWsCall, + ]: obj = klass() obj.name = 'foo %s' % i obj.store() objects[klass.xml_root_node].append(obj) - for klass in [BlockDef, CardDef, NamedDataSource, FormDef, MailTemplate, Workflow, NamedWsCall]: + for klass in [ + BlockDef, + CardDef, + NamedDataSource, + FormDef, + MailTemplate, + CommentTemplate, + Workflow, + NamedWsCall, + ]: assert pub.snapshot_class.count(clause=[Equal('object_type', klass.xml_root_node)]) == 6 # 2 snapshots for this one, but will be displayed only once objects[klass.xml_root_node][-1].name += ' bar' @@ -184,14 +205,18 @@ def test_studio_home_recent_changes(pub): assert ( 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp ) + assert ( + 'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][i].id + not in resp + ) assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][i].id not in resp # too old assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][5].id not in resp assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id not in resp + assert 'backoffice/settings/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id not in resp # only 5 elements - assert 'backoffice/settings/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id in resp assert ( 'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id not in resp ) # not this url @@ -201,6 +226,7 @@ def test_studio_home_recent_changes(pub): ) assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][5].id in resp assert 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][5].id in resp + assert 'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][5].id in resp assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][5].id in resp assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][5].id in resp @@ -227,11 +253,15 @@ def test_studio_home_recent_changes(pub): assert ( 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp ) + assert ( + 'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][i].id + not in resp + ) assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp # too old assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][5].id not in resp + assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id not in resp # only 5 elements - assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id in resp assert 'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id in resp assert ( 'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id @@ -239,6 +269,7 @@ def test_studio_home_recent_changes(pub): ) assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][5].id in resp assert 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][5].id in resp + assert 'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][5].id in resp assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][5].id in resp pub.cfg['admin-permissions'] = {} @@ -256,7 +287,7 @@ def test_studio_home_recent_changes(pub): assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][i].id not in resp assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][i].id not in resp # too old - for i in range(4): + for i in range(5): assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][i].id not in resp assert ( 'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp @@ -264,16 +295,16 @@ def test_studio_home_recent_changes(pub): assert ( 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp ) + assert ( + 'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][i].id + not in resp + ) assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp - # too old - assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][4].id not in resp - assert 'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][4].id not in resp - assert 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][4].id not in resp # only 5 elements - assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][4].id in resp assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id in resp assert 'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id in resp assert 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][5].id in resp + assert 'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][5].id in resp assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][5].id in resp pub.cfg['admin-permissions'] = {} @@ -296,6 +327,10 @@ def test_studio_home_recent_changes(pub): assert ( 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp ) + assert ( + 'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][i].id + not in resp + ) assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp # too old assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][0].id not in resp @@ -324,11 +359,11 @@ def test_studio_home_recent_changes(pub): pub.cfg['admin-permissions'] = {} pub.write_cfg() resp = app.get('/backoffice/studio/all-changes/') - assert '(1-20/42)' in resp + assert '(1-20/48)' in resp resp = resp.click('') - assert '21-40/42' in resp.text + assert '21-40/48' in resp.text resp = resp.click('') - assert '41-42/42' in resp.text + assert '41-48/48' in resp.text user.is_admin = False user.store() diff --git a/tests/test_comment_template.py b/tests/test_comment_template.py new file mode 100644 index 000000000..95be60dce --- /dev/null +++ b/tests/test_comment_template.py @@ -0,0 +1,510 @@ +import io +import os +import re +import xml.etree.ElementTree as ET + +import pytest +from quixote import cleanup +from webtest import Upload + +from wcs.categories import CommentTemplateCategory +from wcs.comment_templates import CommentTemplate +from wcs.fields import FileField +from wcs.formdef import FormDef +from wcs.qommon.http_request import HTTPRequest +from wcs.qommon.ident.password_accounts import PasswordAccount +from wcs.qommon.misc import indent_xml as indent +from wcs.qommon.upload_storage import PicklableUpload +from wcs.workflows import Workflow + +from .utilities import clean_temporary_pub, create_temporary_pub, get_app, login + + +def setup_module(module): + cleanup() + + +def teardown_module(module): + clean_temporary_pub() + + +@pytest.fixture +def pub(request): + pub = create_temporary_pub() + req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'}) + pub.set_app_dir(req) + pub._set_request(req) + pub.cfg['identification'] = {'methods': ['password']} + pub.write_cfg() + return pub + + +@pytest.fixture +def superuser(pub): + if pub.user_class.select(lambda x: x.name == 'admin'): + user1 = pub.user_class.select(lambda x: x.name == 'admin')[0] + user1.is_admin = True + user1.store() + return user1 + + user1 = pub.user_class(name='admin') + user1.is_admin = True + user1.store() + + account1 = PasswordAccount(id='admin') + account1.set_password('admin') + account1.user_id = user1.id + account1.store() + + return user1 + + +@pytest.fixture +def comment_template(): + CommentTemplate.wipe() + comment_template = CommentTemplate(name='test CT') + comment_template.comment = 'test comment' + comment_template.store() + return comment_template + + +def test_comment_templates_basics(pub, superuser): + CommentTemplateCategory.wipe() + CommentTemplate.wipe() + + app = login(get_app(pub)) + resp = app.get('/backoffice/workflows/') + assert 'Comment Templates' in resp + resp = resp.click('Comment Templates') + assert 'There are no comment templates defined.' in resp + + resp = resp.click('New') + resp.form['name'] = 'first comment template' + resp = resp.form.submit('cancel').follow() + assert 'There are no comment templates defined.' in resp + + resp = resp.click('New') + resp.form['name'] = 'first comment template' + resp = resp.form.submit('submit').follow() + resp.form['comment'] = 'comment body' + resp = resp.form.submit('submit').follow() + + resp = resp.click('Edit') + resp.form['comment'] = 'edited comment body' + resp.form['attachments$element0'] = 'plop' + resp = resp.form.submit('submit').follow() + + resp = resp.click('Edit') + assert resp.form['comment'].value == 'edited comment body' + assert resp.form['attachments$element0'].value == 'plop' + resp = resp.form.submit('submit').follow() + + resp = resp.click('Delete') + resp = resp.form.submit('cancel').follow() + assert 'first comment template' in resp + + resp = resp.click('Delete') + resp = resp.form.submit('submit').follow() + assert 'first comment template' not in resp + assert 'There are no comment templates defined.' in resp + + resp = resp.click('New') + resp.form['name'] = 'first comment template' + resp = resp.form.submit('submit').follow() + resp.form['comment'] = 'comment body' + resp = resp.form.submit('submit').follow() + + resp = app.get('/backoffice/workflows/') + resp = resp.click('Comment Templates') + assert 'first comment template' in resp + + +def test_comment_template_in_use(pub, superuser): + Workflow.wipe() + CommentTemplate.wipe() + workflow = Workflow(name='test workflow') + st1 = workflow.add_status('Status1') + item = st1.add_action('register-comment') + item.comment = 'Hello' + workflow.store() + + comment_template = CommentTemplate(name='test comment template') + comment_template.comment = 'test comment' + comment_template.store() + assert comment_template.is_in_use() is False + + item.comment_template = comment_template.slug + workflow.store() + assert comment_template.is_in_use() is True + + # check workflow usage is displayed + app = login(get_app(pub)) + resp = app.get('/backoffice/workflows/comment-templates/%s/' % comment_template.id) + assert 'Usage in workflows' in resp.text + assert 'test workflow' in resp.text + resp.click('test workflow') # make sure the link is ok + + resp = resp.click('Delete') + assert 'still used' in resp.text + + +def test_admin_workflow_edit(pub, superuser): + CommentTemplateCategory.wipe() + Workflow.wipe() + CommentTemplate.wipe() + comment_template = CommentTemplate(name='test comment template') + comment_template.comment = 'test comment' + comment_template.store() + + workflow = Workflow(name='test comment template') + st1 = workflow.add_status('Status1') + item = st1.add_action('register-comment') + item.comment = 'Hello' + workflow.store() + + app = login(get_app(pub)) + resp = app.get('/backoffice/workflows/%s/status/%s/items/1/' % (workflow.id, st1.id)) + assert [o[0] for o in resp.form['comment_template'].options] == ['', 'test-comment-template'] + + cat_b = CommentTemplateCategory(name='Cat B') + cat_b.store() + comment_template = CommentTemplate(name='foo bar') + comment_template.category_id = cat_b.id + comment_template.store() + comment_template = CommentTemplate(name='bar foo') + comment_template.category_id = cat_b.id + comment_template.store() + cat_a = CommentTemplateCategory(name='Cat A') + cat_a.store() + comment_template = CommentTemplate(name='foo baz') + comment_template.category_id = cat_a.id + comment_template.store() + + resp = app.get('/backoffice/workflows/%s/status/%s/items/1/' % (workflow.id, st1.id)) + assert [o[0] for o in resp.form['comment_template'].options] == [ + '', + 'foo-baz', + 'bar-foo', + 'foo-bar', + 'test-comment-template', + ] + resp.form['comment_template'] = 'test-comment-template' + resp = resp.form.submit('submit') + + workflow = Workflow.get(workflow.id) + assert workflow.possible_status[0].items[0].comment_template == 'test-comment-template' + + +def test_comment_templates_category(pub, superuser): + CommentTemplateCategory.wipe() + CommentTemplate.wipe() + + app = login(get_app(pub)) + resp = app.get('/backoffice/workflows/comment-templates/new') + assert 'category_id' not in resp.form.fields + + comment_template = CommentTemplate(name='foo') + comment_template.store() + + resp = app.get('/backoffice/workflows/comment-templates/categories/') + resp = resp.click('New Category') + resp.form['name'] = 'a new category' + resp.form['description'] = 'description of the category' + resp = resp.form.submit('submit') + assert CommentTemplateCategory.count() == 1 + category = CommentTemplateCategory.select()[0] + assert category.name == 'a new category' + + resp = app.get('/backoffice/workflows/comment-templates/new') + resp.form['name'] = 'template 2' + resp = resp.form.submit('submit').follow() + assert CommentTemplate.count() == 2 + assert CommentTemplate.get(2).category_id is None + resp = app.get('/backoffice/workflows/comment-templates/new') + resp.form['name'] = 'template 3' + resp.form['category_id'] = str(category.id) + resp = resp.form.submit('submit').follow() + assert CommentTemplate.count() == 3 + assert CommentTemplate.get(3).category_id == str(category.id) + + resp = app.get('/backoffice/workflows/comment-templates/%s/' % comment_template.id) + resp = resp.click(href=re.compile('^edit$')) + resp.form['category_id'] = str(category.id) + resp = resp.form.submit('cancel').follow() + comment_template.refresh_from_storage() + assert comment_template.category_id is None + + resp = app.get('/backoffice/workflows/comment-templates/%s/' % comment_template.id) + resp = resp.click(href=re.compile('^edit$')) + resp.form['category_id'] = str(category.id) + resp.form['comment'] = 'comment body' + resp = resp.form.submit('submit').follow() + comment_template.refresh_from_storage() + assert str(comment_template.category_id) == str(category.id) + + resp = app.get('/backoffice/workflows/comment-templates/%s/' % comment_template.id) + resp = resp.click(href=re.compile('^edit$')) + assert resp.form['category_id'].value == str(category.id) + + resp = app.get('/backoffice/workflows/comment-templates/categories/') + resp = resp.click('New Category') + resp.form['name'] = 'a second category' + resp.form['description'] = 'description of the category' + resp = resp.form.submit('submit') + assert CommentTemplateCategory.count() == 2 + category2 = [x for x in CommentTemplateCategory.select() if x.id != category.id][0] + assert category2.name == 'a second category' + + app.get( + '/backoffice/workflows/comment-templates/categories/update_order?order=%s;%s;' + % (category2.id, category.id) + ) + categories = CommentTemplateCategory.select() + CommentTemplateCategory.sort_by_position(categories) + assert [x.id for x in categories] == [str(category2.id), str(category.id)] + + app.get( + '/backoffice/workflows/comment-templates/categories/update_order?order=%s;%s;0' + % (category.id, category2.id) + ) + categories = CommentTemplateCategory.select() + CommentTemplateCategory.sort_by_position(categories) + assert [x.id for x in categories] == [str(category.id), str(category2.id)] + + resp = app.get('/backoffice/workflows/comment-templates/categories/') + resp = resp.click('a new category') + resp = resp.click('Delete') + resp = resp.form.submit() + comment_template.refresh_from_storage() + assert not comment_template.category_id + + +def test_workflow_register_comment_template(pub): + Workflow.wipe() + CommentTemplate.wipe() + + comment_template = CommentTemplate(name='test comment template') + comment_template.comment = 'test comment' + comment_template.store() + + workflow = Workflow(name='test comment template') + st1 = workflow.add_status('Status1') + item = st1.add_action('register-comment') + item.comment = 'Hello' + item.comment_template = comment_template.slug + workflow.store() + + formdef = FormDef() + formdef.name = 'baz' + formdef.fields = [] + formdef.store() + + formdata = formdef.data_class()() + formdata.just_created() + formdata.store() + + item.perform(formdata) + assert len(formdata.evolution) == 1 + assert len(formdata.evolution[0].parts) == 2 + assert formdata.evolution[-1].parts[1].content == 'test comment' + + # check nothing is registered and an error is logged if the comment template is missing + CommentTemplate.wipe() + item.perform(formdata) + assert len(formdata.evolution) == 1 + assert len(formdata.evolution[0].parts) == 2 + assert pub.loggederror_class.count() == 1 + logged_error = pub.loggederror_class.select()[0] + assert ( + logged_error.summary + == 'reference to invalid comment template test-comment-template in status Status1' + ) + + +def test_workflow_register_comment_template_attachments(pub): + Workflow.wipe() + CommentTemplate.wipe() + + comment_template = CommentTemplate(name='test comment template') + comment_template.comment = 'test comment' + comment_template.attachments = ['form_var_file1_raw'] + comment_template.store() + + workflow = Workflow(name='test comment template') + st1 = workflow.add_status('Status1') + item = st1.add_action('register-comment') + item.comment = 'Hello' + item.comment_template = comment_template.slug + workflow.store() + + formdef = FormDef() + formdef.name = 'baz' + formdef.fields = [ + FileField(id='1', label='File', type='file', varname='file1'), + ] + formdef.store() + + upload = PicklableUpload('test.jpeg', 'image/jpeg') + with open(os.path.join(os.path.dirname(__file__), 'image-with-gps-data.jpeg'), 'rb') as fd: + upload.receive([fd.read()]) + formdata = formdef.data_class()() + formdata.data = {'1': upload} + formdata.just_created() + formdata.store() + pub.substitutions.feed(formdata) + + item.perform(formdata) + assert len(formdata.evolution) == 1 + assert len(formdata.evolution[0].parts) == 3 + assert formdata.evolution[-1].parts[2].content == 'test comment' + assert formdata.evolution[-1].parts[1].base_filename == 'test.jpeg' + + # check two files are sent if attachments are also defined on the action itself. + item.attachments = ['form_var_file1_raw'] + item.perform(formdata) + assert len(formdata.evolution) == 1 + assert len(formdata.evolution[0].parts) == 6 + assert formdata.evolution[-1].parts[5].content == 'test comment' + assert formdata.evolution[-1].parts[4].base_filename == 'test.jpeg' + assert formdata.evolution[-1].parts[3].base_filename == 'test.jpeg' + + +def test_workflow_register_comment_template_empty(pub): + Workflow.wipe() + CommentTemplate.wipe() + + comment_template = CommentTemplate(name='test comment template') + comment_template.comment = None + comment_template.store() + + workflow = Workflow(name='test comment template') + st1 = workflow.add_status('Status1') + item = st1.add_action('register-comment') + item.comment = 'Hello' + item.comment_template = comment_template.slug + workflow.store() + + formdef = FormDef() + formdef.name = 'baz' + formdef.store() + + formdata = formdef.data_class()() + formdata.data = {} + formdata.just_created() + formdata.store() + pub.substitutions.feed(formdata) + + item.perform(formdata) + assert len(formdata.evolution) == 1 + assert len(formdata.evolution[0].parts) == 1 + + +def test_comment_templates_export(pub, superuser, comment_template): + app = login(get_app(pub)) + resp = app.get('/backoffice/workflows/comment-templates/1/') + + resp = resp.click(href='export') + xml_export = resp.text + + ds = io.StringIO(xml_export) + comment_template2 = CommentTemplate.import_from_xml(ds) + assert comment_template2.name == 'test CT' + + +def test_comment_templates_import(pub, superuser, comment_template): + comment_template.slug = 'foobar' + comment_template.store() + comment_template_xml = ET.tostring(comment_template.export_to_xml(include_id=True)) + CommentTemplate.wipe() + assert CommentTemplate.count() == 0 + + app = login(get_app(pub)) + resp = app.get('/backoffice/workflows/comment-templates/') + resp = resp.click(href='import') + resp.forms[0]['file'] = Upload('comment_template.wcs', comment_template_xml) + resp = resp.forms[0].submit() + assert CommentTemplate.count() == 1 + assert {wc.slug for wc in CommentTemplate.select()} == {'foobar'} + + # check slug + resp = app.get('/backoffice/workflows/comment-templates/') + resp = resp.click(href='import') + resp.forms[0]['file'] = Upload('comment_template.wcs', comment_template_xml) + resp = resp.forms[0].submit() + assert CommentTemplate.count() == 2 + assert {wc.slug for wc in CommentTemplate.select()} == {'foobar', 'test-ct'} + resp = app.get('/backoffice/workflows/comment-templates/') + resp = resp.click(href='import') + resp.forms[0]['file'] = Upload('comment_template.wcs', comment_template_xml) + resp = resp.forms[0].submit() + assert CommentTemplate.count() == 3 + assert {wc.slug for wc in CommentTemplate.select()} == {'foobar', 'test-ct', 'test-ct-1'} + + # import an invalid file + resp = app.get('/backoffice/workflows/comment-templates/') + resp = resp.click(href='import') + resp.form['file'] = Upload('comment_template.wcs', b'garbage') + resp = resp.form.submit() + assert 'Invalid File' in resp.text + + +def test_comment_templates_duplicate(pub, superuser, comment_template): + app = login(get_app(pub)) + resp = app.get('/backoffice/workflows/comment-templates/1/') + + resp = resp.click(href='duplicate') + assert resp.form['name'].value == 'test CT (copy)' + resp = resp.form.submit('cancel').follow() + assert CommentTemplate.count() == 1 + + resp = resp.click(href='duplicate') + assert resp.form['name'].value == 'test CT (copy)' + resp = resp.form.submit('submit').follow() + assert CommentTemplate.count() == 2 + + resp = app.get('/backoffice/workflows/comment-templates/1/') + resp = resp.click(href='duplicate') + assert resp.form['name'].value == 'test CT (copy 2)' + resp.form['name'].value = 'other copy' + resp = resp.form.submit('submit').follow() + assert CommentTemplate.count() == 3 + assert {x.name for x in CommentTemplate.select()} == {'test CT', 'test CT (copy)', 'other copy'} + assert {x.slug for x in CommentTemplate.select()} == {'test-ct', 'test-ct-copy', 'other-copy'} + + +def export_to_indented_xml(comment_template, include_id=False): + comment_template_xml = comment_template.export_to_xml(include_id=include_id) + indent(comment_template_xml) + return comment_template_xml + + +def assert_import_export_works(comment_template, include_id=False): + comment_template2 = CommentTemplate.import_from_xml_tree( + ET.fromstring(ET.tostring(comment_template.export_to_xml(include_id))), include_id + ) + assert ET.tostring(export_to_indented_xml(comment_template)) == ET.tostring( + export_to_indented_xml(comment_template2) + ) + return comment_template2 + + +def test_comment_template(pub): + comment_template = CommentTemplate(name='test') + assert_import_export_works(comment_template, include_id=True) + + +def test_comment_template_with_category(pub): + category = CommentTemplateCategory(name='test category') + category.store() + + comment_template = CommentTemplate(name='test category') + comment_template.category_id = category.id + comment_template.store() + comment_template2 = assert_import_export_works(comment_template, include_id=True) + assert comment_template2.category_id == comment_template.category_id + + # import with non existing category + CommentTemplateCategory.wipe() + export = ET.tostring(comment_template.export_to_xml(include_id=True)) + comment_template3 = CommentTemplate.import_from_xml_tree(ET.fromstring(export), include_id=True) + assert comment_template3.category_id is None diff --git a/tests/test_snapshots.py b/tests/test_snapshots.py index b579a5c4d..81ec97586 100644 --- a/tests/test_snapshots.py +++ b/tests/test_snapshots.py @@ -9,6 +9,7 @@ from quixote.http_request import Upload from wcs.blocks import BlockDef from wcs.carddef import CardDef from wcs.categories import Category +from wcs.comment_templates import CommentTemplate from wcs.data_sources import NamedDataSource from wcs.fields import CommentField, ItemField, PageField, StringField from wcs.formdef import FormDef @@ -1151,6 +1152,29 @@ def test_mail_template_snapshot_browse(pub): resp = resp.click('Edit') +def test_comment_template_snapshot_browse(pub): + create_superuser(pub) + create_role(pub) + + CommentTemplate.wipe() + comment_template = CommentTemplate(name='test') + comment_template.store() + assert pub.snapshot_class.count() == 1 + # check calling .store() without changes doesn't create snapshots + comment_template.store() + assert pub.snapshot_class.count() == 1 + + app = login(get_app(pub)) + + resp = app.get('/backoffice/workflows/comment-templates/%s/history/' % comment_template.id) + snapshot = pub.snapshot_class.select_object_history(comment_template)[0] + resp = resp.click(href='%s/view/' % snapshot.id) + assert 'This comment template is readonly' in resp.text + assert '

%s

' % localstrftime(snapshot.timestamp) in resp.text + with pytest.raises(IndexError): + resp = resp.click('Edit') + + def test_category_snapshot_browse(pub): create_superuser(pub) create_role(pub) diff --git a/wcs/admin/categories.py b/wcs/admin/categories.py index 876378f23..e64591c03 100644 --- a/wcs/admin/categories.py +++ b/wcs/admin/categories.py @@ -26,10 +26,12 @@ from wcs.categories import ( BlockCategory, CardDefCategory, Category, + CommentTemplateCategory, DataSourceCategory, MailTemplateCategory, WorkflowCategory, ) +from wcs.comment_templates import CommentTemplate from wcs.data_sources import NamedDataSource from wcs.formdef import FormDef from wcs.mail_templates import MailTemplate @@ -177,6 +179,11 @@ class MailTemplateCategoryUI(CategoryUI): management_roles_hint_text = None +class CommentTemplateCategoryUI(CategoryUI): + category_class = CommentTemplateCategory + management_roles_hint_text = None + + class DataSourceCategoryUI(CategoryUI): category_class = DataSourceCategory management_roles_hint_text = None @@ -334,6 +341,14 @@ class MailTemplateCategoryPage(CategoryPage): empty_message = _('No mail template associated to this category.') +class CommentTemplateCategoryPage(CategoryPage): + category_class = CommentTemplateCategory + category_ui_class = CommentTemplateCategoryUI + object_class = CommentTemplate + usage_title = _('Comment templates in this category') + empty_message = _('No comment template associated to this category.') + + class DataSourceCategoryPage(CategoryPage): category_class = DataSourceCategory category_ui_class = DataSourceCategoryUI @@ -452,6 +467,14 @@ class MailTemplateCategoriesDirectory(CategoriesDirectory): category_explanation = _('Categories are used to sort the different mail templates.') +class CommentTemplateCategoriesDirectory(CategoriesDirectory): + base_section = 'workflows' + category_class = CommentTemplateCategory + category_ui_class = CommentTemplateCategoryUI + category_page_class = CommentTemplateCategoryPage + category_explanation = _('Categories are used to sort the different comment templates.') + + class DataSourceCategoriesDirectory(CategoriesDirectory): base_section = 'workflows' category_class = DataSourceCategory diff --git a/wcs/admin/comment_templates.py b/wcs/admin/comment_templates.py new file mode 100644 index 000000000..e517a1fef --- /dev/null +++ b/wcs/admin/comment_templates.py @@ -0,0 +1,360 @@ +# w.c.s. - web application for online forms +# Copyright (C) 2005-2022 Entr'ouvert +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . + +from quixote import get_publisher, get_response, redirect +from quixote.directory import Directory +from quixote.html import TemplateIO, htmltext + +from wcs.admin import utils +from wcs.admin.categories import CommentTemplateCategoriesDirectory, get_categories +from wcs.backoffice.snapshots import SnapshotsDirectory +from wcs.categories import CommentTemplateCategory +from wcs.comment_templates import CommentTemplate +from wcs.qommon import _, errors, misc, template +from wcs.qommon.backoffice.menu import html_top +from wcs.qommon.form import ( + ComputedExpressionWidget, + FileWidget, + Form, + HtmlWidget, + SingleSelectWidget, + SlugWidget, + StringWidget, + TextWidget, + WidgetList, + get_session, +) + + +class CommentTemplatesDirectory(Directory): + _q_exports = ['', 'new', 'categories', ('import', 'p_import')] + do_not_call_in_templates = True + categories = CommentTemplateCategoriesDirectory() + + def _q_traverse(self, path): + if not get_publisher().get_backoffice_root().is_global_accessible('workflows'): + raise errors.AccessForbiddenError() + get_response().breadcrumb.append(('comment-templates/', _('Comment Templates'))) + return super()._q_traverse(path) + + def _q_lookup(self, component): + return CommentTemplatePage(component) + + def _q_index(self): + html_top('comment_templates', title=_('Comment Templates')) + categories = CommentTemplateCategory.select() + CommentTemplateCategory.sort_by_position(categories) + comment_templates = CommentTemplate.select(order_by='name') + if categories: + categories.append(CommentTemplateCategory(_('Misc'))) + for category in categories: + category.comment_templates = [x for x in comment_templates if x.category_id == category.id] + return template.QommonTemplateResponse( + templates=['wcs/backoffice/comment-templates.html'], + context={'view': self, 'comment_templates': comment_templates, 'categories': categories}, + ) + + def new(self): + form = Form(enctype='multipart/form-data') + form.add(StringWidget, 'name', title=_('Name'), required=True, size=50) + category_options = get_categories(CommentTemplateCategory) + if category_options: + category_options = [(None, '---', '')] + list(category_options) + form.add( + SingleSelectWidget, + 'category_id', + title=_('Category'), + options=category_options, + ) + form.add_submit('submit', _('Add')) + form.add_submit('cancel', _('Cancel')) + if form.get_widget('cancel').parse(): + return redirect('.') + + if form.is_submitted() and not form.has_errors(): + comment_template = CommentTemplate(name=form.get_widget('name').parse()) + if form.get_widget('category_id'): + comment_template.category_id = form.get_widget('category_id').parse() + comment_template.store() + return redirect('%s/edit' % comment_template.id) + + get_response().breadcrumb.append(('new', _('New Comment Template'))) + html_top('comment_templates', title=_('New Comment Template')) + r = TemplateIO(html=True) + r += htmltext('

%s

') % _('New Comment Template') + r += form.render() + return r.getvalue() + + def p_import(self): + form = Form(enctype='multipart/form-data') + import_title = _('Import Comment Template') + + form.add(FileWidget, 'file', title=_('File'), required=True) + form.add_submit('submit', import_title) + form.add_submit('cancel', _('Cancel')) + + if form.get_submit() == 'cancel': + return redirect('.') + + if form.is_submitted() and not form.has_errors(): + try: + return self.import_submit(form) + except ValueError: + pass + + get_response().breadcrumb.append(('import', _('Import'))) + html_top('comment_templates', title=import_title) + r = TemplateIO(html=True) + r += htmltext('

%s

') % import_title + r += htmltext('

%s

') % _('You can install a new comment template by uploading a file.') + r += form.render() + return r.getvalue() + + def import_submit(self, form): + fp = form.get_widget('file').parse().fp + + error = False + try: + comment_template = CommentTemplate.import_from_xml(fp) + get_session().message = ('info', _('This comment template has been successfully imported.')) + except ValueError: + error = True + + if error: + form.set_error('file', _('Invalid File')) + raise ValueError() + + # check slug unicity + known_slugs = { + x.slug: x.id for x in CommentTemplate.select(ignore_migration=True, ignore_errors=True) + } + if comment_template.slug in known_slugs: + comment_template.slug = None # a new one will be set in .store() + comment_template.store() + return redirect('%s/' % comment_template.id) + + +class CommentTemplatePage(Directory): + _q_exports = [ + '', + 'edit', + 'delete', + 'duplicate', + 'export', + ('history', 'snapshots_dir'), + ] + do_not_call_in_templates = True + + def __init__(self, component, instance=None): + try: + self.comment_template = instance or CommentTemplate.get(component) + except KeyError: + raise errors.TraversalError() + get_response().breadcrumb.append((component + '/', self.comment_template.name)) + self.snapshots_dir = SnapshotsDirectory(self.comment_template) + + def get_sidebar(self): + r = TemplateIO(html=True) + if self.comment_template.is_readonly(): + r += htmltext('

%s

') % _('This comment template is readonly.') + r += utils.snapshot_info_block(snapshot=self.comment_template.snapshot_object) + r += htmltext('') + return r.getvalue() + + def _q_index(self): + html_top('comment_templates', title=self.comment_template.name) + get_response().filter['sidebar'] = self.get_sidebar() + return template.QommonTemplateResponse( + templates=['wcs/backoffice/comment-template.html'], + context={'view': self, 'comment_template': self.comment_template}, + ) + + def get_form(self): + form = Form(enctype='multipart/form-data', use_tabs=True) + form.add( + StringWidget, 'name', title=_('Name'), required=True, size=30, value=self.comment_template.name + ) + category_options = get_categories(CommentTemplateCategory) + if category_options: + category_options = [(None, '---', '')] + list(category_options) + form.add( + SingleSelectWidget, + 'category_id', + title=_('Category'), + options=category_options, + value=self.comment_template.category_id, + ) + + form.add( + TextWidget, + 'description', + title=_('Description'), + cols=80, + rows=3, + value=self.comment_template.description, + ) + form.add( + TextWidget, + 'comment', + title=_('Comment'), + value=self.comment_template.comment, + cols=80, + rows=15, + require=True, + validation_function=ComputedExpressionWidget.validate_template, + ) + + if self.comment_template.slug and not self.comment_template.is_in_use(): + form.add( + SlugWidget, + 'slug', + value=self.comment_template.slug, + advanced=True, + ) + + form.add( + WidgetList, + 'attachments', + title=_('Attachments (templates or Python expressions)'), + element_type=StringWidget, + value=self.comment_template.attachments, + add_element_label=_('Add attachment'), + element_kwargs={'render_br': False, 'size': 50}, + advanced=True, + ) + + if not self.comment_template.is_readonly(): + form.add_submit('submit', _('Submit')) + form.add_submit('cancel', _('Cancel')) + return form + + def submit_form(self, form): + name = form.get_widget('name').parse() + slug_widget = form.get_widget('slug') + if slug_widget: + slug = form.get_widget('slug').parse() + + for comment_template in CommentTemplate.select(): + if comment_template.id == self.comment_template.id: + continue + if slug_widget and slug == comment_template.slug: + slug_widget.set_error(_('This value is already used.')) + if form.has_errors(): + raise ValueError() + + self.comment_template.name = name + if form.get_widget('category_id'): + self.comment_template.category_id = form.get_widget('category_id').parse() + self.comment_template.description = form.get_widget('description').parse() + self.comment_template.comment = form.get_widget('comment').parse() + self.comment_template.attachments = form.get_widget('attachments').parse() + if slug_widget: + self.comment_template.slug = slug + self.comment_template.store() + + def edit(self): + form = self.get_form() + if form.get_submit() == 'cancel': + return redirect('.') + + if form.get_submit() == 'submit' and not form.has_errors(): + try: + self.submit_form(form) + except ValueError: + pass + else: + return redirect('.') + + get_response().breadcrumb.append(('edit', _('Edit'))) + html_top('comment_templates', title=_('Edit Comment Template')) + r = TemplateIO(html=True) + r += htmltext('

%s

') % _('Edit Comment Template') + r += form.render() + r += get_publisher().substitutions.get_substitution_html_table() + + return r.getvalue() + + def delete(self): + form = Form(enctype='multipart/form-data') + if not self.comment_template.is_in_use(): + form.widgets.append( + HtmlWidget('

%s

' % _('You are about to irrevocably delete this comment template.')) + ) + form.add_submit('delete', _('Submit')) + else: + form.widgets.append( + HtmlWidget('

%s

' % _('This comment template is still used, it cannot be deleted.')) + ) + form.add_submit('cancel', _('Cancel')) + if form.get_widget('cancel').parse(): + return redirect('.') + if not form.is_submitted() or form.has_errors(): + get_response().breadcrumb.append(('delete', _('Delete'))) + html_top('comment_templates', title=_('Delete Comment Template')) + r = TemplateIO(html=True) + r += htmltext('

%s %s

') % (_('Deleting Comment Template:'), self.comment_template.name) + r += form.render() + return r.getvalue() + else: + self.comment_template.remove_self() + return redirect('..') + + def export(self): + return misc.xml_response( + self.comment_template, + filename='comment-template-%s.wcs' % self.comment_template.slug, + content_type='application/x-wcs-comment-template', + ) + + def duplicate(self): + form = Form(enctype='multipart/form-data') + name_widget = form.add(StringWidget, 'name', title=_('Name'), required=True, size=30) + form.add_submit('submit', _('Submit')) + form.add_submit('cancel', _('Cancel')) + if form.get_widget('cancel').parse(): + return redirect('.') + + if not form.is_submitted(): + original_name = self.comment_template.name + new_name = '%s %s' % (original_name, _('(copy)')) + names = [x.name for x in CommentTemplate.select()] + no = 2 + while new_name in names: + new_name = _('%(name)s (copy %(no)d)') % {'name': original_name, 'no': no} + no += 1 + name_widget.set_value(new_name) + + if not form.is_submitted() or form.has_errors(): + html_top('comment_templates', title=_('Duplicate Comment Template')) + r = TemplateIO(html=True) + get_response().breadcrumb.append(('duplicate', _('Duplicate'))) + r += htmltext('

%s

') % _('Duplicate Comment Template') + r += form.render() + return r.getvalue() + + self.comment_template.id = None + self.comment_template.slug = None + self.comment_template.name = form.get_widget('name').parse() + self.comment_template.store() + return redirect('../%s/' % self.comment_template.id) diff --git a/wcs/admin/settings.py b/wcs/admin/settings.py index 1d3475c5b..300598618 100644 --- a/wcs/admin/settings.py +++ b/wcs/admin/settings.py @@ -738,10 +738,14 @@ class SettingsDirectory(AccessControlled, Directory): form.add(CheckboxWidget, 'workflow_categories', title=_('Workflow Categories'), value=True) form.add(CheckboxWidget, 'block_categories', title=_('Fields Blocks Categories'), value=True) form.add(CheckboxWidget, 'mail_template_categories', title=_('Mail Templates Categories'), value=True) + form.add( + CheckboxWidget, 'comment_template_categories', title=_('Comment Templates Categories'), value=True + ) form.add(CheckboxWidget, 'data_source_categories', title=_('Data Sources Categories'), value=True) form.add(CheckboxWidget, 'settings', title=_('Settings'), value=False) form.add(CheckboxWidget, 'datasources', title=_('Data sources'), value=True) form.add(CheckboxWidget, 'mail-templates', title=_('Mail templates'), value=True) + form.add(CheckboxWidget, 'comment-templates', title=_('Comment templates'), value=True) form.add(CheckboxWidget, 'wscalls', title=_('Webservice calls'), value=True) form.add(CheckboxWidget, 'apiaccess', title=_('API access'), value=True) form.add_submit('submit', _('Submit')) @@ -773,9 +777,11 @@ class SettingsDirectory(AccessControlled, Directory): 'workflow_categories', 'block_categories', 'mail_template_categories', + 'comment_template_categories', 'data_source_categories', 'wscalls', 'mail-templates', + 'comment-templates', 'apiaccess', ): continue @@ -871,10 +877,12 @@ class SettingsDirectory(AccessControlled, Directory): 'workflow_categories', 'block_categories', 'mail_template_categories', + 'comment_template_categories', 'data_source_categories', 'datasources', 'wscalls', 'mail-templates', + 'comment-templates', 'blockdefs', 'apiaccess', ): @@ -934,6 +942,7 @@ class SettingsDirectory(AccessControlled, Directory): try: results = self.import_submit(form) results['mail_templates'] = results['mail-templates'] + results['comment_templates'] = results['comment-templates'] except zipfile.BadZipfile: results = None reason = _('Not a valid export file') diff --git a/wcs/admin/workflows.py b/wcs/admin/workflows.py index d7eea27be..693b14a42 100644 --- a/wcs/admin/workflows.py +++ b/wcs/admin/workflows.py @@ -63,6 +63,7 @@ from wcs.workflows import ( ) from . import utils +from .comment_templates import CommentTemplatesDirectory from .data_sources import NamedDataSourcesDirectory from .fields import FieldDefPage, FieldsDirectory from .logged_errors import LoggedErrorsDirectory @@ -1944,10 +1945,12 @@ class WorkflowsDirectory(Directory): ('import', 'p_import'), ('data-sources', 'data_sources'), ('mail-templates', 'mail_templates'), + ('comment-templates', 'comment_templates'), ] data_sources = NamedDataSourcesDirectoryInWorkflows() mail_templates = MailTemplatesDirectory() + comment_templates = CommentTemplatesDirectory() category_class = WorkflowCategory categories = WorkflowCategoriesDirectory() @@ -1979,6 +1982,7 @@ class WorkflowsDirectory(Directory): r += htmltext('

%s

') % _('Workflows') r += htmltext('') if is_global_accessible(): + r += htmltext('%s') % _('Comment Templates') r += htmltext('%s') % _('Mail Templates') r += htmltext('%s') % _('Data sources') r += htmltext('%s') % _('Categories') diff --git a/wcs/backoffice/studio.py b/wcs/backoffice/studio.py index f7524c223..11f2cf425 100644 --- a/wcs/backoffice/studio.py +++ b/wcs/backoffice/studio.py @@ -21,6 +21,7 @@ from wcs.admin.logged_errors import LoggedErrorsDirectory from wcs.backoffice.deprecations import DeprecationsDirectory from wcs.blocks import BlockDef from wcs.carddef import CardDef +from wcs.comment_templates import CommentTemplate from wcs.data_sources import NamedDataSource from wcs.formdef import FormDef from wcs.mail_templates import MailTemplate @@ -46,7 +47,7 @@ class ChangesDirectory(Directory): backoffice_root = get_publisher().get_backoffice_root() object_types = [] if backoffice_root.is_accessible('workflows'): - object_types += [Workflow, MailTemplate] + object_types += [Workflow, MailTemplate, CommentTemplate] if backoffice_root.is_accessible('forms'): object_types += [NamedDataSource, BlockDef, FormDef] if backoffice_root.is_accessible('workflows'): @@ -103,7 +104,8 @@ class StudioDirectory(Directory): extra_links.append(('../forms/blocks/', pgettext('studio', 'Field blocks'))) if backoffice_root.is_accessible('workflows'): extra_links.append(('../workflows/mail-templates/', pgettext('studio', 'Mail templates'))) - object_types += [Workflow, MailTemplate] + extra_links.append(('../workflows/comment-templates/', pgettext('studio', 'Comment templates'))) + object_types += [Workflow, MailTemplate, CommentTemplate] if backoffice_root.is_accessible('forms'): extra_links.append(('../forms/data-sources/', pgettext('studio', 'Data sources'))) object_types += [NamedDataSource, BlockDef, FormDef] diff --git a/wcs/categories.py b/wcs/categories.py index 91fa6ec32..276c2e9a4 100644 --- a/wcs/categories.py +++ b/wcs/categories.py @@ -301,6 +301,27 @@ class MailTemplateCategory(Category): return MailTemplate +class CommentTemplateCategory(Category): + _names = 'comment_template_categories' + xml_root_node = 'comment_template_category' + backoffice_class = 'wcs.admin.categories.CommentTemplateCategoryPage' + backoffice_base_url = 'workflows/comment-templates/categories/' + + # declarations for serialization + XML_NODES = [ + ('name', 'str'), + ('url_name', 'str'), + ('description', 'str'), + ('position', 'int'), + ] + + @classmethod + def get_object_class(cls): + from .comment_templates import CommentTemplate + + return CommentTemplate + + class DataSourceCategory(Category): _names = 'data_source_categories' xml_root_node = 'data_source_category' diff --git a/wcs/comment_templates.py b/wcs/comment_templates.py new file mode 100644 index 000000000..8a2fc473d --- /dev/null +++ b/wcs/comment_templates.py @@ -0,0 +1,138 @@ +# w.c.s. - web application for online forms +# Copyright (C) 2005-2022 Entr'ouvert +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . + +from collections import defaultdict + +from quixote import get_publisher + +from wcs.categories import CommentTemplateCategory +from wcs.qommon import _, get_logger +from wcs.qommon.form import OptGroup +from wcs.qommon.xml_storage import XmlStorableObject + + +class CommentTemplate(XmlStorableObject): + _names = 'comment-templates' + xml_root_node = 'comment-template' + backoffice_class = 'wcs.admin.comment_templates.CommentTemplatePage' + verbose_name = _('Comment template') + verbose_name_plural = _('Comment templates') + + name = None + slug = None + description = None + comment = None + attachments = [] + category_id = None + + # declarations for serialization + XML_NODES = [ + ('name', 'str'), + ('slug', 'str'), + ('description', 'str'), + ('comment', 'str'), + ('attachments', 'str_list'), + ] + + def __init__(self, name=None): + XmlStorableObject.__init__(self) + self.name = name + + @property + def category(self): + return CommentTemplateCategory.get(self.category_id, ignore_errors=True) + + @category.setter + def category(self, category): + if category: + self.category_id = category.id + elif self.category_id: + self.category_id = None + + def get_admin_url(self): + base_url = get_publisher().get_backoffice_url() + return '%s/workflows/comment-templates/%s/' % (base_url, self.id) + + def store(self, comment=None, *args, **kwargs): + assert not self.is_readonly() + if self.slug is None: + # set slug if it's not yet there + self.slug = self.get_new_slug() + super().store(*args, **kwargs) + if get_publisher().snapshot_class: + get_publisher().snapshot_class.snap(instance=self, comment=comment) + + def get_places_of_use(self): + from wcs.workflows import Workflow + + for workflow in Workflow.select(ignore_errors=True, ignore_migration=True): + for item in workflow.get_all_items(): + if item.key != 'register-comment': + continue + if item.comment_template == self.slug: + yield workflow + break + + def is_in_use(self): + return any(self.get_places_of_use()) + + @classmethod + def get_as_options_list(cls): + def get_option(mt): + option = [mt.slug, mt.name, mt.slug] + if get_publisher().get_backoffice_root().is_accessible('workflows'): + option.append({'data-goto-url': mt.get_admin_url()}) + return option + + comment_templates_by_category_names = defaultdict(list) + for comment_template in cls.select(order_by='name'): + name = '' + if comment_template.category: + name = comment_template.category.name + comment_templates_by_category_names[name].append(comment_template) + category_names = list(comment_templates_by_category_names.keys()) + if len(category_names) == 1 and category_names[0] == '': + # no category found + return [get_option(mt) for mt in comment_templates_by_category_names['']] + options = [] + # sort categories + category_names = sorted(category_names) + # comment template without categories at the end + if category_names[0] == '': + category_names = category_names[1:] + [''] + # group by category name + for name in category_names: + options.append(OptGroup(name or _('Without category'))) + options.extend([get_option(mt) for mt in comment_templates_by_category_names[name]]) + return options + + @classmethod + def get_by_slug(cls, slug, ignore_errors=True): + comment_template = super().get_by_slug(slug, ignore_errors=ignore_errors) + if comment_template is None: + get_logger().warning("comment template '%s' does not exist" % slug) + return comment_template + + def export_to_xml(self, include_id=False): + root = super().export_to_xml(include_id=include_id) + CommentTemplateCategory.object_category_xml_export(self, root, include_id=include_id) + return root + + @classmethod + def import_from_xml_tree(cls, tree, include_id=False, **kwargs): + comment_template = super().import_from_xml_tree(tree, include_id=include_id, **kwargs) + CommentTemplateCategory.object_category_xml_import(comment_template, tree, include_id=include_id) + return comment_template diff --git a/wcs/publisher.py b/wcs/publisher.py index 0c507f254..4fce37bef 100644 --- a/wcs/publisher.py +++ b/wcs/publisher.py @@ -225,12 +225,14 @@ class WcsPublisher(QommonPublisher): 'workflow_categories': 0, 'block_categories': 0, 'mail_template_categories': 0, + 'comment_template_categories': 0, 'data_source_categories': 0, 'roles': 0, 'settings': 0, 'datasources': 0, 'wscalls': 0, 'mail-templates': 0, + 'comment-templates': 0, 'blockdefs': 0, 'apiaccess': 0, } @@ -496,6 +498,7 @@ class WcsPublisher(QommonPublisher): from wcs.blocks import BlockDef from wcs.carddef import CardDef from wcs.categories import BlockCategory, CardDefCategory, Category, WorkflowCategory + from wcs.comment_templates import CommentTemplate from wcs.data_sources import NamedDataSource from wcs.formdef import FormDef from wcs.mail_templates import MailTemplate @@ -510,6 +513,7 @@ class WcsPublisher(QommonPublisher): Workflow, NamedWsCall, MailTemplate, + CommentTemplate, Category, CardDefCategory, WorkflowCategory, diff --git a/wcs/templates/wcs/backoffice/comment-template.html b/wcs/templates/wcs/backoffice/comment-template.html new file mode 100644 index 000000000..039c94735 --- /dev/null +++ b/wcs/templates/wcs/backoffice/comment-template.html @@ -0,0 +1,39 @@ +{% extends "wcs/backoffice/base.html" %} +{% load i18n %} + +{% block appbar-title %}{% trans "Comment Template" %} - {{ comment_template.name }}{% endblock %} + +{% block appbar-actions %} + {% if not comment_template.is_readonly %} + {% trans "Edit" %} + {% endif %} +{% endblock %} + +{% block content %} + {% if comment_template.description %} +
{{ comment_template.description }}
+ {% endif %} + + {% if comment_template.comment %} +
+
{{ comment_template.comment }}
+
+ + {% for workflow in comment_template.get_places_of_use %} + {% if forloop.first %} +
+

{% trans "Usage in workflows" %}

+ +
+ {% endif %} + {% endfor %} + + {% else %} +
{% trans "This comment template still needs to be configured." %}
+ {% endif %} + +{% endblock %} diff --git a/wcs/templates/wcs/backoffice/comment-templates.html b/wcs/templates/wcs/backoffice/comment-templates.html new file mode 100644 index 000000000..d3d6b0dfa --- /dev/null +++ b/wcs/templates/wcs/backoffice/comment-templates.html @@ -0,0 +1,37 @@ +{% extends "wcs/backoffice/base.html" %} +{% load i18n %} + +{% block appbar-title %}{% trans "Comment Templates" %}{% endblock %} + +{% block appbar-actions %} + {% trans "Categories" %} + {% trans "Import" %} + {% trans "New comment template" %} +{% endblock %} + +{% block content %} + {% if categories %} + {% for category in categories %} + {% if category.comment_templates %} +
+

{{ category.name }}

+ +
+ {% endif %} + {% endfor %} + {% elif comment_templates %} + + {% else %} +
+ {% trans "There are no comment templates defined." %} +
+ {% endif %} +{% endblock %} diff --git a/wcs/templates/wcs/backoffice/settings/import.html b/wcs/templates/wcs/backoffice/settings/import.html index 9775cbb45..cb8361186 100644 --- a/wcs/templates/wcs/backoffice/settings/import.html +++ b/wcs/templates/wcs/backoffice/settings/import.html @@ -33,12 +33,27 @@ {% if results.workflow_categories %}
  • {% blocktrans count counter=results.workflow_categories %}1 workflow category{% plural %}{{ counter }} workflow categories{% endblocktrans %}
  • {% endif %} + {% if results.block_categories %} +
  • {% blocktrans count counter=results.block_categories %}1 block category{% plural %}{{ counter }} block categories{% endblocktrans %}
  • + {% endif %} + {% if results.mail_template_categories %} +
  • {% blocktrans count counter=results.mail_template_categories %}1 mail template category{% plural %}{{ counter }} mail template categories{% endblocktrans %}
  • + {% endif %} + {% if results.comment_template_categories %} +
  • {% blocktrans count counter=results.comment_template_categories %}1 comment template category{% plural %}{{ counter }} comment template categories{% endblocktrans %}
  • + {% endif %} + {% if results.data_source_categories %} +
  • {% blocktrans count counter=results.data_source_categories %}1 data source category{% plural %}{{ counter }} data source categories{% endblocktrans %}
  • + {% endif %} {% if results.datasources %}
  • {% blocktrans count counter=results.datasources %}1 data source{% plural %}{{ counter }} data sources{% endblocktrans %}
  • {% endif %} {% if results.mail_templates %}
  • {% blocktrans count counter=results.mail_templates %}1 mail template{% plural %}{{ counter }} mail templates{% endblocktrans %}
  • {% endif %} + {% if results.comment_templates %} +
  • {% blocktrans count counter=results.comment_templates %}1 comment template{% plural %}{{ counter }} comment templates{% endblocktrans %}
  • + {% endif %} {% if results.wscalls %}
  • {% blocktrans count counter=results.wscalls %}1 webservice call{% plural %}{{ counter }} webservice calls{% endblocktrans %}
  • {% endif %} diff --git a/wcs/wf/register_comment.py b/wcs/wf/register_comment.py index 9aa4c8b54..d54ef5739 100644 --- a/wcs/wf/register_comment.py +++ b/wcs/wf/register_comment.py @@ -17,6 +17,7 @@ from quixote import get_publisher from quixote.html import htmltext +from wcs.comment_templates import CommentTemplate from wcs.workflows import ( AttachmentEvolutionPart, EvolutionPart, @@ -26,7 +27,7 @@ from wcs.workflows import ( ) from ..qommon import _, ezt -from ..qommon.form import TextWidget, WidgetListOfRoles +from ..qommon.form import SingleSelectWidget, TextWidget, WidgetListOfRoles from ..qommon.template import TemplateError @@ -84,14 +85,37 @@ class RegisterCommenterWorkflowStatusItem(WorkflowStatusItem): category = 'interaction' comment = None + comment_template = None to = None attachments = None def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs): super().add_parameters_widgets(form, parameters, prefix=prefix, formdef=formdef, **kwargs) + subject_body_attrs = {} + if 'comment' in parameters: + if CommentTemplate.count(): + subject_body_attrs = { + 'data-dynamic-display-value': '', + 'data-dynamic-display-child-of': '%scomment_template' % prefix, + } if 'comment' in parameters: form.add( - TextWidget, '%scomment' % prefix, title=_('Message'), value=self.comment, cols=80, rows=10 + TextWidget, + '%scomment' % prefix, + title=_('Message'), + value=self.comment, + cols=80, + rows=10, + attrs=subject_body_attrs, + ) + if 'comment_template' in parameters and CommentTemplate.count(): + form.add( + SingleSelectWidget, + '%scomment_template' % prefix, + title=_('Comment Template'), + value=self.comment_template, + options=[(None, '', '')] + CommentTemplate.get_as_options_list(), + attrs={'data-dynamic-display-parent': 'true'}, ) if 'to' in parameters: form.add( @@ -105,7 +129,7 @@ class RegisterCommenterWorkflowStatusItem(WorkflowStatusItem): ) def get_parameters(self): - return ('comment', 'to', 'attachments', 'condition') + return ('comment_template', 'comment', 'to', 'attachments', 'condition') def attach_uploads_to_formdata(self, formdata, uploads, to): if not formdata.evolution[-1].parts: @@ -124,18 +148,36 @@ class RegisterCommenterWorkflowStatusItem(WorkflowStatusItem): if not formdata.evolution: return + if self.comment_template: + comment_template = CommentTemplate.get_by_slug(self.comment_template) + if comment_template: + comment = comment_template.comment + extra_attachments = comment_template.attachments + else: + message = _( + 'reference to invalid comment template %(comment_template)s in status %(status)s' + ) % { + 'status': self.parent.name, + 'comment_template': self.comment_template, + } + get_publisher().record_error(message, formdata=formdata, status_item=self) + return + else: + comment = self.comment + extra_attachments = None + # process attachments first, they might be used in the comment # (with substitution vars) - if self.attachments: - uploads = self.convert_attachments_to_uploads() + if self.attachments or extra_attachments: + uploads = self.convert_attachments_to_uploads(extra_attachments) self.attach_uploads_to_formdata(formdata, uploads, self.to) formdata.store() # store and invalidate cache, so references can be used in the comment message. # the comment can use attachments done above - if self.comment: + if comment: try: formdata.evolution[-1].add_part( - JournalEvolutionPart(formdata, get_publisher().translate(self.comment), self.to) + JournalEvolutionPart(formdata, get_publisher().translate(comment), self.to) ) formdata.store() except TemplateError as e: