backoffice: make most objects documentable (#19777)

This commit is contained in:
Frédéric Péters 2024-04-07 10:33:22 +02:00
parent 3cd6f61a3c
commit c82031b4d0
45 changed files with 925 additions and 192 deletions

View File

@ -685,3 +685,37 @@ def test_block_test_results(pub):
resp.form['varname'] = 'test_3'
resp = resp.form.submit('submit').follow()
assert TestResult.count() == 1
def test_block_documentation(pub):
create_superuser(pub)
BlockDef.wipe()
blockdef = FormDef()
blockdef.name = 'block title'
blockdef.fields = [fields.BoolField(id='1', label='Bool')]
blockdef.store()
app = login(get_app(pub))
resp = app.get(blockdef.get_admin_url())
assert resp.pyquery('.documentation[hidden]')
resp = app.post_json(blockdef.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
blockdef.refresh_from_storage()
assert blockdef.documentation == '<p>doc</p>'
resp = app.get(blockdef.get_admin_url())
assert resp.pyquery('.documentation:not([hidden])')
resp = app.get(blockdef.get_admin_url() + 'fields/1/')
assert resp.pyquery('.documentation[hidden]')
assert resp.pyquery('#sidebar[hidden]')
resp = app.post_json(
blockdef.get_admin_url() + 'fields/1/update-documentation', {'content': '<p>doc</p>'}
)
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
blockdef.refresh_from_storage()
assert blockdef.fields[0].documentation == '<p>doc</p>'
resp = app.get(blockdef.get_admin_url() + 'fields/1/')
assert resp.pyquery('.documentation:not([hidden])')
assert resp.pyquery('#sidebar:not([hidden])')

View File

@ -1180,3 +1180,35 @@ def test_cards_management_options(pub):
assert_option_display(resp, 'Templates', 'Custom')
resp = resp.click('Management', href='options/management')
assert resp.form['history_pane_default_mode'].value == 'expanded'
def test_card_documentation(pub):
create_superuser(pub)
CardDef.wipe()
carddef = FormDef()
carddef.name = 'card title'
carddef.fields = [fields.BoolField(id='1', label='Bool')]
carddef.store()
app = login(get_app(pub))
resp = app.get(carddef.get_admin_url())
assert resp.pyquery('.documentation[hidden]')
resp = app.post_json(carddef.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
carddef.refresh_from_storage()
assert carddef.documentation == '<p>doc</p>'
resp = app.get(carddef.get_admin_url())
assert resp.pyquery('.documentation:not([hidden])')
resp = app.get(carddef.get_admin_url() + 'fields/1/')
assert resp.pyquery('.documentation[hidden]')
assert resp.pyquery('#sidebar[hidden]')
resp = app.post_json(carddef.get_admin_url() + 'fields/1/update-documentation', {'content': '<p>doc</p>'})
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
carddef.refresh_from_storage()
assert carddef.fields[0].documentation == '<p>doc</p>'
resp = app.get(carddef.get_admin_url() + 'fields/1/')
assert resp.pyquery('.documentation:not([hidden])')
assert resp.pyquery('#sidebar:not([hidden])')

View File

@ -195,7 +195,6 @@ def test_data_sources_new(pub):
resp = app.get('/backoffice/settings/data-sources/')
resp = resp.click('New Data Source')
resp.forms[0]['name'] = 'a new data source'
resp.forms[0]['description'] = 'description of the data source'
resp.forms[0]['data_source$type'] = 'python'
resp.forms[0]['data_source$value'] = repr([{'id': '1', 'text': 'un'}, {'id': '2', 'text': 'deux'}])
@ -209,13 +208,11 @@ def test_data_sources_new(pub):
assert 'Edit Data Source' in resp.text
assert NamedDataSource.get(1).name == 'a new data source'
assert NamedDataSource.get(1).description == 'description of the data source'
# add a second one
resp = app.get('/backoffice/settings/data-sources/')
resp = resp.click('New Data Source')
resp.forms[0]['name'] = 'an other data source'
resp.forms[0]['description'] = 'description of the data source'
resp.forms[0]['data_source$type'] = 'python'
resp = resp.forms[0].submit('data_source$apply')
resp.forms[0]['data_source$value'] = repr([{'id': '1', 'text': 'un'}, {'id': '2', 'text': 'deux'}])
@ -408,7 +405,6 @@ def test_data_sources_category(pub):
resp = app.get('/backoffice/settings/data-sources/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 DataSourceCategory.count() == 1
category = DataSourceCategory.select()[0]
@ -440,7 +436,6 @@ def test_data_sources_category(pub):
resp = app.get('/backoffice/settings/data-sources/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 DataSourceCategory.count() == 2
category2 = [x for x in DataSourceCategory.select() if x.id != category.id][0]
@ -872,13 +867,10 @@ def test_data_sources_edit(pub):
resp = resp.click(href='edit')
assert resp.forms[0]['name'].value == 'foobar'
resp.forms[0]['description'] = 'data source description'
resp = resp.forms[0].submit('submit')
assert resp.location == 'http://example.net/backoffice/settings/data-sources/1/'
resp = resp.follow()
assert NamedDataSource.get(1).description == 'data source description'
resp = app.get('/backoffice/settings/data-sources/1/edit')
assert '>Data Attribute</label>' in resp.text
assert '>Id Attribute</label>' in resp.text
@ -1146,3 +1138,22 @@ def test_data_sources_agenda_refresh(mock_collect, pub, chrono_url):
resp = resp.follow()
assert 'Agendas will be updated in the background.' in resp.text
assert NamedDataSource.count() == 2
def test_datasource_documentation(pub):
create_superuser(pub)
NamedDataSource.wipe()
data_source = NamedDataSource(name='foobar')
data_source.store()
app = login(get_app(pub))
resp = app.get(data_source.get_admin_url())
assert resp.pyquery('.documentation[hidden]')
resp = app.post_json(data_source.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
data_source.refresh_from_storage()
assert data_source.documentation == '<p>doc</p>'
resp = app.get(data_source.get_admin_url())
assert resp.pyquery('.documentation:not([hidden])')

View File

@ -5179,3 +5179,35 @@ def test_admin_form_sql_integrity_error(pub):
== 'There are integrity errors in the database column types.'
)
assert resp.pyquery('.errornotice li').text() == 'String, expected: character varying, got: boolean.'
def test_form_documentation(pub):
create_superuser(pub)
FormDef.wipe()
formdef = FormDef()
formdef.name = 'form title'
formdef.fields = [fields.BoolField(id='1', label='Bool')]
formdef.store()
app = login(get_app(pub))
resp = app.get(formdef.get_admin_url())
assert resp.pyquery('.documentation[hidden]')
resp = app.post_json(formdef.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
formdef.refresh_from_storage()
assert formdef.documentation == '<p>doc</p>'
resp = app.get(formdef.get_admin_url())
assert resp.pyquery('.documentation:not([hidden])')
resp = app.get(formdef.get_admin_url() + 'fields/1/')
assert resp.pyquery('.documentation[hidden]')
assert resp.pyquery('#sidebar[hidden]')
resp = app.post_json(formdef.get_admin_url() + 'fields/1/update-documentation', {'content': '<p>doc</p>'})
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
formdef.refresh_from_storage()
assert formdef.fields[0].documentation == '<p>doc</p>'
resp = app.get(formdef.get_admin_url() + 'fields/1/')
assert resp.pyquery('.documentation:not([hidden])')
assert resp.pyquery('#sidebar:not([hidden])')

View File

@ -4429,3 +4429,112 @@ def test_workflow_test_results(pub):
assert TestResult.count() == 2
result = TestResult.select(order_by='id')[1]
assert result.reason == 'Workflow: New status "new status"'
def test_workflow_documentation(pub):
create_superuser(pub)
Workflow.wipe()
workflow = Workflow(name='Workflow One')
status = workflow.add_status(name='New status')
status.add_action('anonymise')
workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow)
workflow.backoffice_fields_formdef.fields = [
fields.StringField(id='bo234', label='bo field 1'),
]
workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
workflow.variables_formdef.fields = [
fields.StringField(id='va123', label='bo field 1'),
]
global_action = workflow.add_global_action('action1')
workflow.store()
app = login(get_app(pub))
resp = app.get(workflow.get_admin_url())
assert resp.pyquery('.documentation[hidden]')
assert app.post_json(workflow.get_admin_url() + 'update-documentation', {}).json.get('err') == 1
resp = app.post_json(workflow.get_admin_url() + 'update-documentation', {'content': ''})
assert resp.json == {'err': 0, 'empty': True, 'changed': False}
resp = app.post_json(workflow.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
workflow.refresh_from_storage()
assert workflow.documentation == '<p>doc</p>'
# check forbidden HTML is cleaned
resp = app.post_json(
workflow.get_admin_url() + 'update-documentation',
{'content': '<p>iframe</p><iframe src="xx"></iframe>'},
)
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
workflow.refresh_from_storage()
assert workflow.documentation == '<p>iframe</p>'
resp = app.get(workflow.get_admin_url())
assert resp.pyquery('.documentation:not([hidden])')
resp = app.get(workflow.get_admin_url() + 'variables/fields/')
assert resp.pyquery('.documentation[hidden]')
resp = app.post_json(
workflow.get_admin_url() + 'variables/fields/update-documentation', {'content': '<p>doc</p>'}
)
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
workflow.refresh_from_storage()
assert workflow.variables_formdef.documentation == '<p>doc</p>'
resp = app.get(workflow.get_admin_url() + 'variables/fields/')
assert resp.pyquery('.documentation:not([hidden])')
resp = app.get(workflow.get_admin_url() + 'variables/fields/va123/')
assert resp.pyquery('.documentation[hidden]')
assert resp.pyquery('#sidebar[hidden]')
resp = app.post_json(
workflow.get_admin_url() + 'variables/fields/va123/update-documentation', {'content': '<p>doc</p>'}
)
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
workflow.refresh_from_storage()
assert workflow.variables_formdef.fields[0].documentation == '<p>doc</p>'
resp = app.get(workflow.get_admin_url() + 'variables/fields/va123/')
assert resp.pyquery('.documentation:not([hidden])')
assert resp.pyquery('#sidebar:not([hidden])')
resp = app.get(workflow.get_admin_url() + 'backoffice-fields/fields/')
assert resp.pyquery('.documentation[hidden]')
resp = app.post_json(
workflow.get_admin_url() + 'backoffice-fields/fields/update-documentation', {'content': '<p>doc</p>'}
)
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
workflow.refresh_from_storage()
assert workflow.backoffice_fields_formdef.documentation == '<p>doc</p>'
resp = app.get(workflow.get_admin_url() + 'backoffice-fields/fields/')
assert resp.pyquery('.documentation:not([hidden])')
resp = app.get(workflow.get_admin_url() + 'backoffice-fields/fields/bo234/')
assert resp.pyquery('.documentation[hidden]')
assert resp.pyquery('#sidebar[hidden]')
resp = app.post_json(
workflow.get_admin_url() + 'backoffice-fields/fields/bo234/update-documentation',
{'content': '<p>doc</p>'},
)
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
workflow.refresh_from_storage()
assert workflow.backoffice_fields_formdef.fields[0].documentation == '<p>doc</p>'
resp = app.get(workflow.get_admin_url() + 'backoffice-fields/fields/bo234/')
assert resp.pyquery('.documentation:not([hidden])')
assert resp.pyquery('#sidebar:not([hidden])')
resp = app.get(global_action.get_admin_url())
assert resp.pyquery('.documentation[hidden]')
resp = app.post_json(global_action.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
workflow.refresh_from_storage()
assert workflow.global_actions[0].documentation == '<p>doc</p>'
resp = app.get(global_action.get_admin_url())
assert resp.pyquery('.documentation:not([hidden])')
resp = app.get(status.get_admin_url())
assert resp.pyquery('.documentation[hidden]')
resp = app.post_json(status.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
workflow.refresh_from_storage()
assert workflow.possible_status[0].documentation == '<p>doc</p>'
resp = app.get(status.get_admin_url())
assert resp.pyquery('.documentation:not([hidden])')

View File

@ -36,7 +36,6 @@ def teardown_module(module):
def wscall():
NamedWsCall.wipe()
wscall = NamedWsCall(name='xxx')
wscall.description = 'description'
wscall.notify_on_errors = True
wscall.record_on_errors = True
wscall.request = {
@ -68,7 +67,6 @@ def test_wscalls_new(pub, value):
assert resp.form['notify_on_errors'].value is None
assert resp.form['record_on_errors'].value == 'yes'
resp.form['name'] = 'a new webservice call'
resp.form['description'] = 'description'
resp.form['notify_on_errors'] = value
resp.form['record_on_errors'] = value
resp.form['request$url'] = 'http://remote.example.net/json'
@ -111,14 +109,12 @@ def test_wscalls_edit(pub, wscall):
assert resp.form['notify_on_errors'].value == 'yes'
assert resp.form['record_on_errors'].value == 'yes'
assert 'slug' in resp.form.fields
resp.form['description'] = 'bla bla bla'
resp.form['notify_on_errors'] = False
resp.form['record_on_errors'] = False
resp = resp.form.submit('submit')
assert resp.location == 'http://example.net/backoffice/settings/wscalls/xxx/'
resp = resp.follow()
assert NamedWsCall.get('xxx').description == 'bla bla bla'
assert NamedWsCall.get('xxx').notify_on_errors is False
assert NamedWsCall.get('xxx').record_on_errors is False
@ -235,7 +231,6 @@ def test_wscalls_empty_param_values(pub):
resp = app.get('/backoffice/settings/wscalls/')
resp = resp.click('New webservice call')
resp.form['name'] = 'a new webservice call'
resp.form['description'] = 'description'
resp.form['request$qs_data$element0key'] = 'foo'
resp.form['request$post_data$element0key'] = 'bar'
resp = resp.form.submit('submit').follow()
@ -253,7 +248,6 @@ def test_wscalls_timeout(pub):
resp = app.get('/backoffice/settings/wscalls/')
resp = resp.click('New webservice call')
resp.form['name'] = 'a new webservice call'
resp.form['description'] = 'description'
resp.form['request$timeout'] = 'plop'
resp = resp.form.submit('submit')
assert resp.pyquery('[data-widget-name="request$timeout"].widget-with-error')
@ -300,3 +294,22 @@ def test_wscalls_usage(pub, wscall):
formdef.store()
resp = app.get(usage_url)
assert 'No usage detected.' in resp.text
def test_wscall_documentation(pub):
create_superuser(pub)
NamedWsCall.wipe()
wscall = NamedWsCall(name='foobar')
wscall.store()
app = login(get_app(pub))
resp = app.get(wscall.get_admin_url())
assert resp.pyquery('.documentation[hidden]')
resp = app.post_json(wscall.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
wscall.refresh_from_storage()
assert wscall.documentation == '<p>doc</p>'
resp = app.get(wscall.get_admin_url())
assert resp.pyquery('.documentation:not([hidden])')

View File

@ -508,3 +508,40 @@ def test_comment_template_with_category(pub):
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
def test_comment_template_migration(pub):
comment_template = CommentTemplate(name='test template')
comment_template.description = 'hello'
assert comment_template.migrate() is True
assert not comment_template.description
assert comment_template.documentation == 'hello'
def test_comment_template_legacy_xml(pub):
comment_template = CommentTemplate(name='test template')
comment_template.documentation = 'hello'
export = ET.tostring(export_to_indented_xml(comment_template))
export = export.replace(b'documentation>', b'description>')
comment_template2 = CommentTemplate.import_from_xml_tree(ET.fromstring(export))
comment_template2.store()
comment_template2.refresh_from_storage()
assert comment_template2.documentation
def test_comment_template_documentation(pub, superuser):
CommentTemplate.wipe()
comment_template = CommentTemplate(name='foobar')
comment_template.store()
app = login(get_app(pub))
resp = app.get(comment_template.get_admin_url())
assert resp.pyquery('.documentation[hidden]')
resp = app.post_json(comment_template.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
comment_template.refresh_from_storage()
assert comment_template.documentation == '<p>doc</p>'
resp = app.get(comment_template.get_admin_url())
assert resp.pyquery('.documentation:not([hidden])')

View File

@ -542,3 +542,40 @@ def test_mail_template_with_category(pub):
export = ET.tostring(mail_template.export_to_xml(include_id=True))
mail_template3 = MailTemplate.import_from_xml_tree(ET.fromstring(export), include_id=True)
assert mail_template3.category_id is None
def test_mail_template_migration(pub):
mail_template = MailTemplate(name='test template')
mail_template.description = 'hello'
assert mail_template.migrate() is True
assert not mail_template.description
assert mail_template.documentation == 'hello'
def test_mail_template_legacy_xml(pub):
mail_template = MailTemplate(name='test template')
mail_template.documentation = 'hello'
export = ET.tostring(export_to_indented_xml(mail_template))
export = export.replace(b'documentation>', b'description>')
mail_template2 = MailTemplate.import_from_xml_tree(ET.fromstring(export))
mail_template2.store()
mail_template2.refresh_from_storage()
assert mail_template2.documentation
def test_mail_template_documentation(pub, superuser):
MailTemplate.wipe()
mail_template = MailTemplate(name='foobar')
mail_template.store()
app = login(get_app(pub))
resp = app.get(mail_template.get_admin_url())
assert resp.pyquery('.documentation[hidden]')
resp = app.post_json(mail_template.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
mail_template.refresh_from_storage()
assert mail_template.documentation == '<p>doc</p>'
resp = app.get(mail_template.get_admin_url())
assert resp.pyquery('.documentation:not([hidden])')

View File

@ -1101,3 +1101,38 @@ def test_import_root_node_error():
excinfo.value.msg
== 'Provided XML file is invalid, it starts with a <wrong_root_node> tag instead of <workflow>'
)
def test_documentation_attributes(pub):
Workflow.wipe()
workflow = Workflow(name='Workflow One')
workflow.documentation = 'doc1'
status = workflow.add_status(name='New status')
status.documentation = 'doc2'
action = status.add_action('anonymise')
action.documentation = 'doc3'
workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow)
workflow.backoffice_fields_formdef.documentation = 'doc4'
workflow.backoffice_fields_formdef.fields = [
StringField(id='bo234', label='bo field 1'),
]
workflow.backoffice_fields_formdef.fields[0].documentation = 'doc5'
workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
workflow.variables_formdef.documentation = 'doc6'
workflow.variables_formdef.fields = [
StringField(id='va123', label='var field 1'),
]
workflow.variables_formdef.fields[0].documentation = 'doc7'
global_action = workflow.add_global_action('action1')
global_action.documentation = 'doc8'
workflow.store()
wf2 = assert_import_export_works(workflow)
assert wf2.documentation == 'doc1'
assert wf2.possible_status[0].documentation == 'doc2'
assert wf2.possible_status[0].items[0].documentation == 'doc3'
assert wf2.backoffice_fields_formdef.documentation == 'doc4'
assert wf2.backoffice_fields_formdef.fields[0].documentation == 'doc5'
assert wf2.variables_formdef.documentation == 'doc6'
assert wf2.variables_formdef.fields[0].documentation == 'doc7'
assert wf2.global_actions[0].documentation == 'doc8'

View File

@ -55,6 +55,7 @@ class BlockDirectory(FieldsDirectory):
'duplicate',
('history', 'snapshots_dir'),
'overwrite',
('update-documentation', 'update_documentation'),
]
field_def_page_class = BlockFieldDefPage
blacklisted_types = ['page', 'table', 'table-select', 'tablerows', 'ranked-items', 'blocks', 'computed']
@ -113,11 +114,13 @@ class BlockDirectory(FieldsDirectory):
r += htmltext('<li><a href="export">%s</a></li>') % _('Export')
r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
r += htmltext('</ul>')
r += self.get_documentable_button()
r += htmltext('<a href="settings" rel="popup" role="button">%s</a>') % _('Settings')
r += htmltext('</span>')
r += htmltext('</div>')
r += utils.last_modification_block(obj=self.objectdef)
r += get_session().display_message()
r += self.get_documentable_zone()
if not self.objectdef.fields:
r += htmltext('<div class="infonotice">%s</div>') % _('There are not yet any fields defined.')

View File

@ -20,6 +20,7 @@ from quixote.html import TemplateIO, htmltext
from wcs.admin import utils
from wcs.admin.categories import CommentTemplateCategoriesDirectory, get_categories
from wcs.admin.documentable import DocumentableMixin
from wcs.backoffice.applications import ApplicationsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.categories import CommentTemplateCategory
@ -169,7 +170,7 @@ class CommentTemplatesDirectory(Directory):
return redirect('%s/' % comment_template.id)
class CommentTemplatePage(Directory):
class CommentTemplatePage(Directory, DocumentableMixin):
_q_exports = [
'',
'edit',
@ -177,6 +178,7 @@ class CommentTemplatePage(Directory):
'duplicate',
'export',
('history', 'snapshots_dir'),
('update-documentation', 'update_documentation'),
]
do_not_call_in_templates = True
@ -187,6 +189,8 @@ class CommentTemplatePage(Directory):
raise errors.TraversalError()
get_response().breadcrumb.append((component + '/', self.comment_template.name))
self.snapshots_dir = SnapshotsDirectory(self.comment_template)
self.documented_object = self.comment_template
self.documented_element = self.comment_template
def get_sidebar(self):
r = TemplateIO(html=True)
@ -247,14 +251,6 @@ class CommentTemplatePage(Directory):
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',
@ -307,7 +303,6 @@ class CommentTemplatePage(Directory):
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:

View File

@ -20,6 +20,7 @@ from quixote.html import TemplateIO, htmltext
from wcs.admin import utils
from wcs.admin.categories import DataSourceCategoriesDirectory, get_categories
from wcs.admin.documentable import DocumentableMixin
from wcs.backoffice.applications import ApplicationsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.carddef import CardDef
@ -45,7 +46,6 @@ from wcs.qommon.form import (
SingleSelectWidget,
SlugWidget,
StringWidget,
TextWidget,
WidgetDict,
WidgetList,
get_response,
@ -73,14 +73,6 @@ class NamedDataSourceUI:
options=category_options,
value=self.datasource.category_id,
)
form.add(
TextWidget,
'description',
title=_('Description'),
cols=40,
rows=5,
value=self.datasource.description,
)
if not self.datasource or (
self.datasource.type != 'wcs:users' and self.datasource.external != 'agenda_manual'
):
@ -297,7 +289,7 @@ class NamedDataSourceUI:
self.datasource.store()
class NamedDataSourcePage(Directory):
class NamedDataSourcePage(Directory, DocumentableMixin):
_q_exports = [
'',
'edit',
@ -306,6 +298,7 @@ class NamedDataSourcePage(Directory):
'duplicate',
('history', 'snapshots_dir'),
('preview-block', 'preview_block'),
('update-documentation', 'update_documentation'),
]
do_not_call_in_templates = True
@ -319,6 +312,8 @@ class NamedDataSourcePage(Directory):
self.datasource_ui = NamedDataSourceUI(self.datasource)
get_response().breadcrumb.append((component + '/', self.datasource.name))
self.snapshots_dir = SnapshotsDirectory(self.datasource)
self.documented_object = self.datasource
self.documented_element = self.datasource
def get_sidebar(self):
r = TemplateIO(html=True)

62
wcs/admin/documentable.py Normal file
View File

@ -0,0 +1,62 @@
# w.c.s. - web application for online forms
# Copyright (C) 2005-2024 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 <http://www.gnu.org/licenses/>.
import json
from quixote import get_request, get_response
from quixote.html import htmltext
from wcs.qommon import _, template
from wcs.qommon.form import RichTextWidget
class DocumentableMixin:
def get_documentable_button(self):
return htmltext(template.render('wcs/backoffice/includes/documentation-editor-link.html', {}))
def get_documentable_zone(self):
return htmltext('<span class="actions">%s</span>') % template.render(
'wcs/backoffice/includes/documentation.html',
{'element': self.documented_element, 'object': self.documented_object},
)
def update_documentation(self):
get_request().ignore_session = True
get_response().set_content_type('application/json')
try:
content = get_request().json['content']
except (KeyError, TypeError):
return json.dumps({'err': 1})
content = RichTextWidget('').clean_html(content) or None
changed = False
if content != self.documented_element.documentation:
changed = True
self.documented_element.documentation = content
self.documented_object.store(_('Documentation update'))
return json.dumps(
{'err': 0, 'empty': not bool(self.documented_element.documentation), 'changed': changed}
)
class DocumentableFieldMixin:
def documentation_part(self):
if not self.field.documentation:
get_response().filter['sidebar_attrs'] = 'hidden'
return template.render(
'wcs/backoffice/includes/documentation.html',
{'element': self.documented_element, 'object': self.documented_object},
)

View File

@ -26,18 +26,21 @@ from wcs.admin import utils
from wcs.carddef import CardDef
from wcs.fields import BlockField, get_field_options
from wcs.formdef import FormDef, UpdateStatisticsDataAfterJob
from wcs.qommon import _, errors, get_cfg, misc
from wcs.qommon import _, errors, get_cfg, misc, template
from wcs.qommon.admin.menu import command_icon
from wcs.qommon.form import CheckboxWidget, Form, HtmlWidget, OptGroup, SingleSelectWidget, StringWidget
from wcs.qommon.substitution import CompatibilityNamesDict
from .documentable import DocumentableFieldMixin, DocumentableMixin
class FieldDefPage(Directory):
_q_exports = ['', 'delete', 'duplicate']
class FieldDefPage(Directory, DocumentableMixin, DocumentableFieldMixin):
_q_exports = ['', 'delete', 'duplicate', ('update-documentation', 'update_documentation')]
large = False
page_id = None
blacklisted_attributes = []
is_documentable = True
def __init__(self, objectdef, field_id):
self.objectdef = objectdef
@ -47,6 +50,8 @@ class FieldDefPage(Directory):
raise errors.TraversalError()
if not self.field.label:
self.field.label = str(_('None'))
self.documented_object = objectdef
self.documented_element = self.field
label = misc.ellipsize(self.field.unhtmled_label, 40)
last_breadcrumb_url_part, last_breadcrumb_label = get_response().breadcrumb[-1]
get_response().breadcrumb = get_response().breadcrumb[:-1]
@ -67,7 +72,11 @@ class FieldDefPage(Directory):
return form
def get_sidebar(self):
return None
if not self.is_documentable:
return None
r = TemplateIO(html=True)
r += self.documentation_part()
return r.getvalue()
def _q_index(self):
form = self.form()
@ -94,9 +103,15 @@ class FieldDefPage(Directory):
get_response().set_title(self.objectdef.name)
get_response().filter['sidebar'] = self.get_sidebar() # noqa pylint: disable=assignment-from-none
r = TemplateIO(html=True)
r += htmltext('<div id="appbar" class="field-edit">')
r += htmltext('<h2 class="field-edit--title">%s</h2>') % misc.ellipsize(
self.field.unhtmled_label, 80
)
if self.is_documentable:
r += htmltext('<span class="actions">%s</span>') % template.render(
'wcs/backoffice/includes/documentation-editor-link.html', {}
)
r += htmltext('</div>')
if isinstance(self.field, BlockField):
try:
block_field = self.field.block
@ -329,8 +344,15 @@ class FieldsPagesDirectory(Directory):
return directory
class FieldsDirectory(Directory):
_q_exports = ['', 'update_order', 'move_page_fields', 'new', 'pages']
class FieldsDirectory(Directory, DocumentableMixin):
_q_exports = [
'',
'update_order',
'move_page_fields',
'new',
'pages',
('update-documentation', 'update_documentation'),
]
field_def_page_class = FieldDefPage
blacklisted_types = []
page_id = None
@ -345,6 +367,8 @@ class FieldsDirectory(Directory):
def __init__(self, objectdef):
self.objectdef = objectdef
self.documented_object = self.objectdef
self.documented_element = self.objectdef
self.pages = FieldsPagesDirectory(self)
def _q_traverse(self, path):

View File

@ -69,6 +69,7 @@ from . import utils
from .blocks import BlocksDirectory
from .categories import CategoriesDirectory, get_categories
from .data_sources import NamedDataSourcesDirectory
from .documentable import DocumentableMixin
from .fields import FieldDefPage, FieldsDirectory
from .logged_errors import LoggedErrorsDirectory
@ -621,7 +622,7 @@ class WorkflowRoleDirectory(Directory):
return redirect('..')
class FormDefPage(Directory, TempfileDirectoryMixin):
class FormDefPage(Directory, TempfileDirectoryMixin, DocumentableMixin):
do_not_call_in_templates = True
_q_exports = [
'',
@ -648,6 +649,7 @@ class FormDefPage(Directory, TempfileDirectoryMixin):
('backoffice-submission-roles', 'backoffice_submission_roles'),
('logged-errors', 'logged_errors_dir'),
('history', 'snapshots_dir'),
('update-documentation', 'update_documentation'),
]
formdef_class = FormDef
@ -691,6 +693,8 @@ class FormDefPage(Directory, TempfileDirectoryMixin):
parent_dir=self, formdef_class=self.formdef_class, formdef_id=self.formdef.id
)
self.snapshots_dir = SnapshotsDirectory(self.formdef)
self.documented_object = self.formdef
self.documented_element = self.formdef
def add_option_line(self, link, label, current_value, popup=True):
return htmltext(

View File

@ -20,6 +20,7 @@ from quixote.html import TemplateIO, htmltext
from wcs.admin import utils
from wcs.admin.categories import MailTemplateCategoriesDirectory, get_categories
from wcs.admin.documentable import DocumentableMixin
from wcs.backoffice.applications import ApplicationsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.categories import MailTemplateCategory
@ -167,7 +168,7 @@ class MailTemplatesDirectory(Directory):
return redirect('%s/' % mail_template.id)
class MailTemplatePage(Directory):
class MailTemplatePage(Directory, DocumentableMixin):
_q_exports = [
'',
'edit',
@ -175,6 +176,7 @@ class MailTemplatePage(Directory):
'duplicate',
'export',
('history', 'snapshots_dir'),
('update-documentation', 'update_documentation'),
]
do_not_call_in_templates = True
@ -185,6 +187,8 @@ class MailTemplatePage(Directory):
raise errors.TraversalError()
get_response().breadcrumb.append((component + '/', self.mail_template.name))
self.snapshots_dir = SnapshotsDirectory(self.mail_template)
self.documented_object = self.mail_template
self.documented_element = self.mail_template
def get_sidebar(self):
r = TemplateIO(html=True)
@ -242,15 +246,6 @@ class MailTemplatePage(Directory):
options=category_options,
value=self.mail_template.category_id,
)
form.add(
TextWidget,
'description',
title=_('Description'),
cols=80,
rows=3,
value=self.mail_template.description,
)
form.add(
StringWidget,
'subject',
@ -312,7 +307,6 @@ class MailTemplatePage(Directory):
self.mail_template.name = name
if form.get_widget('category_id'):
self.mail_template.category_id = form.get_widget('category_id').parse()
self.mail_template.description = form.get_widget('description').parse()
self.mail_template.subject = form.get_widget('subject').parse()
self.mail_template.body = form.get_widget('body').parse()
self.mail_template.attachments = form.get_widget('attachments').parse()

View File

@ -131,6 +131,7 @@ authentication is unavailable. Lasso must be installed to use it.'
class UserFieldDefPage(FieldDefPage):
blacklisted_attributes = ['condition']
is_documentable = False
class UserFieldsDirectory(FieldsDirectory):

View File

@ -67,6 +67,7 @@ from wcs.workflows import (
from . import utils
from .comment_templates import CommentTemplatesDirectory
from .data_sources import NamedDataSourcesDirectory
from .documentable import DocumentableFieldMixin, DocumentableMixin
from .fields import FieldDefPage, FieldsDirectory
from .logged_errors import LoggedErrorsDirectory
from .mail_templates import MailTemplatesDirectory
@ -440,8 +441,13 @@ class WorkflowUI:
return workflow
class WorkflowItemPage(Directory):
_q_exports = ['', 'delete', 'copy']
class WorkflowItemPage(Directory, DocumentableMixin):
_q_exports = [
'',
'delete',
'copy',
('update-documentation', 'update_documentation'),
]
do_not_call_in_templates = True
def __init__(self, workflow, parent, component):
@ -451,6 +457,8 @@ class WorkflowItemPage(Directory):
raise errors.TraversalError()
self.workflow = workflow
self.parent = parent
self.documented_object = self.workflow
self.documented_element = self.item
get_response().breadcrumb.append(('items/%s/' % component, self.item.description))
def _q_index(self):
@ -495,6 +503,8 @@ class WorkflowItemPage(Directory):
context = {
'view': self,
'html_form': form,
'workflow': self.workflow,
'has_sidebar': True,
'action': self.item,
'get_substitution_html_table': get_publisher().substitutions.get_substitution_html_table,
}
@ -673,7 +683,7 @@ class GlobalActionItemsDir(ToChildDirectory):
klass = WorkflowItemPage
class WorkflowStatusPage(Directory):
class WorkflowStatusPage(Directory, DocumentableMixin):
_q_exports = [
'',
'delete',
@ -687,6 +697,7 @@ class WorkflowStatusPage(Directory):
'fullscreen',
('schema.svg', 'svg'),
'svg',
('update-documentation', 'update_documentation'),
]
do_not_call_in_templates = True
@ -698,6 +709,8 @@ class WorkflowStatusPage(Directory):
raise errors.TraversalError()
self.items_dir = WorkflowItemsDir(workflow, self.status)
self.documented_object = self.workflow
self.documented_element = self.status
get_response().breadcrumb.append(('status/%s/' % status_id, self.status.name))
def _q_index(self):
@ -1087,9 +1100,16 @@ class WorkflowStatusDirectory(Directory):
return r.getvalue()
class WorkflowVariablesFieldDefPage(FieldDefPage):
class WorkflowVariablesFieldDefPage(FieldDefPage, DocumentableFieldMixin):
section = 'workflows'
blacklisted_attributes = ['condition', 'prefill', 'display_locations', 'anonymise']
has_documentation = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.workflow = self.objectdef.workflow
self.documented_object = self.workflow
self.documented_element = self.field
def form(self):
form = super().form()
@ -1115,7 +1135,7 @@ class WorkflowVariablesFieldDefPage(FieldDefPage):
super().submit(form)
class WorkflowBackofficeFieldDefPage(FieldDefPage):
class WorkflowBackofficeFieldDefPage(FieldDefPage, DocumentableFieldMixin):
section = 'workflows'
blacklisted_attributes = ['condition']
@ -1128,21 +1148,23 @@ class WorkflowBackofficeFieldDefPage(FieldDefPage):
continue
if any(x.get('field_id') == self.field.id for x in action.fields or []):
usage_actions.append(action)
if not usage_actions:
return
r = TemplateIO(html=True)
r += htmltext('<div class="actions-using-this-field">')
r += htmltext('<h3>%s</h3>') % _('Actions using this field')
r += htmltext('<ul>')
for action in usage_actions:
label = _('"%s" action') % action.label if action.label else _('Action')
if isinstance(action.parent, WorkflowGlobalAction):
location = _('in global action "%s"') % action.parent.name
else:
location = _('in status "%s"') % action.parent.name
r += htmltext(f'<li><a href="{action.get_admin_url()}">%s %s</a></li>') % (label, location)
r += htmltext('</ul>')
r += htmltext('<div>')
r += self.documentation_part()
if usage_actions:
get_response().filter['sidebar_attrs'] = ''
r += htmltext('<div class="actions-using-this-field">')
r += htmltext('<h3>%s</h3>') % _('Actions using this field')
r += htmltext('<ul>')
for action in usage_actions:
label = _('"%s" action') % action.label if action.label else _('Action')
if isinstance(action.parent, WorkflowGlobalAction):
location = _('in global action "%s"') % action.parent.name
else:
location = _('in status "%s"') % action.parent.name
r += htmltext(f'<li><a href="{action.get_admin_url()}">%s %s</a></li>') % (label, location)
r += htmltext('</ul>')
r += htmltext('<div>')
return r.getvalue()
def form(self):
@ -1164,7 +1186,7 @@ class WorkflowBackofficeFieldDefPage(FieldDefPage):
class WorkflowVariablesFieldsDirectory(FieldsDirectory):
_q_exports = ['', 'update_order', 'new']
_q_exports = ['', 'update_order', 'new', ('update-documentation', 'update_documentation')]
section = 'workflows'
field_def_page_class = WorkflowVariablesFieldDefPage
@ -1181,8 +1203,12 @@ class WorkflowVariablesFieldsDirectory(FieldsDirectory):
def index_top(self):
r = TemplateIO(html=True)
r += htmltext('<div id="appbar">')
r += htmltext('<h2>%s - %s - %s</h2>') % (_('Workflow'), self.objectdef.name, _('Variables'))
r += htmltext('<span class="actions">%s</span>') % self.get_documentable_button()
r += htmltext('</div>')
r += get_session().display_message()
r += self.get_documentable_zone()
if not self.objectdef.fields:
r += htmltext('<p>%s</p>') % _('There are not yet any variables.')
return r.getvalue()
@ -1192,7 +1218,7 @@ class WorkflowVariablesFieldsDirectory(FieldsDirectory):
class WorkflowBackofficeFieldsDirectory(FieldsDirectory):
_q_exports = ['', 'update_order', 'new']
_q_exports = ['', 'update_order', 'new', ('update-documentation', 'update_documentation')]
section = 'workflows'
field_def_page_class = WorkflowBackofficeFieldDefPage
@ -1207,10 +1233,19 @@ class WorkflowBackofficeFieldsDirectory(FieldsDirectory):
fields_count_total_soft_limit = 40
fields_count_total_hard_limit = 80
def __init__(self, objectdef):
super().__init__(objectdef)
self.documented_object = objectdef
self.documented_element = objectdef
def index_top(self):
r = TemplateIO(html=True)
r += htmltext('<div id="appbar">')
r += htmltext('<h2>%s - %s - %s</h2>') % (_('Workflow'), self.objectdef.name, _('Backoffice Fields'))
r += htmltext('<span class="actions">%s</span>') % self.get_documentable_button()
r += htmltext('</div>')
r += get_session().display_message()
r += self.get_documentable_zone()
if not self.objectdef.fields:
r += htmltext('<p>%s</p>') % _('There are not yet any backoffice fields.')
return r.getvalue()
@ -1431,6 +1466,7 @@ class GlobalActionPage(WorkflowStatusPage):
('triggers', 'triggers_dir'),
'update_triggers_order',
'options',
('update-documentation', 'update_documentation'),
]
def __init__(self, workflow, action_id):
@ -1442,6 +1478,8 @@ class GlobalActionPage(WorkflowStatusPage):
self.status = self.action
self.items_dir = GlobalActionItemsDir(workflow, self.action)
self.triggers_dir = GlobalActionTriggersDir(workflow, self.action)
self.documented_object = self.workflow
self.documented_element = self.action
def _q_traverse(self, path):
get_response().breadcrumb.append(
@ -1619,7 +1657,7 @@ class GlobalActionsDirectory(Directory):
return r.getvalue()
class WorkflowPage(Directory):
class WorkflowPage(Directory, DocumentableMixin):
_q_exports = [
'',
'edit',
@ -1643,6 +1681,7 @@ class WorkflowPage(Directory):
('logged-errors', 'logged_errors_dir'),
('history', 'snapshots_dir'),
('fullscreen'),
('update-documentation', 'update_documentation'),
]
do_not_call_in_templates = True
@ -1665,6 +1704,8 @@ class WorkflowPage(Directory):
self.criticality_levels_dir = CriticalityLevelsDirectory(self.workflow)
self.logged_errors_dir = LoggedErrorsDirectory(parent_dir=self, workflow_id=self.workflow.id)
self.snapshots_dir = SnapshotsDirectory(self.workflow)
self.documented_object = self.workflow
self.documented_element = self.workflow
if component:
get_response().breadcrumb.append((component + '/', self.workflow.name))

View File

@ -21,10 +21,11 @@ from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
from wcs.admin import utils
from wcs.admin.documentable import DocumentableMixin
from wcs.backoffice.applications import ApplicationsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.qommon import _, errors, misc, template
from wcs.qommon.form import CheckboxWidget, FileWidget, Form, HtmlWidget, SlugWidget, StringWidget, TextWidget
from wcs.qommon.form import CheckboxWidget, FileWidget, Form, HtmlWidget, SlugWidget, StringWidget
from wcs.utils import grep_strings
from wcs.wscalls import NamedWsCall, NamedWsCallImportError, WsCallRequestWidget
@ -38,9 +39,6 @@ class NamedWsCallUI:
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.wscall.name)
form.add(
TextWidget, 'description', title=_('Description'), cols=40, rows=5, value=self.wscall.description
)
if self.wscall.slug:
form.add(
SlugWidget,
@ -100,7 +98,6 @@ class NamedWsCallUI:
raise ValueError()
self.wscall.name = name
self.wscall.description = form.get_widget('description').parse()
self.wscall.notify_on_errors = form.get_widget('notify_on_errors').parse()
self.wscall.record_on_errors = form.get_widget('record_on_errors').parse()
self.wscall.request = form.get_widget('request').parse()
@ -109,7 +106,7 @@ class NamedWsCallUI:
self.wscall.store()
class NamedWsCallPage(Directory):
class NamedWsCallPage(Directory, DocumentableMixin):
do_not_call_in_templates = True
_q_exports = [
'',
@ -118,6 +115,7 @@ class NamedWsCallPage(Directory):
'export',
('history', 'snapshots_dir'),
'usage',
('update-documentation', 'update_documentation'),
]
def __init__(self, component, instance=None):
@ -128,6 +126,8 @@ class NamedWsCallPage(Directory):
self.wscall_ui = NamedWsCallUI(self.wscall)
get_response().breadcrumb.append((component + '/', self.wscall.name))
self.snapshots_dir = SnapshotsDirectory(self.wscall)
self.documented_object = self.wscall
self.documented_element = self.wscall
def get_sidebar(self):
r = TemplateIO(html=True)

View File

@ -60,11 +60,12 @@ class BlockDef(StorableObject):
fields = None
digest_template = None
category_id = None
documentation = None
SLUG_DASH = '_'
# declarations for serialization
TEXT_ATTRIBUTES = ['name', 'slug', 'digest_template']
TEXT_ATTRIBUTES = ['name', 'slug', 'digest_template', 'documentation']
def __init__(self, name=None, **kwargs):
super().__init__(**kwargs)

View File

@ -34,7 +34,7 @@ class CommentTemplate(XmlStorableObject):
name = None
slug = None
description = None
documentation = None
comment = None
attachments = []
category_id = None
@ -43,7 +43,8 @@ class CommentTemplate(XmlStorableObject):
XML_NODES = [
('name', 'str'),
('slug', 'str'),
('description', 'str'),
('description', 'str'), # legacy
('documentation', 'str'),
('comment', 'str'),
('attachments', 'str_list'),
]
@ -52,6 +53,16 @@ class CommentTemplate(XmlStorableObject):
XmlStorableObject.__init__(self)
self.name = name
def migrate(self):
changed = False
if getattr(self, 'description', None): # 2024-04-07
self.documentation = getattr(self, 'description')
self.description = None
changed = True
if changed:
self.store(comment=_('Automatic update'), snapshot_store_user=False)
return changed
@property
def category(self):
return CommentTemplateCategory.get(self.category_id, ignore_errors=True)
@ -67,14 +78,16 @@ class CommentTemplate(XmlStorableObject):
base_url = get_publisher().get_backoffice_url()
return '%s/workflows/comment-templates/%s/' % (base_url, self.id)
def store(self, comment=None, application=None, *args, **kwargs):
def store(self, comment=None, snapshot_store_user=True, application=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, application=application)
get_publisher().snapshot_class.snap(
instance=self, store_user=snapshot_store_user, comment=comment, application=application
)
def get_places_of_use(self):
from wcs.workflows import Workflow

View File

@ -676,7 +676,7 @@ class NamedDataSource(XmlStorableObject):
name = None
slug = None
description = None
documentation = None
data_source = None
cache_duration = None
query_parameter = None
@ -702,7 +702,8 @@ class NamedDataSource(XmlStorableObject):
XML_NODES = [
('name', 'str'),
('slug', 'str'),
('description', 'str'),
('description', 'str'), # legacy
('documentation', 'str'),
('cache_duration', 'str'),
('query_parameter', 'str'),
('id_parameter', 'str'),
@ -737,6 +738,11 @@ class NamedDataSource(XmlStorableObject):
self.data_source['value'] = translate_url(publisher, url)
changed = True
if getattr(self, 'description', None): # 2024-04-07
self.documentation = getattr(self, 'description')
self.description = None
changed = True
if changed:
self.store(comment=_('Automatic update'), snapshot_store_user=False)

View File

@ -223,6 +223,7 @@ class Field:
is_no_data_field = False
can_include_in_listing = False
available_for_filter = False
documentation = None
# flag a field for removal by AnonymiseWorkflowStatusItem
# possible values are final, intermediate, no.
@ -232,7 +233,7 @@ class Field:
# declarations for serialization, they are mostly for legacy files,
# new exports directly include typing attributes.
TEXT_ATTRIBUTES = ['label', 'type', 'hint', 'varname', 'extra_css_class']
TEXT_ATTRIBUTES = ['label', 'type', 'hint', 'varname', 'extra_css_class', 'documentation']
def __init__(self, **kwargs):
for k, v in kwargs.items():
@ -320,7 +321,7 @@ class Field:
def export_to_xml(self, include_id=False):
field = ET.Element('field')
extra_fields = ['default_value'] # specific to workflow variables
extra_fields = ['default_value', 'documentation'] # default_value is specific to workflow variables
if include_id:
extra_fields.append('id')
ET.SubElement(field, 'type').text = self.key
@ -359,7 +360,7 @@ class Field:
return field
def init_with_xml(self, elem, include_id=False, snapshot=False):
extra_fields = ['default_value'] # specific to workflow variables
extra_fields = ['documentation', 'default_value'] # default_value is specific to workflow variables
for attribute in self.get_admin_attributes() + extra_fields:
el = elem.find(attribute)
if hasattr(self, '%s_init_with_xml' % attribute):

View File

@ -186,6 +186,7 @@ class FormDef(StorableObject):
drafts_lifespan = None
drafts_max_per_user = None
user_support = None
documentation = None
geolocations = None
history_pane_default_mode = 'expanded'
@ -219,6 +220,7 @@ class FormDef(StorableObject):
'drafts_lifespan',
'drafts_max_per_user',
'user_support',
'documentation',
]
BOOLEAN_ATTRIBUTES = [
'discussion',

View File

@ -34,7 +34,7 @@ class MailTemplate(XmlStorableObject):
name = None
slug = None
description = None
documentation = None
subject = None
body = None
attachments = []
@ -44,7 +44,8 @@ class MailTemplate(XmlStorableObject):
XML_NODES = [
('name', 'str'),
('slug', 'str'),
('description', 'str'),
('description', 'str'), # legacy
('documentation', 'str'),
('subject', 'str'),
('body', 'str'),
('attachments', 'str_list'),
@ -54,6 +55,16 @@ class MailTemplate(XmlStorableObject):
XmlStorableObject.__init__(self)
self.name = name
def migrate(self):
changed = False
if getattr(self, 'description', None): # 2024-04-07
self.documentation = getattr(self, 'description')
self.description = None
changed = True
if changed:
self.store(comment=_('Automatic update'), snapshot_store_user=False)
return changed
@property
def category(self):
return MailTemplateCategory.get(self.category_id, ignore_errors=True)
@ -69,14 +80,16 @@ class MailTemplate(XmlStorableObject):
base_url = get_publisher().get_backoffice_url()
return '%s/workflows/mail-templates/%s/' % (base_url, self.id)
def store(self, comment=None, application=None, *args, **kwargs):
def store(self, comment=None, snapshot_store_user=True, application=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, application=application)
get_publisher().snapshot_class.snap(
instance=self, store_user=snapshot_store_user, comment=comment, application=application
)
def get_places_of_use(self):
from wcs.workflows import Workflow

View File

@ -2681,49 +2681,49 @@ class WysiwygTextWidget(TextWidget):
def get_plain_text_value(self):
return misc.html2text(self.value)
def clean_html(self, value):
try:
from bleach.css_sanitizer import CSSSanitizer
css_sanitizer = CSSSanitizer(allowed_css_properties=self.ALL_STYLES)
kwargs = {
'css_sanitizer': css_sanitizer,
}
except ModuleNotFoundError:
# bleach < 5
kwargs = {'styles': self.ALL_STYLES}
cleaner = Cleaner(
tags=getattr(self, 'allowed_tags', None) or self.ALL_TAGS,
attributes=self.ALL_ATTRS,
strip=True,
strip_comments=False,
filters=[
partial(
linkifier.LinkifyFilter,
skip_tags=['pre'],
parse_email=True,
url_re=self.URL_RE,
email_re=self.EMAIL_RE,
)
],
**kwargs,
)
value = cleaner.clean(value).removeprefix('<br />').removesuffix('<br />')
if not strip_tags(value).strip() and not ('<img' in value or '<hr' in value):
value = ''
return value
def _parse(self, request):
TextWidget._parse(self, request, use_validation_function=False)
if self.value:
all_tags = self.ALL_TAGS[:]
self.allowed_tags = self.ALL_TAGS[:]
if get_publisher().get_site_option('ckeditor-allow-style-tag'):
all_tags.append('style')
self.allowed_tags.append('style')
if get_publisher().get_site_option('ckeditor-allow-script-tag'):
all_tags.append('script')
self.allowed_tags.append('script')
try:
from bleach.css_sanitizer import CSSSanitizer
css_sanitizer = CSSSanitizer(allowed_css_properties=self.ALL_STYLES)
kwargs = {
'css_sanitizer': css_sanitizer,
}
except ModuleNotFoundError:
# bleach < 5
kwargs = {'styles': self.ALL_STYLES}
cleaner = Cleaner(
tags=all_tags,
attributes=self.ALL_ATTRS,
strip=True,
strip_comments=False,
filters=[
partial(
linkifier.LinkifyFilter,
skip_tags=['pre'],
parse_email=True,
url_re=self.URL_RE,
email_re=self.EMAIL_RE,
)
],
**kwargs,
)
self.value = cleaner.clean(self.value)
if self.value.startswith('<br />'):
self.value = self.value[6:]
if self.value.endswith('<br />'):
self.value = self.value[:-6]
if not strip_tags(self.value).strip() and not ('<img' in self.value or '<hr' in self.value):
self.value = ''
self.value = self.clean_html(self.value)
# unescape Django template tags
def unquote_django(matchobj):

View File

@ -3243,3 +3243,83 @@ div.dataview.compact-dataview {
display: flex;
}
}
.ro-documentation {
p:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
}
.bo-block.documentation {
position: relative;
margin-bottom: 0;
button.save {
display: none;
position: absolute;
height: 2.5em;
bottom: -1.25em;
right: 0.5em;
}
&.active {
button.save {
display: block;
}
border-color: var(--primary-color);
}
}
aside .bo-block.documentation {
// keep some space for godo toolbar
margin-top: 5em;
}
.focus-editor-link {
&::before {
font-family: FontAwesome;
content: "\f040"; // pencil
}
}
.godo.html-edition,
.godo.html-edition--show {
--padding: 0.5em;
outline: none;
background: transparent;
padding-bottom: 0;
p:last-child {
margin-bottom: 0;
}
.godo--editor {
min-height: auto;
border: none;
padding: 0;
}
}
.godo.html-edition--show {
.godo--editor > :first-child {
padding-top: var(--padding);
}
}
.documentation-save-marks {
position: absolute;
right: 0.5em;
margin-top: -1.5em;
span {
visibility: hidden;
margin-left: -0.5em;
}
}
#appbar.field-edit {
margin-bottom: 0;
}
#appbar > h2 {
// always keep a bit of space, for documentation button
max-width: calc(100% - 80px);
}

View File

@ -519,4 +519,81 @@ $(function() {
el.parentNode.classList.toggle('display-codepoints')
})
)
const documentation_block = document.querySelector('.bo-block.documentation')
const editor = document.getElementById('documentation-editor')
const editor_link = document.querySelector('.focus-editor-link')
const title_byline = document.querySelector('.object--status-infos')
const documentation_save_button = document.querySelector('.bo-block.documentation button.save')
var clear_documentation_save_marks_timeout_id = null
if (editor_link) {
document.querySelector('#documentation-editor .godo--editor').setAttribute('contenteditable', 'false')
documentation_save_button.addEventListener('click', (e) => {
editor.sourceContent = editor.getHTML()
var documentation_message = Object()
documentation_message['content'] = editor.sourceContent.innerHTML
document.querySelector('.documentation-save-marks .mark-error').style.visibility = 'hidden'
document.querySelector('.documentation-save-marks .mark-success').style.visibility = 'hidden'
document.querySelector('.documentation-save-marks .mark-sent').style.visibility = 'visible'
fetch(`${window.location.pathname}update-documentation`, {
method: 'POST',
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
body: JSON.stringify(documentation_message)
}).then((response) => {
if (! response.ok) {
return
}
return response.json()
}).then((json) => {
if (json && json.err == 0) {
if (json.changed) {
document.querySelector('.documentation-save-marks .mark-success').style.visibility = 'visible'
} else {
document.querySelector('.documentation-save-marks .mark-sent').style.visibility = 'hidden'
document.querySelector('.documentation-save-marks .mark-success').style.visibility = 'hidden'
}
if (json.empty) {
document.querySelector('.bo-block.documentation').setAttribute('hidden', 'hidden')
}
} else {
document.querySelector('.documentation-save-marks .mark-error').style.visibility = 'visible'
}
if (clear_documentation_save_marks_timeout_id) clearTimeout(clear_documentation_save_marks_timeout_id)
clear_documentation_save_marks_timeout_id = setTimeout(
function() {
document.querySelector('.documentation-save-marks .mark-error').style.visibility = 'hidden'
document.querySelector('.documentation-save-marks .mark-success').style.visibility = 'hidden'
document.querySelector('.documentation-save-marks .mark-sent').style.visibility = 'hidden'
}, 5000)
})
})
editor_link.addEventListener('click', (e) => {
e.preventDefault()
if (editor_link.getAttribute('aria-pressed') == 'true') {
editor.validEdition()
documentation_save_button.dispatchEvent(new Event('click'))
documentation_block.classList.remove('active')
document.querySelector('#documentation-editor .godo--editor').setAttribute('contenteditable', 'false')
editor_link.setAttribute('aria-pressed', false)
if (title_byline) title_byline.style.visibility = 'visible'
} else {
documentation_block.classList.add('active')
document.querySelector('.bo-block.documentation').removeAttribute('hidden')
if (document.querySelector('aside .bo-block.documentation')) {
document.getElementById('sidebar').style.display = 'block'
document.getElementById('sidebar').removeAttribute('hidden')
if (document.getElementById('sticky-sidebar').style.display == 'none') {
document.getElementById('sidebar-toggle').dispatchEvent(new Event('click'))
}
}
if (title_byline) title_byline.style.visibility = 'hidden'
editor_link.setAttribute('aria-pressed', true)
document.querySelector('#documentation-editor .godo--editor').setAttribute('contenteditable', 'true')
editor.showEdition()
editor.view.focus()
}
})
}
});

View File

@ -3,6 +3,10 @@
CKEDITOR.env.origIsCompatible = CKEDITOR.env.isCompatible;
CKEDITOR.env.isCompatible = true;
/* do not turn all contenteditable into ckeditor, as some pages may have both
* godo and ckeditor */
CKEDITOR.disableAutoInline = true;
$(document).ready( function() {
if (CKEDITOR.env.origIsCompatible == false) {
/* bail out if ckeditor advertised itself as not supported */

View File

@ -52,7 +52,7 @@
{% block sidebar %}
{% if sidebar or has_sidebar %}
<aside id="sidebar">
<aside id="sidebar" {% block sidebar-attrs %}{{ sidebar_attrs|default:"" }}{% endblock %}>
<button id="sidebar-toggle" aria-label="{% trans "Toggle sidebar" %}">&#8286;</button>
<div id="sticky-sidebar">
{% block sidebar-content %}

View File

@ -17,6 +17,7 @@
{{ publisher.get_request.session.display_message|safe }}
{% include "wcs/backoffice/includes/sql-fields-integrity.html" %}
{% include "wcs/backoffice/includes/documentation.html" with element=formdef object=formdef %}
<div class="bo-block">
<h3>{% trans "Information" %}</h3>

View File

@ -5,14 +5,13 @@
{% block appbar-actions %}
{% if not comment_template.is_readonly %}
{% include "wcs/backoffice/includes/documentation-editor-link.html" %}
<a href="edit">{% trans "Edit" %}</a>
{% endif %}
{% endblock %}
{% block content %}
{% if comment_template.description %}
<div class="bo-block">{{ comment_template.description }}</div>
{% endif %}
{% include "wcs/backoffice/includes/documentation.html" with element=comment_template object=comment_template %}
{% if comment_template.comment %}
<div class="section">

View File

@ -5,14 +5,13 @@
<h2>{% trans "Data Source" %} - {{ datasource.name }}</h2>
<span class="actions">
{% if not datasource.is_readonly %}
{% include "wcs/backoffice/includes/documentation-editor-link.html" %}
<a href="edit">{% trans "Edit" %}</a>
{% endif %}
</span>
</div>
{% if datasource.description %}
<div class="bo-block">{{ datasource.description }}</div>
{% endif %}
{% include "wcs/backoffice/includes/documentation.html" with element=datasource object=datasource %}
{% if datasource.data_source %}
<div class="section">

View File

@ -16,6 +16,7 @@
<li><a href="export">{% trans "Export" %}</a></li>
<li><a href="delete" rel="popup">{% trans "Delete" %}</a></li>
</ul>
{% include "wcs/backoffice/includes/documentation-editor-link.html" %}
<a rel="popup" href="title">{% trans "change title" %}</a>
{% endif %}
{% endblock %}
@ -35,7 +36,9 @@
</div>
{{ publisher.get_request.session.display_message|safe }}
{% include "wcs/backoffice/includes/sql-fields-integrity.html" %}
{% include "wcs/backoffice/includes/documentation.html" with element=formdef object=formdef %}
<div class="bo-block">
<h3>{% trans "Information" %}</h3>

View File

@ -0,0 +1,2 @@
{% load i18n %}
<a class="focus-editor-link" title="{% trans "Edit documentation" %}"><span class="sr-only">{% trans "Edit documentation" %}</span></a>

View File

@ -0,0 +1,21 @@
{% load i18n %}
<div class="bo-block documentation" {% if not element.documentation %}hidden{% endif %}>
{% if object.is_readonly %}
<div class="ro-documentation">{{ element.documentation|safe }}</div>
{% else %}
<script type="module" src="/static/xstatic/js/godo.js?{{version_hash}}"></script>
<div class="documentation-save-marks">
<span class="mark-error"></span>
<span class="mark-success"></span>
<span class="mark-sent"></span>
</div>
<div id="div-godo-source" >{{ element.documentation|default:"<p></p>"|safe }}</div>
<godo-editor
tabindex="0"
linked-source="div-godo-source"
heading-levels="3,4"
id="documentation-editor"
></godo-editor>
<button class="save">{% trans "Save" %}</button>
{% endif %}
</div>

View File

@ -5,14 +5,13 @@
{% block appbar-actions %}
{% if not mail_template.is_readonly %}
{% include "wcs/backoffice/includes/documentation-editor-link.html" %}
<a href="edit">{% trans "Edit" %}</a>
{% endif %}
{% endblock %}
{% block content %}
{% if mail_template.description %}
<div class="bo-block">{{ mail_template.description }}</div>
{% endif %}
{% include "wcs/backoffice/includes/documentation.html" with element=mail_template object=mail_template %}
{% if mail_template.subject and mail_template.body %}
<div class="section">

View File

@ -3,6 +3,12 @@
{% block appbar-title %}{{ action.description }}{% endblock %}
{% block appbar-actions %}
{% if not workflow.is_readonly %}
{% include "wcs/backoffice/includes/documentation-editor-link.html" %}
{% endif %}
{% endblock %}
{% block content %}
{{ block.super }}
@ -13,3 +19,11 @@
{% endif %}
{% endblock %}
{% block sidebar-attrs %}
{% if not action.documentation %}style="display: none"{% endif %}
{% endblock %}
{% block sidebar-content %}
{% include "wcs/backoffice/includes/documentation.html" with element=action object=workflow %}
{% endblock %}

View File

@ -11,12 +11,15 @@
{% endif %}
</ul>
{% if not workflow.is_readonly %}
{% include "wcs/backoffice/includes/documentation-editor-link.html" %}
<a rel="popup" href="options">{% trans "Options" %}</a>
<a rel="popup" href="edit">{% trans "Change Name" %}</a>
{% endif %}
{% endblock %}
{% block body %}
{% include "wcs/backoffice/includes/documentation.html" with element=action object=workflow %}
<div class="bo-block">
<h2>{% trans "Actions" %}</h2>
{% if not action.items %}

View File

@ -11,6 +11,7 @@
{% endif %}
</ul>
{% if not workflow.is_readonly %}
{% include "wcs/backoffice/includes/documentation-editor-link.html" %}
<a href="options">{% trans "Options" %}</a>
<a rel="popup" href="edit">{% trans "Change Name" %}</a>
{% endif %}
@ -19,6 +20,8 @@
{% block content %}
{{ block.super }}
{% include "wcs/backoffice/includes/documentation.html" with element=status object=workflow %}
{% with visibility_mode=status.get_visibility_mode %}
{% if visibility_mode != 'all' %}
<div class="bo-block">

View File

@ -12,6 +12,7 @@
{% endif %}
</ul>
{% if not workflow.is_readonly %}
{% include "wcs/backoffice/includes/documentation-editor-link.html" %}
<a rel="popup" href="category">{% trans "change category" %}</a>
<a rel="popup" href="edit">{% trans "change title" %}</a>
{% endif %}
@ -22,6 +23,8 @@
{{ view.last_modification_block|safe }}
</div>
{% include "wcs/backoffice/includes/documentation.html" with element=workflow object=workflow %}
<div class="splitcontent-left">
<div class="bo-block">
<h3>{% trans "Possible Status" %}

View File

@ -5,14 +5,13 @@
<h2>{% trans "Webservice Call" %} - {{ wscall.name }}</h2>
{% if not wscall.is_readonly %}
<span class="actions">
{% include "wcs/backoffice/includes/documentation-editor-link.html" %}
<a href="edit">{% trans "Edit" %}</a>
</span>
{% endif %}
</div>
{% if wscall.description %}
<div class="bo-block">{{ wscall.description }}</div>
{% endif %}
{% include "wcs/backoffice/includes/documentation.html" with element=wscall object=wscall %}
<div class="bo-block">
<h3>{% trans "Parameters" %}</h3>

View File

@ -118,6 +118,7 @@ class WorkflowFormFieldsFormDef(FormDef):
class WorkflowFormFieldDefPage(FieldDefPage):
section = 'workflows'
blacklisted_attributes = ['display_locations', 'anonymise']
is_documentable = False
def get_deletion_extra_warning(self):
return None

View File

@ -714,8 +714,9 @@ class WorkflowVariablesFieldsFormDef(FormDef):
self.workflow = workflow
if self.workflow.is_readonly():
self.readonly = True
if workflow.variables_formdef and workflow.variables_formdef.fields:
self.fields = self.workflow.variables_formdef.fields
if workflow.variables_formdef:
self.documentation = workflow.variables_formdef.documentation
self.fields = self.workflow.variables_formdef.fields or []
else:
self.fields = []
@ -768,8 +769,9 @@ class WorkflowBackofficeFieldsFormDef(FormDef):
def __init__(self, workflow):
self.id = None
self.workflow = workflow
if workflow.backoffice_fields_formdef and workflow.backoffice_fields_formdef.fields:
self.fields = self.workflow.backoffice_fields_formdef.fields
if workflow.backoffice_fields_formdef:
self.documentation = workflow.backoffice_fields_formdef.documentation
self.fields = self.workflow.backoffice_fields_formdef.fields or []
else:
self.fields = []
@ -804,6 +806,7 @@ class Workflow(StorableObject):
name = None
slug = None
documentation = None
possible_status = None
roles = None
variables_formdef = None
@ -1207,9 +1210,10 @@ class Workflow(StorableObject):
root = ET.Element('workflow')
if include_id and self.id and not str(self.id).startswith('_'):
root.attrib['id'] = str(self.id)
ET.SubElement(root, 'name').text = self.name
if self.slug:
ET.SubElement(root, 'slug').text = self.slug
for attr in ('name', 'slug', 'documentation'):
value = getattr(self, attr, None)
if value:
ET.SubElement(root, attr).text = value
WorkflowCategory.object_category_xml_export(self, root, include_id=include_id)
@ -1238,6 +1242,8 @@ class Workflow(StorableObject):
variables = ET.SubElement(root, 'variables')
formdef = ET.SubElement(variables, 'formdef')
ET.SubElement(formdef, 'name').text = '-' # required by formdef xml import
if self.variables_formdef.documentation:
ET.SubElement(formdef, 'documentation').text = self.variables_formdef.documentation
fields = ET.SubElement(formdef, 'fields')
for field in self.variables_formdef.fields:
fields.append(field.export_to_xml(include_id=include_id))
@ -1246,6 +1252,8 @@ class Workflow(StorableObject):
variables = ET.SubElement(root, 'backoffice-fields')
formdef = ET.SubElement(variables, 'formdef')
ET.SubElement(formdef, 'name').text = '-' # required by formdef xml import
if self.backoffice_fields_formdef.documentation:
ET.SubElement(formdef, 'documentation').text = self.backoffice_fields_formdef.documentation
fields = ET.SubElement(formdef, 'fields')
for field in self.backoffice_fields_formdef.fields:
fields.append(field.export_to_xml(include_id=include_id))
@ -1297,8 +1305,9 @@ class Workflow(StorableObject):
workflow.name = xml_node_text(tree.find('name'))
if tree.find('slug') is not None:
workflow.slug = xml_node_text(tree.find('slug'))
for attribute in ('slug', 'documentation'):
if tree.find(attribute) is not None:
setattr(workflow, attribute, xml_node_text(tree.find(attribute)))
WorkflowCategory.object_category_xml_import(workflow, tree, include_id=include_id)
@ -1365,6 +1374,7 @@ class Workflow(StorableObject):
raise WorkflowImportError(e.msg, details=e.details)
else:
workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
workflow.variables_formdef.documentation = imported_formdef.documentation
workflow.variables_formdef.fields = imported_formdef.fields
variables = tree.find('backoffice-fields')
@ -1381,6 +1391,7 @@ class Workflow(StorableObject):
raise WorkflowImportError(e.msg, details=e.details)
else:
workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow=workflow)
workflow.backoffice_fields_formdef.documentation = imported_formdef.documentation
workflow.backoffice_fields_formdef.fields = imported_formdef.fields
if unknown_referenced_objects_details:
@ -1585,7 +1596,7 @@ class XmlSerialisable:
node.attrib['type'] = self.key
if include_id and getattr(self, 'id', None):
node.attrib['id'] = self.id
for attribute in self.get_parameters():
for attribute in self.get_parameters() + ('documentation',):
if getattr(self, '%s_export_to_xml' % attribute, None):
getattr(self, '%s_export_to_xml' % attribute)(node, include_id=include_id)
continue
@ -1611,7 +1622,7 @@ class XmlSerialisable:
def init_with_xml(self, elem, include_id=False, snapshot=False, check_datasources=True):
if include_id and elem.attrib.get('id'):
self.id = elem.attrib.get('id')
for attribute in self.get_parameters():
for attribute in self.get_parameters() + ('documentation',):
el = elem.find(attribute)
if getattr(self, '%s_init_with_xml' % attribute, None):
getattr(self, '%s_init_with_xml' % attribute)(el, include_id=include_id, snapshot=snapshot)
@ -2513,6 +2524,7 @@ class WorkflowGlobalAction(SerieOfActionsMixin):
name = None
triggers = None
backoffice_info_text = None
documentation = None
def __init__(self, name=None):
self.name = name
@ -2575,11 +2587,11 @@ class WorkflowGlobalAction(SerieOfActionsMixin):
def export_to_xml(self, include_id=False):
status = ET.Element('action')
ET.SubElement(status, 'id').text = self.id
ET.SubElement(status, 'name').text = self.name
if self.backoffice_info_text:
ET.SubElement(status, 'backoffice_info_text').text = self.backoffice_info_text
for attr in ('id', 'name', 'backoffice_info_text', 'documentation'):
value = getattr(self, attr, None)
if value:
ET.SubElement(status, attr).text = str(value)
items = ET.SubElement(status, 'items')
for item in self.items:
@ -2592,10 +2604,10 @@ class WorkflowGlobalAction(SerieOfActionsMixin):
return status
def init_with_xml(self, elem, include_id=False, snapshot=False):
self.id = xml_node_text(elem.find('id'))
self.name = xml_node_text(elem.find('name'))
if elem.find('backoffice_info_text') is not None:
self.backoffice_info_text = xml_node_text(elem.find('backoffice_info_text'))
for attr in ('id', 'name', 'backoffice_info_text', 'documentation'):
node = elem.find(attr)
if node is not None:
setattr(self, attr, xml_node_text(node))
self.items = []
for item in elem.find('items'):
@ -2689,6 +2701,7 @@ class WorkflowStatus(SerieOfActionsMixin):
forced_endpoint = False
colour = '#FFFFFF'
backoffice_info_text = None
documentation = None
extra_css_class = ''
loop_items_template = None
after_loop_status = None
@ -2948,18 +2961,24 @@ class WorkflowStatus(SerieOfActionsMixin):
def export_to_xml(self, include_id=False):
status = ET.Element('status')
ET.SubElement(status, 'id').text = str(self.id)
ET.SubElement(status, 'name').text = self.name
ET.SubElement(status, 'colour').text = self.colour
if self.extra_css_class:
ET.SubElement(status, 'extra_css_class').text = self.extra_css_class
for attr in (
'id',
'name',
'colour',
'extra_css_class',
'backoffice_info_text',
'loop_items_template',
'after_loop_status',
'documentation',
):
value = getattr(self, attr, None)
if value:
ET.SubElement(status, attr).text = str(value)
if self.forced_endpoint:
ET.SubElement(status, 'forced_endpoint').text = 'true'
if self.backoffice_info_text:
ET.SubElement(status, 'backoffice_info_text').text = self.backoffice_info_text
visibility_node = ET.SubElement(status, 'visibility')
for role in self.visibility or []:
ET.SubElement(visibility_node, 'role').text = str(role)
@ -2968,28 +2987,25 @@ class WorkflowStatus(SerieOfActionsMixin):
for item in self.items:
items.append(item.export_to_xml(include_id=include_id))
if self.loop_items_template:
ET.SubElement(status, 'loop_items_template').text = self.loop_items_template
if self.after_loop_status:
ET.SubElement(status, 'after_loop_status').text = self.after_loop_status
return status
def init_with_xml(self, elem, include_id=False, snapshot=False, check_datasources=True):
self.id = xml_node_text(elem.find('id'))
self.name = xml_node_text(elem.find('name'))
if elem.find('colour') is not None:
self.colour = xml_node_text(elem.find('colour'))
if elem.find('extra_css_class') is not None:
self.extra_css_class = xml_node_text(elem.find('extra_css_class'))
for attr in (
'id',
'name',
'colour',
'extra_css_class',
'backoffice_info_text',
'loop_items_template',
'after_loop_status',
'documentation',
):
node = elem.find(attr)
if node is not None:
setattr(self, attr, xml_node_text(node))
if elem.find('forced_endpoint') is not None:
self.forced_endpoint = elem.find('forced_endpoint').text == 'true'
if elem.find('backoffice_info_text') is not None:
self.backoffice_info_text = xml_node_text(elem.find('backoffice_info_text'))
if elem.find('loop_items_template') is not None:
self.loop_items_template = xml_node_text(elem.find('loop_items_template'))
if elem.find('after_loop_status') is not None:
self.after_loop_status = xml_node_text(elem.find('after_loop_status'))
self.visibility = []
for visibility_role in elem.findall('visibility/role'):
@ -3047,6 +3063,7 @@ class WorkflowStatusItem(XmlSerialisable):
category = None # (key, label)
id = None
condition = None
documentation = None
endpoint = True # means it's not possible to interact, and/or cause a status change
waitpoint = False # means it's possible to wait (user interaction, or other event)

View File

@ -250,7 +250,7 @@ class NamedWsCall(XmlStorableObject):
name = None
slug = None
description = None
documentation = None
request = None
notify_on_errors = False
record_on_errors = False
@ -261,7 +261,8 @@ class NamedWsCall(XmlStorableObject):
XML_NODES = [
('name', 'str'),
('slug', 'str'),
('description', 'str'),
('description', 'str'), # legacy
('documentation', 'str'),
('request', 'request'),
('notify_on_errors', 'bool'),
('record_on_errors', 'bool'),
@ -271,6 +272,16 @@ class NamedWsCall(XmlStorableObject):
XmlStorableObject.__init__(self)
self.name = name
def migrate(self):
changed = False
if getattr(self, 'description', None): # 2024-04-07
self.documentation = getattr(self, 'description')
self.description = None
changed = True
if changed:
self.store(comment=_('Automatic update'), snapshot_store_user=False)
return changed
def get_admin_url(self):
base_url = get_publisher().get_backoffice_url()
return '%s/settings/wscalls/%s/' % (base_url, self.slug)
@ -331,7 +342,7 @@ class NamedWsCall(XmlStorableObject):
request['post_formdata'] = bool(element.find('post_formdata') is not None)
return request
def store(self, comment=None, application=None, *args, **kwargs):
def store(self, comment=None, snapshot_store_user=True, application=None, *args, **kwargs):
assert not self.is_readonly()
if self.slug is None:
# set slug if it's not yet there
@ -341,7 +352,9 @@ class NamedWsCall(XmlStorableObject):
self.id = self.slug
super().store(*args, **kwargs)
if get_publisher().snapshot_class:
get_publisher().snapshot_class.snap(instance=self, comment=comment, application=application)
get_publisher().snapshot_class.snap(
instance=self, comment=comment, store_user=snapshot_store_user, application=application
)
@classmethod
def get_substitution_variables(cls):