From 687a2456cea7e5eac42d8fa3ddda4d8a3eadb709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Mon, 6 Jun 2016 18:55:21 +0200 Subject: [PATCH 1/9] general: add support for backoffice fields (#8273) --- tests/test_admin_pages.py | 77 ++++++++++++++++++++++++ tests/test_api.py | 20 ++++++- tests/test_backoffice_pages.py | 37 +++++++++++- tests/test_formdata.py | 21 ++++++- tests/test_workflow_import.py | 15 +++++ tests/test_workflows.py | 38 +++++++++++- wcs/admin/workflows.py | 59 +++++++++++++++++++ wcs/backoffice/management.py | 14 ++--- wcs/formdata.py | 6 +- wcs/formdef.py | 5 +- wcs/forms/common.py | 44 ++++++++++---- wcs/qommon/static/css/dc2/admin.css | 30 ++++++++++ wcs/sql.py | 16 ++--- wcs/wf/backoffice_fields.py | 114 ++++++++++++++++++++++++++++++++++++ wcs/workflows.py | 74 ++++++++++++++++++++--- 15 files changed, 531 insertions(+), 39 deletions(-) create mode 100644 wcs/wf/backoffice_fields.py diff --git a/tests/test_admin_pages.py b/tests/test_admin_pages.py index be156c3..1d52c1c 100644 --- a/tests/test_admin_pages.py +++ b/tests/test_admin_pages.py @@ -1614,6 +1614,83 @@ def test_workflows_variables_edit(pub): assert Workflow.get(1).variables_formdef.fields[0].key == 'string' assert Workflow.get(1).variables_formdef.fields[0].varname == '1*1*message' +def test_workflows_backoffice_fields(pub): + create_superuser(pub) + create_role() + + Workflow.wipe() + workflow = Workflow(name='foo') + workflow.add_status(name='baz') + workflow.store() + + formdef = FormDef() + formdef.name = 'form title' + formdef.workflow_id = workflow.id + formdef.fields = [] + formdef.store() + + app = login(get_app(pub)) + resp = app.get('/backoffice/workflows/1/') + resp = resp.click('baz') + assert not 'Set Backoffice Field' in resp.body + + resp = app.get('/backoffice/workflows/1/') + resp = resp.click(href='backoffice-fields/') + assert resp.location == 'http://example.net/backoffice/workflows/1/backoffice-fields/fields/' + resp = resp.follow() + + # makes sure we can't add page fields + assert 'value="New Page"' not in resp.body + + # add a simple field + resp.forms[0]['label'] = 'foobar' + resp.forms[0]['type'] = 'Text (line)' + resp = resp.forms[0].submit() + assert resp.location == 'http://example.net/backoffice/workflows/1/backoffice-fields/fields/' + resp = resp.follow() + + # check it's been saved correctly + assert 'foobar' in resp.body + assert len(Workflow.get(1).backoffice_fields_formdef.fields) == 1 + assert Workflow.get(1).backoffice_fields_formdef.fields[0].id.startswith('bo') + assert Workflow.get(1).backoffice_fields_formdef.fields[0].key == 'string' + assert Workflow.get(1).backoffice_fields_formdef.fields[0].label == 'foobar' + + backoffice_field_id = Workflow.get(1).backoffice_fields_formdef.fields[0].id + formdef = FormDef.get(formdef.id) + data_class = formdef.data_class() + data_class.wipe() + formdata = data_class() + formdata.data = {backoffice_field_id: 'HELLO'} + formdata.status = 'wf-new' + formdata.store() + + assert data_class.get(formdata.id).data[backoffice_field_id] == 'HELLO' + + # check the "set backoffice fields" action is now available + resp = app.get('/backoffice/workflows/1/') + resp = resp.click('baz') + resp.forms[0]['type'] = 'Set Backoffice Fields' + resp = resp.forms[0].submit() + resp = resp.follow() + + resp = resp.click('Set Backoffice Fields') + + # add a second field + resp = app.get('/backoffice/workflows/1/') + resp = resp.click(href='backoffice-fields/', index=0) + assert resp.location == 'http://example.net/backoffice/workflows/1/backoffice-fields/fields/' + resp = resp.follow() + resp.forms[0]['label'] = 'foobar2' + resp.forms[0]['type'] = 'Text (line)' + resp = resp.forms[0].submit() + assert resp.location == 'http://example.net/backoffice/workflows/1/backoffice-fields/fields/' + resp = resp.follow() + workflow = Workflow.get(workflow.id) + assert len(workflow.backoffice_fields_formdef.fields) == 2 + assert workflow.backoffice_fields_formdef.fields[0].id == 'bo1' + assert workflow.backoffice_fields_formdef.fields[1].id == 'bo2' + def test_workflows_functions(pub): create_superuser(pub) create_role() diff --git a/tests/test_api.py b/tests/test_api.py index 0f28c64..6cf5792 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -20,7 +20,7 @@ from wcs.formdef import FormDef from wcs.formdata import Evolution from wcs.categories import Category from wcs.data_sources import NamedDataSource -from wcs.workflows import Workflow, EditableWorkflowStatusItem +from wcs.workflows import Workflow, EditableWorkflowStatusItem, WorkflowBackofficeFieldsFormDef from wcs.wf.jump import JumpWorkflowStatusItem from wcs import fields, qommon from wcs.api_utils import sign_url @@ -830,6 +830,24 @@ def test_formdata(pub, local_user): resp2 = get_app(pub).get(sign_uri('/test/%s/' % formdata.id, user=local_user), status=403) +def test_formdata_backoffice_fields(pub, local_user): + test_formdata(pub, local_user) + workflow = Workflow.get(2) + workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow) + workflow.backoffice_fields_formdef.fields = [ + fields.StringField(id='bo1', label='1st backoffice field', + type='string', varname='backoffice_blah'), + ] + workflow.store() + + formdef = FormDef.select()[0] + formdata = formdef.data_class().select()[0] + formdata.data['bo1'] = 'Hello world' + formdata.store() + + resp = get_app(pub).get(sign_uri('/api/forms/test/%s/' % formdata.id, user=local_user)) + assert resp.json['workflow']['fields']['backoffice_blah'] == 'Hello world' + def test_formdata_edit(pub, local_user): test_formdata(pub, local_user) formdef = FormDef.select()[0] diff --git a/tests/test_backoffice_pages.py b/tests/test_backoffice_pages.py index f650d5f..b485392 100644 --- a/tests/test_backoffice_pages.py +++ b/tests/test_backoffice_pages.py @@ -20,7 +20,8 @@ from wcs.qommon.http_request import HTTPRequest from wcs.roles import Role from wcs.workflows import (Workflow, CommentableWorkflowStatusItem, ChoiceWorkflowStatusItem, EditableWorkflowStatusItem, - JumpOnSubmitWorkflowStatusItem, WorkflowCriticalityLevel) + JumpOnSubmitWorkflowStatusItem, WorkflowCriticalityLevel, + WorkflowBackofficeFieldsFormDef) from wcs.wf.dispatch import DispatchWorkflowStatusItem from wcs.wf.wscall import WebserviceCallStatusItem from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem @@ -2437,3 +2438,37 @@ def test_inspect_page(pub): assert (pq('[title="form_var_foo_str_but_non_utf8"]') .parents('li').children('div.value span') .text() == '\'\\xed\\xa0\\x00\'') + +def test_backoffice_fields(pub): + user = create_user(pub) + create_environment(pub) + + wf = Workflow(name='bo fields') + wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf) + wf.backoffice_fields_formdef.fields = [ + fields.StringField(id='bo1', label='1st backoffice field', + type='string', varname='backoffice_blah'), + ] + st1 = wf.add_status('Status1') + wf.store() + + formdef = FormDef.get_by_urlname('form-title') + formdef.workflow_id = wf.id + formdef.store() + + formdata = formdef.data_class()() + formdata.data = {} + formdata.just_created() + formdata.store() + + app = login(get_app(pub)) + resp = app.get(formdata.get_url(backoffice=True)) + assert not 'Backoffice Data' in resp.body + assert not '1st backoffice field' in resp.body + + formdata.data = {'bo1': 'HELLO WORLD'} + formdata.store() + resp = app.get(formdata.get_url(backoffice=True)) + assert 'Backoffice Data' in resp.body + assert '1st backoffice field' in resp.body + assert 'HELLO WORLD' in resp.body diff --git a/tests/test_formdata.py b/tests/test_formdata.py index a23c41e..7e03db5 100644 --- a/tests/test_formdata.py +++ b/tests/test_formdata.py @@ -10,7 +10,7 @@ from wcs.qommon.http_request import HTTPRequest from wcs import fields, formdef from wcs.formdef import FormDef from wcs.formdata import Evolution -from wcs.workflows import Workflow, WorkflowCriticalityLevel +from wcs.workflows import Workflow, WorkflowCriticalityLevel, WorkflowBackofficeFieldsFormDef from wcs.wf.anonymise import AnonymiseWorkflowStatusItem from wcs.wf.wscall import JournalWsCallErrorPart from wcs.wf.register_comment import JournalEvolutionPart @@ -529,3 +529,22 @@ def test_field_bool_substvars(pub): variables = formdata.get_substitution_variables() assert variables.get('form_var_xxx') == 'True' assert variables.get('form_var_xxx_raw') is True + +def test_backoffice_field_varname(pub): + wf = Workflow(name='bo fields') + wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf) + wf.backoffice_fields_formdef.fields = [ + fields.StringField(id='bo1', label='1st backoffice field', + type='string', varname='backoffice_blah'), + ] + st1 = wf.add_status('Status1') + wf.store() + + formdef.workflow_id = wf.id + formdef.data_class().wipe() + formdef.fields = [fields.StringField(id='0', label='string', varname='foo')] + formdef.store() + formdata = formdef.data_class()() + formdata.data = {'bo1': 'test'} + substvars = formdata.get_substitution_variables() + assert substvars.get('form_var_backoffice_blah') == 'test' diff --git a/tests/test_workflow_import.py b/tests/test_workflow_import.py index af367dc..f9eeaf4 100644 --- a/tests/test_workflow_import.py +++ b/tests/test_workflow_import.py @@ -12,6 +12,7 @@ from wcs.wf.wscall import WebserviceCallStatusItem from wcs.wf.dispatch import DispatchWorkflowStatusItem from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem from wcs.wf.profile import UpdateUserProfileStatusItem +from wcs.wf.backoffice_fields import SetBackofficeFieldsWorkflowStatusItem from wcs.roles import Role from wcs.fields import StringField @@ -468,3 +469,17 @@ def test_profile_action(): wf2 = assert_import_export_works(wf) item2 = wf2.possible_status[0].items[0] assert item2.fields == [{'field_id': '__email', 'value': '=form_var_foo'}] + +def test_set_backoffice_fields_action(): + wf = Workflow(name='status') + st1 = wf.add_status('Status1', 'st1') + + item = SetBackofficeFieldsWorkflowStatusItem() + item.id = '_item' + item.fields = [{'field_id': 'bo1', 'value': '=form_var_foo'}] + st1.items.append(item) + item.parent = st1 + + wf2 = assert_import_export_works(wf) + item2 = wf2.possible_status[0].items[0] + assert item2.fields == [{'field_id': 'bo1', 'value': '=form_var_foo'}] diff --git a/tests/test_workflows.py b/tests/test_workflows.py index a109723..ebd47a7 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -20,7 +20,7 @@ from wcs.workflows import (Workflow, WorkflowStatusItem, SendmailWorkflowStatusItem, SendSMSWorkflowStatusItem, DisplayMessageWorkflowStatusItem, AbortActionException, WorkflowCriticalityLevel, - AttachmentEvolutionPart) + AttachmentEvolutionPart, WorkflowBackofficeFieldsFormDef) from wcs.wf.anonymise import AnonymiseWorkflowStatusItem from wcs.wf.criticality import ModifyCriticalityWorkflowStatusItem, MODE_INC, MODE_DEC, MODE_SET from wcs.wf.dispatch import DispatchWorkflowStatusItem @@ -33,6 +33,7 @@ from wcs.wf.roles import AddRoleWorkflowStatusItem, RemoveRoleWorkflowStatusItem from wcs.wf.wscall import WebserviceCallStatusItem from wcs.wf.export_to_model import transform_to_pdf from wcs.wf.geolocate import GeolocateWorkflowStatusItem +from wcs.wf.backoffice_fields import SetBackofficeFieldsWorkflowStatusItem from utilities import (create_temporary_pub, MockSubstitutionVariables, emails, http_requests, clean_temporary_pub, sms_mocking) @@ -1639,3 +1640,38 @@ def test_profile(pub): item.fields = [{'field_id': 'plop', 'value': '=form_var_foo'}] item.perform(formdata) assert pub.user_class.get(user.id).form_data == {'3': 'Plop'} + +def test_set_backoffice_field(pub): + Workflow.wipe() + wf = Workflow(name='xxx') + wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf) + wf.backoffice_fields_formdef.fields = [ + StringField(id='bo1', label='1st backoffice field', + type='string', varname='backoffice_blah'), + ] + st1 = wf.add_status('Status1') + wf.store() + + formdef = FormDef() + formdef.name = 'baz' + formdef.fields = [ + StringField(id='1', label='String', type='string', varname='string'), + ] + formdef.workflow_id = wf.id + formdef.store() + + formdata = formdef.data_class()() + formdata.data = {'1': 'HELLO'} + formdata.just_created() + formdata.store() + pub.substitutions.feed(formdata) + + item = SetBackofficeFieldsWorkflowStatusItem() + item.perform(formdata) + + item = SetBackofficeFieldsWorkflowStatusItem() + item.fields = [{'field_id': 'bo1', 'value': '=form_var_string'}] + item.perform(formdata) + + formdata = formdef.data_class().get(formdata.id) + assert formdata.data['bo1'] == 'HELLO' diff --git a/wcs/admin/workflows.py b/wcs/admin/workflows.py index 736aac1..b9b887c 100644 --- a/wcs/admin/workflows.py +++ b/wcs/admin/workflows.py @@ -832,6 +832,10 @@ class WorkflowVariablesFieldDefPage(FieldDefPage): return form +class WorkflowBackofficeFieldDefPage(FieldDefPage): + section = 'workflows' + + class WorkflowVariablesFieldsDirectory(FieldsDirectory): _q_exports = ['', 'update_order', 'new'] @@ -853,6 +857,27 @@ class WorkflowVariablesFieldsDirectory(FieldsDirectory): pass +class WorkflowBackofficeFieldsDirectory(FieldsDirectory): + _q_exports = ['', 'update_order', 'new'] + + section = 'workflows' + field_def_page_class = WorkflowBackofficeFieldDefPage + support_import = False + blacklisted_types = ['page'] + + def index_top(self): + r = TemplateIO(html=True) + r += htmltext('

%s - %s - %s

') % (_('Workflow'), + self.objectdef.name, _('Backoffice Fields')) + r += get_session().display_message() + if not self.objectdef.fields: + r += htmltext('

%s

') % _('There are not yet any backoffice fields.') + return r.getvalue() + + def index_bottom(self): + pass + + class VariablesDirectory(Directory): _q_exports = ['', 'fields'] @@ -869,6 +894,23 @@ class VariablesDirectory(Directory): return Directory._q_traverse(self, path) +class BackofficeFieldsDirectory(Directory): + _q_exports = ['', 'fields'] + + def __init__(self, workflow): + self.workflow = workflow + + def _q_index(self): + return redirect('fields/') + + def _q_traverse(self, path): + get_response().breadcrumb.append(('backoffice-fields/', _('Backoffice Fields'))) + self.fields = WorkflowBackofficeFieldsDirectory( + WorkflowBackofficeFieldsFormDef(self.workflow)) + return Directory._q_traverse(self, path) + + + class FunctionsDirectory(Directory): _q_exports = ['', 'new'] @@ -1266,6 +1308,7 @@ class GlobalActionsDirectory(Directory): class WorkflowPage(Directory): _q_exports = ['', 'edit', 'delete', 'newstatus', ('status', 'status_dir'), 'update_order', 'duplicate', 'export', 'svg', ('variables', 'variables_dir'), + ('backoffice-fields', 'backoffice_fields_dir'), 'update_actions_order', 'update_criticality_levels_order', ('functions', 'functions_dir'), ('global-actions', 'global_actions_dir'), ('criticality-levels', 'criticality_levels_dir'), @@ -1280,6 +1323,7 @@ class WorkflowPage(Directory): self.workflow_ui = WorkflowUI(self.workflow) self.status_dir = WorkflowStatusDirectory(self.workflow, html_top) self.variables_dir = VariablesDirectory(self.workflow) + self.backoffice_fields_dir = BackofficeFieldsDirectory(self.workflow) self.functions_dir = FunctionsDirectory(self.workflow) self.global_actions_dir = GlobalActionsDirectory(self.workflow, html_top) self.criticality_levels_dir = CriticalityLevelsDirectory(self.workflow) @@ -1425,6 +1469,21 @@ class WorkflowPage(Directory): r += htmltext('') r += htmltext('') + if not str(self.workflow.id).startswith('_'): + r += htmltext('
') + r += htmltext('

%s') % _('Backoffice Fields') + r += htmltext(' (%s)

') % _('change') + if self.workflow.backoffice_fields_formdef: + r += htmltext('') + r += htmltext('
') + r += htmltext('') # .splitcontent-right r += htmltext('
') diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py index b3bf055..fe72fd8 100644 --- a/wcs/backoffice/management.py +++ b/wcs/backoffice/management.py @@ -183,7 +183,7 @@ class UserViewDirectory(Directory): r += htmltext('

%s

') % self.user.display_name formdef = UserFieldsFormDef() r += htmltext('
') - for field in formdef.fields: + for field in formdef.get_all_fields(): if not hasattr(field, str('get_view_value')): continue value = self.user.form_data.get(field.id) @@ -321,7 +321,7 @@ class UsersViewDirectory(Directory): formdef = UserFieldsFormDef() criteria_fields = [ILike('name', query), ILike('email', query)] - for field in formdef.fields: + for field in formdef.get_all_fields(): if field.type in ('string', 'text', 'email'): criteria_fields.append(ILike('f%s' % field.id, query)) if get_publisher().is_using_postgresql(): @@ -342,7 +342,7 @@ class UsersViewDirectory(Directory): r += htmltext('%s') % _('Name') if include_email_column: r += htmltext('%s') % _('Email') - for field in formdef.fields: + for field in formdef.get_all_fields(): if field.in_listing: r += htmltext('%s') % ( field.id, field.label) @@ -356,7 +356,7 @@ class UsersViewDirectory(Directory): r += htmltext('%s') % (user.name or '') if include_email_column: r += htmltext('%s') % (user.email or '') - for field in formdef.fields: + for field in formdef.get_all_fields(): if field.in_listing: r += htmltext('%s') % (user.form_data.get(field.id) or '') r += htmltext('') @@ -1031,7 +1031,7 @@ class FormPage(Directory): fields.append(FakeField('submission_channel', 'submission_channel', _('Channel'))) fields.append(FakeField('time', 'time', _('Time'))) fields.append(FakeField('user-label', 'user-label', _('User Label'))) - fields.extend(self.formdef.fields) + fields.extend(self.formdef.get_all_fields()) fields.append(FakeField('status', 'status', _('Status'))) fields.append(FakeField('anonymised', 'anonymised', _('Anonymised'))) @@ -1041,7 +1041,7 @@ class FormPage(Directory): field_ids = [x for x in get_request().form.keys()] if not field_ids or ignore_form: field_ids = ['id', 'time', 'user-label'] - for field in self.formdef.fields: + for field in self.formdef.get_all_fields(): if hasattr(field, str('get_view_value')) and field.in_listing: field_ids.append(field.id) field_ids.append('status') @@ -1572,7 +1572,7 @@ class FormPage(Directory): had_page = False last_page = None last_title = None - for f in self.formdef.fields: + for f in self.formdef.get_all_fields(): if excluded_fields and f.id in excluded_fields: continue if f.type == 'page': diff --git a/wcs/formdata.py b/wcs/formdata.py index 7b8d099..578b2e9 100644 --- a/wcs/formdata.py +++ b/wcs/formdata.py @@ -463,7 +463,7 @@ class FormData(StorableObject): self.workflow_data.update(dict) def get_as_dict(self): - return get_dict_with_varnames(self.formdef.fields, self.data, self) + return get_dict_with_varnames(self.formdef.get_all_fields(), self.data, self) def get_substitution_variables(self, minimal=False): d = {} @@ -742,6 +742,10 @@ class FormData(StorableObject): # Workflow data have unknown purpose, do not store them in anonymised export if self.workflow_data and not anonymise: data['workflow']['data'] = self.workflow_data + if self.formdef.workflow.get_backoffice_fields(): + data['workflow']['fields'] = get_json_dict( + self.formdef.workflow.get_backoffice_fields(), + self.data, include_files=include_files, anonymise=anonymise) # add a roles dictionary, with workflow functions and two special # entries for concerned/actions roles. diff --git a/wcs/formdef.py b/wcs/formdef.py index 3bed457..a9ab183 100644 --- a/wcs/formdef.py +++ b/wcs/formdef.py @@ -292,7 +292,10 @@ class FormDef(StorableObject): rebuild_global_views=True) return t - def rebuild_views(self): + def get_all_fields(self): + return (self.fields or []) + self.workflow.get_backoffice_fields() + + def rebuild(self): if get_publisher().is_using_postgresql(): import sql sql.do_formdef_tables(self, rebuild_views=True, diff --git a/wcs/forms/common.py b/wcs/forms/common.py index 6d8250d..6460441 100644 --- a/wcs/forms/common.py +++ b/wcs/forms/common.py @@ -420,10 +420,25 @@ class FormStatusPage(Directory): r += htmltext('
%s') % _('User name') r += htmltext('%s
') % user.display_name + r += self.display_fields(self.formdef.fields, form_url) + + if show_status and self.formdef.is_user_allowed_read_status_and_history( + get_request().user, self.filled): + wf_status = self.filled.get_visible_status() + if wf_status: + r += htmltext('
%s ') % _('Status') + r += htmltext('%s
') % wf_status.name + + r += htmltext('
') # .dataview + r += htmltext('') # .bo-block + + return r.getvalue() + + def display_fields(self, fields, form_url=''): + r = TemplateIO(html=True) on_page = False on_disabled_page = False - for f in self.formdef.fields: - + for f in fields: if f.type == 'page': on_disabled_page = False if not f.is_visible(self.filled.data, self.formdef): @@ -485,17 +500,23 @@ class FormStatusPage(Directory): if on_page: r += htmltext('') - if show_status and self.formdef.is_user_allowed_read_status_and_history( - get_request().user, self.filled): - wf_status = self.filled.get_visible_status() - if wf_status: - r += htmltext('

%s ') % _('Status') - r += htmltext('%s

') % wf_status.name - - r += htmltext('') # .dataview - r += htmltext('') # .bo-block return r.getvalue() + def backoffice_fields_section(self): + backoffice_fields = self.formdef.workflow.get_backoffice_fields() + if not backoffice_fields: + return + content = self.display_fields(backoffice_fields) + if not len(content): + return + r = TemplateIO(html=True) + r += htmltext('
') + r += htmltext('

%s

') % _('Backoffice Data') + r += htmltext('
') + r += content + r += htmltext('
') + r += htmltext('
') + return r.getvalue() def status(self): object_key = 'formdata-%s-%s' % (self.formdef.url_name, self.filled.id) @@ -545,6 +566,7 @@ class FormStatusPage(Directory): break r += self.receipt(always_include_user=True, folded=folded) + r += self.backoffice_fields_section() r += self.history() diff --git a/wcs/qommon/static/css/dc2/admin.css b/wcs/qommon/static/css/dc2/admin.css index 9131609..3bc6148 100644 --- a/wcs/qommon/static/css/dc2/admin.css +++ b/wcs/qommon/static/css/dc2/admin.css @@ -1186,6 +1186,36 @@ div.WidgetDict div.content div.content input { width: calc(100% - 1em); } +div.SetBackofficeFieldsTableWidget table { + width: 100%; + border-spacing: 1ex; +} + +div.SetBackofficeFieldsTableWidget th { + text-align: left; +} + +div.SetBackofficeFieldsTableWidget td { + width: 20%; +} + +div.SetBackofficeFieldsTableWidget td + td { + width: 80%; +} + +div.SetBackofficeFieldsTableWidget br { + display: none; +} + +div.SetBackofficeFieldsTableWidget td select, +div.SetBackofficeFieldsTableWidget td input { + width: 100%; +} + +form table div.widget { + margin-bottom: 1ex; +} + div.ComputedExpressionWidget div.content { position: relative; } diff --git a/wcs/sql.py b/wcs/sql.py index 7bb068d..7e409cb 100644 --- a/wcs/sql.py +++ b/wcs/sql.py @@ -378,7 +378,7 @@ def do_formdef_tables(formdef, conn=None, cur=None, rebuild_views=False, rebuild cur.execute('''ALTER TABLE %s ADD COLUMN criticality_level integer NOT NULL DEFAULT(0)''' % table_name) # add new fields - for field in formdef.fields: + for field in formdef.get_all_fields(): assert field.id is not None sql_type = SQL_TYPE_MAPPING.get(field.key, 'varchar') if sql_type is None: @@ -461,7 +461,7 @@ def do_user_table(): from admin.settings import UserFieldsFormDef formdef = UserFieldsFormDef() - for field in formdef.fields: + for field in formdef.get_all_fields(): sql_type = SQL_TYPE_MAPPING.get(field.key, 'varchar') if sql_type is None: continue @@ -602,7 +602,7 @@ def do_views(formdef, conn, cur, rebuild_global_views=True): view_fields = get_view_fields(formdef) column_names = {} - for field in formdef.fields: + for field in formdef.get_all_fields(): field_key = 'f%s' % field.id if field.type in ('page', 'title', 'subtitle', 'comment'): continue @@ -900,7 +900,7 @@ class SqlMixin(object): def get_sql_dict_from_data(self, data, formdef): sql_dict = {} - for field in formdef.fields: + for field in formdef.get_all_fields(): sql_type = SQL_TYPE_MAPPING.get(field.key, 'varchar') if sql_type is None: continue @@ -932,7 +932,7 @@ class SqlMixin(object): i = len(cls._table_static_fields) if formdef.geolocations: i += len(formdef.geolocations.keys()) - for field in formdef.fields: + for field in formdef.get_all_fields(): sql_type = SQL_TYPE_MAPPING.get(field.key, 'varchar') if sql_type is None: continue @@ -1220,7 +1220,7 @@ class SqlFormData(SqlMixin, wcs.formdata.FormData): fts_strings = [str(self.id)] if self.tracking_code: fts_strings.append(self.tracking_code) - for field in self._formdef.fields: + for field in self._formdef.get_all_fields(): if not self.data.get(field.id): continue value = None @@ -1275,7 +1275,7 @@ class SqlFormData(SqlMixin, wcs.formdata.FormData): @classmethod def get_data_fields(cls): data_fields = ['geoloc_%s' % x for x in (cls._formdef.geolocations or {}).keys()] - for field in cls._formdef.fields: + for field in cls._formdef.get_all_fields(): sql_type = SQL_TYPE_MAPPING.get(field.key, 'varchar') if sql_type is None: continue @@ -1492,7 +1492,7 @@ class SqlUser(SqlMixin, wcs.users.User): @classmethod def get_data_fields(cls): data_fields = [] - for field in cls.get_formdef().fields: + for field in cls.get_formdef().get_all_fields(): sql_type = SQL_TYPE_MAPPING.get(field.key, 'varchar') if sql_type is None: continue diff --git a/wcs/wf/backoffice_fields.py b/wcs/wf/backoffice_fields.py new file mode 100644 index 0000000..55ebc1d --- /dev/null +++ b/wcs/wf/backoffice_fields.py @@ -0,0 +1,114 @@ +# w.c.s. - web application for online forms +# Copyright (C) 2005-2016 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 . + +import sys +import xml.etree.ElementTree as ET + +from quixote import get_publisher + +from qommon import get_logger +from qommon.form import WidgetListAsTable, CompositeWidget, SingleSelectWidget, ComputedExpressionWidget +from wcs.workflows import XmlSerialisable, WorkflowStatusItem, register_item_class +from wcs.wf.profile import FieldNode + + +class SetBackofficeFieldRowWidget(CompositeWidget): + def __init__(self, name, value=None, workflow=None, **kwargs): + CompositeWidget.__init__(self, name, value, **kwargs) + if not value: + value = {} + + fields = [('', '', '')] + fields.extend([(x.id, x.label, x.id) for x in workflow.get_backoffice_fields()]) + self.add(SingleSelectWidget, name='field_id', title=_('Field'), + value=value.get('field_id'), + options=fields, **kwargs) + self.add(ComputedExpressionWidget, name='value', title=_('Value'), + value=value.get('value')) + + def _parse(self, request): + if self.get('value') and self.get('field_id'): + self.value = { + 'value': self.get('value'), + 'field_id': self.get('field_id') + } + else: + self.value = None + + +class SetBackofficeFieldsTableWidget(WidgetListAsTable): + readonly = False + def __init__(self, name, **kwargs): + super(SetBackofficeFieldsTableWidget, self).__init__(name, + element_type=SetBackofficeFieldRowWidget, + element_kwargs={'workflow': kwargs.pop('workflow')}, + **kwargs) + + +class SetBackofficeFieldsWorkflowStatusItem(WorkflowStatusItem): + description = N_('Set Backoffice Fields') + key = 'set-backoffice-fields' + + fields = None + + @classmethod + def is_available(cls, workflow=None): + return bool(workflow and getattr(workflow.backoffice_fields_formdef, 'fields', None)) + + def get_parameters(self): + return ('fields',) + + def add_parameters_widgets(self, form, parameters, prefix='', formdef=None): + if 'fields' in parameters: + form.add(SetBackofficeFieldsTableWidget, '%sfields' % prefix, + title=_('Fields Update'), value=self.fields, + workflow=self.parent.parent) + + def perform(self, formdata): + if not self.fields: + return + for field in self.fields: + try: + formdata.data['%s' % field['field_id']] = self.compute( + field['value'], raises=True) + except: + get_publisher().notify_of_exception(sys.exc_info()) + formdata.store() + + def fields_export_to_xml(self, item, charset, include_id=False): + if not self.fields: + return + + fields_node = ET.SubElement(item, 'fields') + for field in self.fields: + fields_node.append(FieldNode(field).export_to_xml(charset=charset, + include_id=include_id)) + + return fields_node + + def fields_init_with_xml(self, elem, charset, include_id=False): + fields = [] + if elem is None: + return + for field_xml_node in elem.findall('field'): + field_node = FieldNode() + field_node.init_with_xml(field_xml_node, charset, + include_id=include_id) + fields.append(field_node.as_dict()) + if fields: + self.fields = fields + +register_item_class(SetBackofficeFieldsWorkflowStatusItem) diff --git a/wcs/workflows.py b/wcs/workflows.py index bcef49c..7e59832 100644 --- a/wcs/workflows.py +++ b/wcs/workflows.py @@ -249,12 +249,39 @@ class WorkflowVariablesFieldsFormDef(FormDef): self.workflow.store() +class WorkflowBackofficeFieldsFormDef(FormDef): + '''Class to handle workflow backoffice fields, it loads and saves from/to + the workflow object 'backoffice_fields_formdef' attribute.''' + + field_prefix = 'bo' + + def __init__(self, workflow): + self.id = None + self.name = workflow.name + self.workflow = workflow + if workflow.backoffice_fields_formdef and workflow.backoffice_fields_formdef.fields: + self.fields = self.workflow.backoffice_fields_formdef.fields + self.max_field_id = max([int(x.id[len(self.field_prefix):]) for x in self.fields]) + else: + self.fields = [] + self.max_field_id = 0 + + def get_new_field_id(self): + self.max_field_id += 1 + return '%s%s' % (self.field_prefix, self.max_field_id) + + def store(self): + self.workflow.backoffice_fields_formdef = self + self.workflow.store() + + class Workflow(StorableObject): _names = 'workflows' name = None possible_status = None roles = None variables_formdef = None + backoffice_fields_formdef = None global_actions = None criticality_levels = None @@ -286,16 +313,28 @@ class Workflow(StorableObject): self.store() def store(self): - must_update_views = False + must_update = False if self.id: old_self = self.get(self.id, ignore_errors=True, ignore_migration=True) if old_self: old_endpoints = set([x.id for x in old_self.get_endpoint_status()]) if old_endpoints != set([x.id for x in self.get_endpoint_status()]): - must_update_views = True + must_update = True old_criticality_levels = len(old_self.criticality_levels or [0]) if old_criticality_levels != len(self.criticality_levels or [0]): - must_update_views = True + must_update = True + try: + old_backoffice_fields = old_self.backoffice_fields_formdef.fields + except AttributeError: + old_backoffice_fields = [] + try: + new_backoffice_fields = self.backoffice_fields_formdef.fields + except AttributeError: + new_backoffice_fields = [] + if len(old_backoffice_fields) != len(new_backoffice_fields): + must_update = True + elif self.backoffice_fields_formdef: + must_update = True self.last_modification_time = time.localtime() if get_request() and get_request().user: @@ -304,12 +343,11 @@ class Workflow(StorableObject): self.last_modification_user_id = None StorableObject.store(self) - # instruct all related formdefs to update their security rules, and - # their views if endpoints have changed. + # instruct all related formdefs to update. for form in FormDef.select(lambda x: x.workflow_id == self.id, ignore_migration=True): form.data_class().rebuild_security() - if must_update_views: - form.rebuild_views() + if must_update: + form.rebuild() @classmethod def get(cls, id, ignore_errors=False, ignore_migration=False): @@ -340,6 +378,11 @@ class Workflow(StorableObject): return status raise KeyError() + def get_backoffice_fields(self): + if self.backoffice_fields_formdef: + return self.backoffice_fields_formdef.fields or [] + return [] + def add_global_action(self, name, id=None): if [x for x in self.global_actions if x.name == name]: raise DuplicateGlobalActionNameError() @@ -475,6 +518,14 @@ class Workflow(StorableObject): for field in self.variables_formdef.fields: fields.append(field.export_to_xml(charset=charset, include_id=include_id)) + if self.backoffice_fields_formdef: + variables = ET.SubElement(root, 'backoffice-fields') + formdef = ET.SubElement(variables, 'formdef') + ET.SubElement(formdef, 'name').text = '-' # required by formdef xml import + fields = ET.SubElement(formdef, 'fields') + for field in self.backoffice_fields_formdef.fields: + fields.append(field.export_to_xml(charset=charset, include_id=include_id)) + return root @classmethod @@ -546,6 +597,14 @@ class Workflow(StorableObject): imported_formdef = FormDef.import_from_xml_tree(formdef, include_id=True) workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow) workflow.variables_formdef.fields = imported_formdef.fields + + variables = tree.find('backoffice-fields') + if variables is not None: + formdef = variables.find('backoffice-fields') + imported_formdef = FormDef.import_from_xml_tree(formdef, include_id=True) + workflow.backoffice_fields_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow) + workflow.backoffice_fields_formdef.fields = imported_formdef.fields + return workflow def get_list_of_roles(self, include_logged_in_users=True): @@ -2233,5 +2292,6 @@ def load_extra(): import wf.resubmit import wf.criticality import wf.profile + import wf.backoffice_fields from wf.export_to_model import ExportToModel -- 2.8.1