general: introduce data card management (#35089)
This commit is contained in:
parent
154fcca2b6
commit
f7f14c0095
17
README
17
README
|
@ -66,6 +66,23 @@ Some artwork from Dotclear:
|
|||
# published by the Free Software Foundation; either version 2 of
|
||||
# the License, or (at your option) any later version.
|
||||
|
||||
Image from the unDraw project:
|
||||
# https://undraw.co/
|
||||
#
|
||||
# All images, assets and vectors published on unDraw can be used for free. You
|
||||
# can use them for noncommercial and commercial purposes. You do not need to ask
|
||||
# permission from or provide credit to the creator or unDraw.
|
||||
#
|
||||
# More precisely, unDraw grants you an nonexclusive, worldwide copyright
|
||||
# license to download, copy, modify, distribute, perform, and use the assets
|
||||
# provided from unDraw for free, including for commercial purposes, without
|
||||
# permission from or attributing the creator or unDraw. This license does not
|
||||
# include the right to compile assets, vectors or images from unDraw to
|
||||
# replicate a similar or competing service, in any form or distribute the assets
|
||||
# in packs. This extends to automated and non-automated ways to link, embed,
|
||||
# scrape, search or download the assets included on the website without our
|
||||
# consent.
|
||||
|
||||
Universal Feed Parser:
|
||||
# __license__ = """Copyright (c) 2002-2007, Mark Pilgrim, All rights reserved.
|
||||
#
|
||||
|
|
|
@ -52,6 +52,11 @@ def welco_url(request, pub):
|
|||
return site_options(request, pub, 'options', 'welco_url', 'http://welco.example.net')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def studio(request, pub):
|
||||
return site_options(request, pub, 'options', 'studio', 'true')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def emails():
|
||||
with EmailsMocking() as mock:
|
||||
|
|
|
@ -45,6 +45,7 @@ from wcs.wf.jump import JumpWorkflowStatusItem
|
|||
from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem
|
||||
from wcs.wf.wscall import WebserviceCallStatusItem
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs import fields
|
||||
|
||||
from utilities import get_app, login, create_temporary_pub, clean_temporary_pub, HttpRequestsMocking
|
||||
|
@ -3997,9 +3998,10 @@ def test_settings_disabled_screens(pub):
|
|||
assert 'Identification' not in resp.body
|
||||
assert 'Theme' not in resp.body
|
||||
|
||||
def test_settings_export_import(pub):
|
||||
def test_settings_export_import(pub, studio):
|
||||
def wipe():
|
||||
FormDef.wipe()
|
||||
CardDef.wipe()
|
||||
Workflow.wipe()
|
||||
Role.wipe()
|
||||
Category.wipe()
|
||||
|
@ -4030,6 +4032,9 @@ def test_settings_export_import(pub):
|
|||
formdef = FormDef()
|
||||
formdef.name = 'foo'
|
||||
formdef.store()
|
||||
carddef = CardDef()
|
||||
carddef.name = 'bar'
|
||||
carddef.store()
|
||||
Category(name='baz').store()
|
||||
Role(name='qux').store()
|
||||
NamedDataSource(name='quux').store()
|
||||
|
@ -4060,6 +4065,8 @@ def test_settings_export_import(pub):
|
|||
filelist = zipf.namelist()
|
||||
assert 'formdefs/1' not in filelist
|
||||
assert 'formdefs_xml/1' in filelist
|
||||
assert 'carddefs/1' not in filelist
|
||||
assert 'carddefs_xml/1' in filelist
|
||||
assert 'workflows/1' not in filelist
|
||||
assert 'workflows_xml/1' in filelist
|
||||
assert 'models/export_to_model-1.upload' not in filelist
|
||||
|
@ -4086,6 +4093,11 @@ def test_settings_export_import(pub):
|
|||
resp = resp.form.submit('submit')
|
||||
assert 'Imported successfully' in resp.body
|
||||
assert '1 forms' in resp.body
|
||||
assert '1 cards' in resp.body
|
||||
assert FormDef.count() == 1
|
||||
assert FormDef.select()[0].url_name == 'foo'
|
||||
assert CardDef.count() == 1
|
||||
assert CardDef.select()[0].url_name == 'bar'
|
||||
|
||||
# check roles are found by name
|
||||
wipe()
|
||||
|
@ -4887,11 +4899,11 @@ def test_settings_permissions(pub):
|
|||
assert Role.get(role2.id).allows_backoffice_access is True
|
||||
|
||||
# give some roles access to the forms workshop (2nd checkbox) and to the
|
||||
# workflows workshop (3rd)
|
||||
# workflows workshop (4th)
|
||||
resp = app.get('/backoffice/settings/admin-permissions')
|
||||
resp.forms[0]['permissions$c-1-1'].checked = True
|
||||
resp.forms[0]['permissions$c-2-1'].checked = True
|
||||
resp.forms[0]['permissions$c-2-2'].checked = True
|
||||
resp.forms[0]['permissions$c-2-3'].checked = True
|
||||
resp = resp.forms[0].submit()
|
||||
pub.reload_cfg()
|
||||
assert set(pub.cfg['admin-permissions']['forms']) == set([role2.id, role3.id])
|
||||
|
@ -4901,7 +4913,7 @@ def test_settings_permissions(pub):
|
|||
resp = app.get('/backoffice/settings/admin-permissions')
|
||||
resp.forms[0]['permissions$c-1-1'].checked = False
|
||||
resp.forms[0]['permissions$c-2-1'].checked = False
|
||||
resp.forms[0]['permissions$c-2-2'].checked = False
|
||||
resp.forms[0]['permissions$c-2-3'].checked = False
|
||||
resp = resp.forms[0].submit()
|
||||
pub.reload_cfg()
|
||||
assert pub.cfg['admin-permissions']['forms'] == []
|
||||
|
@ -5035,3 +5047,51 @@ def test_postgresql_settings(pub):
|
|||
assert resp.form['port'].value == '5432'
|
||||
resp = resp.form.submit()
|
||||
assert pub.cfg['postgresql']['port'] == 5432
|
||||
|
||||
def test_studio_home(pub, studio):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/')
|
||||
assert 'studio' in resp.body
|
||||
resp = app.get('/backoffice/studio/')
|
||||
assert '../forms/' in resp.body
|
||||
assert '../cards/' in resp.body
|
||||
assert '../workflows/' in resp.body
|
||||
|
||||
pub.cfg['admin-permissions'] = {}
|
||||
for part in ('forms', 'cards', 'workflows'):
|
||||
# check section link are not displayed if user has no access right
|
||||
pub.cfg['admin-permissions'].update({part: ['x']}) # block access
|
||||
pub.write_cfg()
|
||||
if part != 'workflows':
|
||||
resp = app.get('/backoffice/studio/')
|
||||
assert '../%s/' % part not in resp.body
|
||||
else:
|
||||
resp = app.get('/backoffice/studio/', status=403) # totally closed
|
||||
|
||||
resp = app.get('/backoffice/')
|
||||
assert 'studio' not in resp.body
|
||||
|
||||
def test_studio_workflows(pub, studio):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/workflows/')
|
||||
assert '<h2>Workflows for cards</h2>' in resp.body
|
||||
resp = resp.click(r'Default \(cards\)')
|
||||
assert 'status/recorded/' in resp.body
|
||||
assert 'status/deleted/' in resp.body
|
||||
assert 'This is the default workflow,' in resp.body
|
||||
|
||||
def test_cards_new(pub, studio):
|
||||
CardDef.wipe()
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/cards/')
|
||||
resp = resp.click('New Card')
|
||||
resp.form['name'] = 'card title'
|
||||
resp = resp.form.submit()
|
||||
assert resp.location == 'http://example.net/backoffice/cards/1/'
|
||||
resp = resp.follow()
|
||||
assert '<h2>card title' in resp.body
|
||||
assert CardDef.get(1).workflow_id is None
|
||||
assert CardDef.get(1).disabled is False
|
||||
|
|
|
@ -22,6 +22,7 @@ from wcs.qommon.http_request import HTTPRequest
|
|||
from wcs.qommon.form import PicklableUpload
|
||||
from wcs.users import User
|
||||
from wcs.roles import Role
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.formdata import Evolution
|
||||
from wcs.categories import Category
|
||||
|
@ -2633,3 +2634,32 @@ def test_geocoding(pub):
|
|||
pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w'))
|
||||
resp = get_app(pub).get('/api/geocoding?q=test')
|
||||
assert urlopen.call_args[0][0] == 'http://reverse.example.net/?param=value&format=json&q=test&accept-language=en'
|
||||
|
||||
def test_cards(pub, local_user):
|
||||
Role.wipe()
|
||||
role = Role(name='test')
|
||||
role.store()
|
||||
local_user.roles = [role.id]
|
||||
local_user.store()
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'test'
|
||||
carddef.fields = [fields.StringField(id='0', label='foobar', varname='foo')]
|
||||
carddef.workflow_roles = {'_viewer': role.id}
|
||||
carddef.store()
|
||||
|
||||
formdata = carddef.data_class()()
|
||||
formdata.data = {'0': 'blah'}
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
resp = get_app(pub).get(sign_uri('/api/cards/test/list'), status=403)
|
||||
|
||||
resp = get_app(pub).get(sign_uri(
|
||||
'/api/cards/test/list?NameID=%s' %
|
||||
local_user.name_identifiers[0]))
|
||||
assert len(resp.json) == 1
|
||||
resp = get_app(pub).get(sign_uri(
|
||||
'/api/cards/test/list?NameID=%s&full=on' %
|
||||
local_user.name_identifiers[0]))
|
||||
assert resp.json[0]['fields']['foo'] == 'blah'
|
||||
|
|
|
@ -41,6 +41,7 @@ from wcs.wf.wscall import WebserviceCallStatusItem
|
|||
from wcs.wf.redirect_to_url import RedirectToUrlWorkflowStatusItem
|
||||
from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem
|
||||
from wcs.wf.resubmit import ResubmitWorkflowStatusItem
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.categories import Category
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.logged_errors import LoggedError
|
||||
|
@ -102,8 +103,7 @@ def create_user(pub, is_admin=False):
|
|||
|
||||
return user1
|
||||
|
||||
def create_superuser(pub):
|
||||
return create_user(pub, is_admin=True)
|
||||
def create_superuser(pub): return create_user(pub, is_admin=True)
|
||||
|
||||
def create_environment(pub, set_receiver=True):
|
||||
pub.session_manager.session_class.wipe()
|
||||
|
@ -4034,12 +4034,12 @@ def test_inspect_page(pub, local_user):
|
|||
local_user.store()
|
||||
|
||||
resp = login(get_app(pub)).get(formdata.get_url(backoffice=True), status=200)
|
||||
assert 'Form Inspector' not in resp.body
|
||||
assert 'Data Inspector' not in resp.body
|
||||
resp = login(get_app(pub)).get('%sinspect' % formdata.get_url(backoffice=True), status=403)
|
||||
|
||||
create_user(pub, is_admin=True)
|
||||
resp = login(get_app(pub)).get(formdata.get_url(backoffice=True), status=200)
|
||||
resp = resp.click('Form Inspector')
|
||||
resp = resp.click('Data Inspector')
|
||||
|
||||
assert '0' * 1000 in resp.body
|
||||
assert len(resp.body) < 100000
|
||||
|
@ -4817,3 +4817,52 @@ def test_workflow_comment_required(pub):
|
|||
resp = resp.follow()
|
||||
assert 'widget-with-error' not in resp.body
|
||||
assert 'HELLO WORLD 2' in resp.body
|
||||
|
||||
def test_carddata_management(pub, studio):
|
||||
user = create_user(pub)
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/')
|
||||
assert not 'Cards Data' in resp.body
|
||||
carddef = CardDef()
|
||||
carddef.name = 'foo'
|
||||
carddef.fields = [
|
||||
fields.StringField(id='1', label='Test', type='string', varname='foo'),
|
||||
]
|
||||
carddef.store()
|
||||
|
||||
resp = app.get('/backoffice/')
|
||||
assert not 'Cards Data' in resp.body
|
||||
|
||||
carddef.backoffice_submission_roles = user.roles
|
||||
carddef.store()
|
||||
resp = app.get('/backoffice/')
|
||||
assert 'Cards Data' in resp.body
|
||||
|
||||
carddef.backoffice_submission_roles = None
|
||||
carddef.workflow_roles = {'_editor': user.roles[0]}
|
||||
carddef.store()
|
||||
resp = app.get('/backoffice/')
|
||||
assert 'Cards Data' in resp.body
|
||||
|
||||
resp = app.get('/backoffice/data/')
|
||||
resp = resp.click('foo')
|
||||
assert 'Add' not in resp.body
|
||||
|
||||
carddef.backoffice_submission_roles = user.roles
|
||||
carddef.store()
|
||||
|
||||
resp = app.get('/backoffice/data/')
|
||||
resp = resp.click('foo')
|
||||
assert resp.body.count('<tr') == 1 # header
|
||||
assert 'Add' in resp.body
|
||||
resp = resp.click('Add')
|
||||
resp.form['f1'] = 'blah'
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.location.endswith('/backoffice/data/foo/1/')
|
||||
resp = resp.follow()
|
||||
assert 'Edit Card' in resp.body
|
||||
assert 'Delete Card' in resp.body
|
||||
|
||||
resp = app.get('/backoffice/data/')
|
||||
resp = resp.click('foo')
|
||||
assert resp.body.count('<tr') == 2 # header + row of data
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import pytest
|
||||
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.fields import StringField
|
||||
|
||||
from utilities import create_temporary_pub, clean_temporary_pub
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
if 'pub' in metafunc.fixturenames:
|
||||
metafunc.parametrize('pub', ['pickle', 'sql'], indirect=True)
|
||||
|
||||
@pytest.fixture
|
||||
def pub(request):
|
||||
pub = create_temporary_pub(sql_mode=(request.param == 'sql'))
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
pub.set_app_dir(req)
|
||||
pub.cfg['language'] = {'language': 'en'}
|
||||
pub.write_cfg()
|
||||
return pub
|
||||
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_basics(pub):
|
||||
carddef = CardDef()
|
||||
carddef.name = 'foo'
|
||||
carddef.fields = [
|
||||
StringField(id='1', label='Test', type='string', varname='foo'),
|
||||
]
|
||||
carddef.store()
|
||||
assert CardDef.get(carddef.id).name == 'foo'
|
||||
|
||||
carddata_class = carddef.data_class()
|
||||
carddata = carddata_class()
|
||||
carddata.data = {'1': 'hello world'}
|
||||
carddata.just_created()
|
||||
carddata.store()
|
||||
assert carddata.status == 'wf-recorded'
|
||||
|
||||
assert carddata_class.get(carddata.id).data['1'] == 'hello world'
|
||||
assert carddata_class.get(carddata.id).status == 'wf-recorded'
|
||||
|
||||
def test_xml_export_import(pub):
|
||||
carddef = CardDef()
|
||||
carddef.name = 'foo'
|
||||
carddef.fields = [
|
||||
StringField(id='1', label='Test', type='string', varname='foo'),
|
||||
]
|
||||
carddef.store()
|
||||
carddef_xml = carddef.export_to_xml()
|
||||
assert carddef_xml.tag == 'carddef'
|
||||
|
||||
carddef2 = CardDef.import_from_xml_tree(carddef_xml)
|
||||
assert carddef2.name == 'foo'
|
|
@ -59,18 +59,23 @@ def get_workflows(condition=lambda x: True):
|
|||
|
||||
|
||||
class FormDefUI(object):
|
||||
formdef_class = FormDef
|
||||
|
||||
def __init__(self, formdef):
|
||||
self.formdef = formdef
|
||||
|
||||
def get_categories(self):
|
||||
return get_categories()
|
||||
|
||||
def new_form_ui(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
if self.formdef:
|
||||
formdef = self.formdef
|
||||
else:
|
||||
formdef = FormDef()
|
||||
form.add(StringWidget, 'name', title = _('Form Title'), required=True, size=40,
|
||||
formdef = self.formdef_class()
|
||||
form.add(StringWidget, 'name', title = _('Name'), required=True, size=40,
|
||||
value = formdef.name)
|
||||
categories = get_categories()
|
||||
categories = self.get_categories()
|
||||
if categories:
|
||||
form.add(SingleSelectWidget, 'category_id', title = _('Category'),
|
||||
value = formdef.category_id,
|
||||
|
@ -88,10 +93,10 @@ class FormDefUI(object):
|
|||
if self.formdef:
|
||||
formdef = self.formdef
|
||||
else:
|
||||
formdef = FormDef()
|
||||
formdef = self.formdef_class()
|
||||
|
||||
name = form.get_widget('name').parse()
|
||||
formdefs_name = [x.name for x in FormDef.select(ignore_errors=True,
|
||||
formdefs_name = [x.name for x in self.formdef_class.select(ignore_errors=True,
|
||||
lightweight=True) if x.id != formdef.id]
|
||||
if name in formdefs_name:
|
||||
form.get_widget('name').set_error(_('This name is already used'))
|
||||
|
@ -349,12 +354,22 @@ class FormDefPage(Directory):
|
|||
('backoffice-submission-roles', 'backoffice_submission_roles'),
|
||||
('logged-errors', 'logged_errors_dir'),]
|
||||
|
||||
formdef_class = FormDef
|
||||
formdef_export_prefix = 'form'
|
||||
formdef_ui_class = FormDefUI
|
||||
|
||||
delete_message = N_('You are about to irrevocably delete this form.')
|
||||
delete_title = N_('Deleting Form:')
|
||||
overwrite_message = N_(
|
||||
'You can replace this form by uploading a file '
|
||||
'or by pointing to a form URL.')
|
||||
|
||||
def __init__(self, component):
|
||||
try:
|
||||
self.formdef = FormDef.get(component)
|
||||
self.formdef = self.formdef_class.get(component)
|
||||
except KeyError:
|
||||
raise TraversalError()
|
||||
self.formdefui = FormDefUI(self.formdef)
|
||||
self.formdefui = self.formdef_ui_class(self.formdef)
|
||||
get_response().breadcrumb.append((component + '/', self.formdef.name))
|
||||
self.fields = FieldsDirectory(self.formdef)
|
||||
self.fields.html_top = self.html_top
|
||||
|
@ -727,7 +742,7 @@ class FormDefPage(Directory):
|
|||
if self.formdef.url_name == misc.simplify(self.formdef.name):
|
||||
# if name and url name are in sync, keep them that way
|
||||
kwargs['data-slug-sync'] = 'url_name'
|
||||
form.add(StringWidget, 'name', title=_('Form Title'), required=True,
|
||||
form.add(StringWidget, 'name', title=_('Name'), required=True,
|
||||
size=40, value=self.formdef.name, **kwargs)
|
||||
|
||||
disabled_url_name = bool(self.formdef.data_class().count())
|
||||
|
@ -745,7 +760,7 @@ class FormDefPage(Directory):
|
|||
if form.is_submitted() and not form.has_errors():
|
||||
new_name = form.get_widget('name').parse()
|
||||
new_url_name = form.get_widget('url_name').parse()
|
||||
formdefs = [x for x in FormDef.select(ignore_errors=True,
|
||||
formdefs = [x for x in self.formdef_class.select(ignore_errors=True,
|
||||
lightweight=True) if x.id != self.formdef.id]
|
||||
if new_name in [x.name for x in formdefs]:
|
||||
form.get_widget('name').set_error(_('This name is already used.'))
|
||||
|
@ -889,7 +904,7 @@ class FormDefPage(Directory):
|
|||
self.formdefui.formdef.id = None
|
||||
original_name = self.formdefui.formdef.name
|
||||
self.formdefui.formdef.name = self.formdefui.formdef.name + _(' (copy)')
|
||||
formdef_names = [x.name for x in FormDef.select(lightweight=True)]
|
||||
formdef_names = [x.name for x in self.formdef_class.select(lightweight=True)]
|
||||
no = 2
|
||||
while self.formdefui.formdef.name in formdef_names:
|
||||
self.formdefui.formdef.name = _('%(name)s (copy %(no)d)') % {
|
||||
|
@ -903,17 +918,16 @@ class FormDefPage(Directory):
|
|||
|
||||
def delete(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
form.widgets.append(HtmlWidget('<p>%s</p>' % _(
|
||||
'You are about to irrevocably delete this form.')))
|
||||
form.widgets.append(HtmlWidget('<p>%s</p>' % _(self.delete_message)))
|
||||
form.add_submit('delete', _('Delete'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
if form.get_widget('cancel').parse():
|
||||
return redirect('..')
|
||||
if not form.is_submitted() or form.has_errors():
|
||||
get_response().breadcrumb.append(('delete', _('Delete')))
|
||||
self.html_top(title = _('Delete Form'))
|
||||
self.html_top(title=_(self.delete_title))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s %s</h2>') % (_('Deleting Form:'), self.formdef.name)
|
||||
r += htmltext('<h2>%s %s</h2>') % (_(self.delete_title), self.formdef.name)
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
else:
|
||||
|
@ -944,9 +958,7 @@ class FormDefPage(Directory):
|
|||
self.html_top(title = _('Overwrite'))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Overwrite')
|
||||
r += htmltext('<p>%s</p>') % _(
|
||||
'You can replace this new form by uploading a file '\
|
||||
'or by pointing to a form URL.')
|
||||
r += htmltext('<p>%s</p>') % _(self.overwrite_message)
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
|
@ -968,7 +980,7 @@ class FormDefPage(Directory):
|
|||
|
||||
error, reason = False, None
|
||||
try:
|
||||
new_formdef = FormDef.import_from_xml(fp, include_id=True)
|
||||
new_formdef = self.formdef_class.import_from_xml(fp, include_id=True)
|
||||
except FormdefImportError as e:
|
||||
error = True
|
||||
reason = _(e.msg)
|
||||
|
@ -1145,7 +1157,7 @@ class FormDefPage(Directory):
|
|||
response = get_response()
|
||||
response.set_content_type('application/x-wcs-form')
|
||||
response.set_header('content-disposition',
|
||||
'attachment; filename=form-%s.wcs' % self.formdef.url_name)
|
||||
'attachment; filename=%s-%s.wcs' % (self.formdef_export_prefix, self.formdef.url_name))
|
||||
return '<?xml version="1.0" encoding="iso-8859-15"?>\n' + ET.tostring(x)
|
||||
|
||||
def archive(self):
|
||||
|
@ -1457,7 +1469,23 @@ class FormsDirectory(AccessControlled, Directory):
|
|||
|
||||
categories = CategoriesDirectory()
|
||||
data_sources = NamedDataSourcesDirectoryInForms()
|
||||
formdef_class = FormDef
|
||||
formdef_page_class = FormDefPage
|
||||
formdef_ui_class = FormDefUI
|
||||
|
||||
import_title = N_('Import Form')
|
||||
import_submit_label = N_('Import Form')
|
||||
import_paragraph = N_(
|
||||
'You can install a new form by uploading a file '
|
||||
'or by pointing to the form URL.')
|
||||
import_loading_error_message = N_('Error loading form (%s).')
|
||||
import_success_message = N_(
|
||||
'This form has been successfully imported. '
|
||||
'Do note it is disabled by default.')
|
||||
import_error_message = N_(
|
||||
'Imported form contained errors and has been automatically fixed, '
|
||||
'you should nevertheless check everything is ok. '
|
||||
'Do note it is disabled by default.')
|
||||
|
||||
def html_top(self, title):
|
||||
return html_top('forms', title)
|
||||
|
@ -1487,7 +1515,7 @@ class FormsDirectory(AccessControlled, Directory):
|
|||
cats = Category.select()
|
||||
Category.sort_by_position(cats)
|
||||
one = False
|
||||
formdefs = FormDef.select(order_by='name', ignore_errors=True, lightweight=True)
|
||||
formdefs = self.formdef_class.select(order_by='name', ignore_errors=True, lightweight=True)
|
||||
for c in cats:
|
||||
l2 = [x for x in formdefs if str(x.category_id) == str(c.id)]
|
||||
l2 = [x for x in l2 if not x.disabled or (x.disabled and x.disabled_redirection)] \
|
||||
|
@ -1532,7 +1560,7 @@ class FormsDirectory(AccessControlled, Directory):
|
|||
get_response().breadcrumb.append( ('new', _('New')) )
|
||||
if Role.count() == 0:
|
||||
return template.error_page('forms', _('You first have to define roles.'))
|
||||
formdefui = FormDefUI(None)
|
||||
formdefui = self.formdef_ui_class(None)
|
||||
form = formdefui.new_form_ui()
|
||||
if form.get_widget('cancel').parse():
|
||||
return redirect('.')
|
||||
|
@ -1563,7 +1591,7 @@ class FormsDirectory(AccessControlled, Directory):
|
|||
form.add(FileWidget, 'file', title=_('File'), required=False)
|
||||
form.add(UrlWidget, 'url', title=_('Address'), required=False,
|
||||
size=50)
|
||||
form.add_submit('submit', _('Import Form'))
|
||||
form.add_submit('submit', _(self.import_submit_label))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
|
||||
if form.get_submit() == 'cancel':
|
||||
|
@ -1577,16 +1605,15 @@ class FormsDirectory(AccessControlled, Directory):
|
|||
|
||||
get_response().breadcrumb.append( ('forms/', _('Forms')) )
|
||||
get_response().breadcrumb.append( ('import', _('Import')) )
|
||||
self.html_top(title = _('Import Form'))
|
||||
self.html_top(title=_(self.import_title))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Import Form')
|
||||
r += htmltext('<p>%s</p>') % _(
|
||||
'You can install a new form by uploading a file '\
|
||||
'or by pointing to the form URL.')
|
||||
r += htmltext('<h2>%s</h2>') % _(self.import_title)
|
||||
r += htmltext('<p>%s</p>') % _(self.import_paragraph)
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
def import_submit(self, form):
|
||||
self.imported_formdef = None
|
||||
if form.get_widget('file').parse():
|
||||
fp = form.get_widget('file').parse().fp
|
||||
elif form.get_widget('url').parse():
|
||||
|
@ -1594,7 +1621,7 @@ class FormsDirectory(AccessControlled, Directory):
|
|||
try:
|
||||
fp = misc.urlopen(url)
|
||||
except misc.ConnectionError as e:
|
||||
form.set_error('url', _('Error loading form (%s).') % str(e))
|
||||
form.set_error('url', _(self.import_loading_error_message) % str(e))
|
||||
raise ValueError()
|
||||
else:
|
||||
form.set_error('file', _('You have to enter a file or a URL.'))
|
||||
|
@ -1603,17 +1630,12 @@ class FormsDirectory(AccessControlled, Directory):
|
|||
error, reason = False, None
|
||||
try:
|
||||
try:
|
||||
formdef = FormDef.import_from_xml(fp)
|
||||
get_session().message = ('info',
|
||||
_('This form has been successfully imported. '
|
||||
'Do note it is disabled by default.'))
|
||||
formdef = self.formdef_class.import_from_xml(fp)
|
||||
get_session().message = ('info', _(self.import_success_message))
|
||||
except FormdefImportRecoverableError as e:
|
||||
fp.seek(0)
|
||||
formdef = FormDef.import_from_xml(fp, fix_on_error=True)
|
||||
get_session().message = ('info',
|
||||
_('Imported form contained errors and has been automatically fixed, '
|
||||
'you should nevertheless check everything is ok. '
|
||||
'Do note it is disabled by default.'))
|
||||
formdef = self.formdef_class.import_from_xml(fp, fix_on_error=True)
|
||||
get_session().message = ('info', _(self.import_error_message))
|
||||
except FormdefImportError as e:
|
||||
error = True
|
||||
reason = _(e.msg)
|
||||
|
@ -1633,6 +1655,7 @@ class FormsDirectory(AccessControlled, Directory):
|
|||
form.set_error('file', msg)
|
||||
raise ValueError()
|
||||
|
||||
self.imported_formdef = formdef
|
||||
formdef.internal_identifier = None # a new one will be set in .store()
|
||||
formdef.disabled = True
|
||||
formdef.store()
|
||||
|
|
|
@ -19,16 +19,6 @@ from quixote.directory import Directory
|
|||
|
||||
|
||||
class RootDirectory(Directory):
|
||||
menu_items = [ # still used for access control (permissions panel)
|
||||
('forms/', N_('Forms')),
|
||||
('workflows/', N_('Workflows')),
|
||||
('users/', N_('Users')),
|
||||
('roles/', N_('Roles')),
|
||||
('categories/', N_('Categories')),
|
||||
('bounces/', N_('Bounces')),
|
||||
('settings/', N_('Settings')),
|
||||
('/', N_('WCS Form Server'))]
|
||||
|
||||
def _q_traverse(self, path):
|
||||
url = get_request().get_path_query()
|
||||
url = url.replace('/admin/', '/backoffice/', 1)
|
||||
|
|
|
@ -53,9 +53,11 @@ import qommon.ident
|
|||
import qommon.template
|
||||
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.workflows import Workflow
|
||||
from wcs.roles import Role
|
||||
|
||||
from wcs.backoffice.studio import StudioDirectory
|
||||
from .fields import FieldDefPage, FieldsDirectory
|
||||
from .data_sources import NamedDataSourcesDirectory
|
||||
from .wscalls import NamedWsCallsDirectory
|
||||
|
@ -549,11 +551,18 @@ class SettingsDirectory(QommonSettingsDirectory):
|
|||
permissions = [_('Backoffice')]
|
||||
|
||||
permission_keys = []
|
||||
for k, v in get_publisher().get_admin_root().menu_items:
|
||||
if not k.endswith(str('/')):
|
||||
continue
|
||||
k = k.strip(str('/'))
|
||||
if not k:
|
||||
admin_sections = [
|
||||
('forms', N_('Forms')),
|
||||
('carddefs', N_('Cards')),
|
||||
('workflows', N_('Workflows')),
|
||||
('users', N_('Users')),
|
||||
('roles', N_('Roles')),
|
||||
('categories', N_('Categories')),
|
||||
('bounces', N_('Bounces')),
|
||||
('settings', N_('Settings')),
|
||||
]
|
||||
for k, v in admin_sections:
|
||||
if k == 'carddef' and not StudioDirectory.is_visible():
|
||||
continue
|
||||
permissions.append(_(v))
|
||||
permission_keys.append(k)
|
||||
|
@ -847,6 +856,8 @@ class SettingsDirectory(QommonSettingsDirectory):
|
|||
|
||||
form = Form(enctype="multipart/form-data")
|
||||
form.add(CheckboxWidget, 'formdefs', title = _('Forms'), value = True)
|
||||
if StudioDirectory.is_visible():
|
||||
form.add(CheckboxWidget, 'carddefs', title=_('Cards'), value=True)
|
||||
form.add(CheckboxWidget, 'workflows', title = _('Workflows'), value = True)
|
||||
if not get_cfg('sp', {}).get('idp-manage-roles'):
|
||||
form.add(CheckboxWidget, 'roles', title = _('Roles'), value = True)
|
||||
|
@ -892,6 +903,12 @@ class SettingsDirectory(QommonSettingsDirectory):
|
|||
misc.indent_xml(node)
|
||||
z.writestr(os.path.join('formdefs_xml', str(formdef.id)),
|
||||
'<?xml version="1.0" encoding="iso-8859-15"?>\n' + ET.tostring(node))
|
||||
if 'carddefs' in self.dirs:
|
||||
for formdef in CardDef.select():
|
||||
node = formdef.export_to_xml(include_id=True)
|
||||
misc.indent_xml(node)
|
||||
z.writestr(os.path.join('carddefs_xml', str(formdef.id)),
|
||||
'<?xml version="1.0" encoding="iso-8859-15"?>\n' + ET.tostring(node))
|
||||
if 'workflows' in self.dirs:
|
||||
for workflow in Workflow.select():
|
||||
node = workflow.export_to_xml(include_id=True)
|
||||
|
@ -913,7 +930,7 @@ class SettingsDirectory(QommonSettingsDirectory):
|
|||
job.store()
|
||||
|
||||
dirs = []
|
||||
for w in ('formdefs', 'workflows', 'roles', 'categories',
|
||||
for w in ('formdefs', 'carddefs', 'workflows', 'roles', 'categories',
|
||||
'datasources', 'wscalls'):
|
||||
if form.get_widget(w) and form.get_widget(w).parse():
|
||||
dirs.append(w)
|
||||
|
@ -991,6 +1008,8 @@ class SettingsDirectory(QommonSettingsDirectory):
|
|||
r += htmltext('<ul>')
|
||||
if results['formdefs']:
|
||||
r += htmltext('<li>%d %s</li>') % (results['formdefs'], _('forms'))
|
||||
if results['carddefs']:
|
||||
r += htmltext('<li>%d %s</li>') % (results['carddefs'], _('cards'))
|
||||
if results['workflows']:
|
||||
r += htmltext('<li>%d %s</li>') % (results['workflows'], _('workflows'))
|
||||
if results['roles']:
|
||||
|
|
|
@ -36,11 +36,13 @@ from qommon.admin.menu import command_icon
|
|||
from qommon import get_logger
|
||||
|
||||
from wcs.workflows import *
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.formdata import Evolution
|
||||
from .fields import FieldDefPage, FieldsDirectory
|
||||
from .data_sources import NamedDataSourcesDirectory
|
||||
from .logged_errors import LoggedErrorsDirectory
|
||||
from wcs.backoffice.studio import StudioDirectory
|
||||
|
||||
|
||||
def svg(tag):
|
||||
|
@ -1339,10 +1341,13 @@ class WorkflowPage(Directory):
|
|||
]
|
||||
|
||||
def __init__(self, component, html_top):
|
||||
try:
|
||||
self.workflow = Workflow.get(component)
|
||||
except KeyError:
|
||||
raise errors.TraversalError()
|
||||
if component == '_carddef_default':
|
||||
self.workflow = CardDef.get_default_workflow()
|
||||
else:
|
||||
try:
|
||||
self.workflow = Workflow.get(component)
|
||||
except KeyError:
|
||||
raise errors.TraversalError()
|
||||
self.html_top = html_top
|
||||
self.workflow_ui = WorkflowUI(self.workflow)
|
||||
self.status_dir = WorkflowStatusDirectory(self.workflow, html_top)
|
||||
|
@ -1804,30 +1809,51 @@ class WorkflowsDirectory(Directory):
|
|||
r += htmltext('<a class="new-item" rel="popup" href="new">%s</a>') % _('New Workflow')
|
||||
r += htmltext('</span>')
|
||||
r += htmltext('</div>')
|
||||
r += htmltext('<ul class="biglist">')
|
||||
workflows_in_use = set(['_default'])
|
||||
|
||||
formdef_workflows = [Workflow.get_default_workflow()]
|
||||
workflows_in_formdef_use = set(formdef_workflows[0].id)
|
||||
for formdef in FormDef.select(lightweight=True):
|
||||
workflows_in_use.add(str(formdef.workflow_id))
|
||||
workflows = [Workflow.get_default_workflow()] + Workflow.select(order_by='name')
|
||||
has_unused_workflows = False
|
||||
for workflow in workflows:
|
||||
if not str(workflow.id) in workflows_in_use:
|
||||
has_unused_workflows = True
|
||||
continue
|
||||
r += htmltext('<li>')
|
||||
r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (workflow.id, workflow.name)
|
||||
r += htmltext('</li>')
|
||||
r += htmltext('</ul>')
|
||||
if has_unused_workflows:
|
||||
r += htmltext('<h2>%s</h2>') % _('Unused workflows')
|
||||
r += htmltext('<ul class="biglist">')
|
||||
for workflow in workflows:
|
||||
if str(workflow.id) in workflows_in_use:
|
||||
continue
|
||||
r += htmltext('<li>')
|
||||
r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (workflow.id, workflow.name)
|
||||
r += htmltext('</li>')
|
||||
r += htmltext('</ul>')
|
||||
workflows_in_formdef_use.add(str(formdef.workflow_id))
|
||||
|
||||
if StudioDirectory.is_visible():
|
||||
carddef_workflows = [CardDef.get_default_workflow()]
|
||||
workflows_in_carddef_use = set(carddef_workflows[0].id)
|
||||
for carddef in CardDef.select(lightweight=True):
|
||||
workflows_in_carddef_use.add(str(carddef.workflow_id))
|
||||
form_workflow_title = _('Workflows for forms')
|
||||
else:
|
||||
carddef_workflows = []
|
||||
workflows_in_carddef_use = set()
|
||||
form_workflow_title = None
|
||||
|
||||
shared_workflows = []
|
||||
unused_workflows = []
|
||||
|
||||
for workflow in Workflow.select(order_by='name'):
|
||||
if (str(workflow.id) in workflows_in_formdef_use and str(workflow.id) in workflows_in_carddef_use):
|
||||
shared_workflows.append(workflow)
|
||||
elif str(workflow.id) in workflows_in_formdef_use:
|
||||
formdef_workflows.append(workflow)
|
||||
elif str(workflow.id) in workflows_in_carddef_use:
|
||||
carddef_workflows.append(workflow)
|
||||
else:
|
||||
unused_workflows.append(workflow)
|
||||
|
||||
def workflow_section(r, title, workflows):
|
||||
if workflows:
|
||||
if title:
|
||||
r += htmltext('<h2>%s</h2>') % title
|
||||
r += htmltext('<ul class="biglist">')
|
||||
for workflow in workflows:
|
||||
r += htmltext('<li>')
|
||||
r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (workflow.id, workflow.name)
|
||||
r += htmltext('</li>')
|
||||
r += htmltext('</ul>')
|
||||
|
||||
workflow_section(r, _('Workflows for both forms and cards'), shared_workflows)
|
||||
workflow_section(r, form_workflow_title, formdef_workflows)
|
||||
workflow_section(r, _('Workflows for cards'), carddef_workflows)
|
||||
workflow_section(r, _('Unused workflows'), unused_workflows)
|
||||
|
||||
return r.getvalue()
|
||||
|
||||
|
|
17
wcs/api.py
17
wcs/api.py
|
@ -35,6 +35,7 @@ from qommon.form import ComputedExpressionWidget, ConditionWidget
|
|||
from wcs.categories import Category
|
||||
from wcs.conditions import Condition, ValidationError
|
||||
from wcs.data_sources import NamedDataSource
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.roles import Role, logged_users_role
|
||||
from wcs.forms.common import FormStatusPage
|
||||
|
@ -165,10 +166,11 @@ class ApiFormdataPage(FormStatusPage):
|
|||
|
||||
class ApiFormPage(BackofficeFormPage):
|
||||
_q_exports = [('list', 'json'), 'geojson'] # restrict to API endpoints
|
||||
formdef_class = FormDef
|
||||
|
||||
def __init__(self, component):
|
||||
try:
|
||||
self.formdef = FormDef.get_by_urlname(component)
|
||||
self.formdef = self.formdef_class.get_by_urlname(component)
|
||||
except KeyError:
|
||||
raise TraversalError()
|
||||
|
||||
|
@ -209,6 +211,10 @@ class ApiFormPage(BackofficeFormPage):
|
|||
return super(ApiFormPage, self)._q_traverse(path)
|
||||
|
||||
|
||||
class ApiCardPage(ApiFormPage):
|
||||
formdef_class = CardDef
|
||||
|
||||
|
||||
class ApiFormsDirectory(Directory):
|
||||
_q_exports = ['', 'geojson']
|
||||
|
||||
|
@ -295,6 +301,11 @@ class ApiFormsDirectory(Directory):
|
|||
return ApiFormPage(component)
|
||||
|
||||
|
||||
class ApiCardsDirectory(Directory):
|
||||
def _q_lookup(self, component):
|
||||
return ApiCardPage(component)
|
||||
|
||||
|
||||
class ApiFormdefDirectory(Directory):
|
||||
_q_exports = ['schema', 'submit']
|
||||
|
||||
|
@ -796,8 +807,10 @@ class AutocompleteDirectory(Directory):
|
|||
|
||||
class ApiDirectory(Directory):
|
||||
_q_exports = ['forms', 'roles', ('reverse-geocoding', 'reverse_geocoding'),
|
||||
'formdefs', 'categories', 'user', 'users', 'code', 'autocomplete']
|
||||
'formdefs', 'categories', 'user', 'users', 'code', 'autocomplete',
|
||||
'cards']
|
||||
|
||||
cards = ApiCardsDirectory()
|
||||
forms = ApiFormsDirectory()
|
||||
formdefs = ApiFormdefsDirectory()
|
||||
categories = ApiCategoriesDirectory()
|
||||
|
|
|
@ -0,0 +1,264 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# w.c.s. - web application for online forms
|
||||
# Copyright (C) 2005-2019 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 time
|
||||
|
||||
from quixote import get_publisher, get_request, get_response, get_session, redirect
|
||||
from quixote.html import TemplateIO, htmltext
|
||||
|
||||
from qommon import _, misc
|
||||
from qommon.misc import C_
|
||||
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.roles import Role
|
||||
from wcs.workflows import Workflow
|
||||
from wcs.admin.forms import FormsDirectory, FormDefPage, FormDefUI, html_top
|
||||
from wcs.admin.logged_errors import LoggedErrorsDirectory
|
||||
|
||||
|
||||
class CardDefUI(FormDefUI):
|
||||
formdef_class = CardDef
|
||||
|
||||
def get_categories(self):
|
||||
return []
|
||||
|
||||
|
||||
class CardDefPage(FormDefPage):
|
||||
formdef_class = CardDef
|
||||
formdef_export_prefix = 'card'
|
||||
formdef_ui_class = CardDefUI
|
||||
|
||||
delete_message = N_('You are about to irrevocably delete this card.')
|
||||
delete_title = N_('Deleting Card:')
|
||||
overwrite_message = N_(
|
||||
'You can replace this card by uploading a file '
|
||||
'or by pointing to a form URL.')
|
||||
|
||||
def html_top(self, title):
|
||||
return html_top('cards', title)
|
||||
|
||||
def _q_index(self):
|
||||
self.html_top(title=self.formdef.name)
|
||||
r = TemplateIO(html=True)
|
||||
get_response().filter['sidebar'] = self.get_sidebar()
|
||||
get_response().add_javascript(['jquery.js', 'widget_list.js', 'qommon.wysiwyg.js'])
|
||||
|
||||
r += htmltext('<div id="appbar">')
|
||||
r += htmltext('<h2>%s</h2>') % self.formdef.name
|
||||
r += htmltext('<span class="actions">')
|
||||
r += htmltext('<a rel="popup" href="title">%s</a>') % _('change title')
|
||||
r += htmltext('</span>')
|
||||
r += htmltext('</div>')
|
||||
|
||||
if self.formdef.last_modification_time:
|
||||
warning_class = ''
|
||||
if (time.time() - time.mktime(self.formdef.last_modification_time)) < 600:
|
||||
if get_request().user and str(get_request().user.id) != self.formdef.last_modification_user_id:
|
||||
warning_class = 'recent'
|
||||
r += htmltext('<p class="last-modification %s">') % warning_class
|
||||
r += _('Last Modification:')
|
||||
r += ' '
|
||||
r += misc.localstrftime(self.formdef.last_modification_time)
|
||||
r += ' '
|
||||
if self.formdef.last_modification_user_id:
|
||||
try:
|
||||
r += _('by %s') % get_publisher().user_class.get(
|
||||
self.formdef.last_modification_user_id).display_name
|
||||
except KeyError:
|
||||
pass
|
||||
r += htmltext('</p>')
|
||||
|
||||
r += get_session().display_message()
|
||||
|
||||
def add_option_line(link, label, current_value):
|
||||
return htmltext(
|
||||
'<li><a rel="popup" href="%(link)s">'
|
||||
'<span class="label">%(label)s</span> '
|
||||
'<span class="value">%(current_value)s</span>'
|
||||
'</a></li>' % {
|
||||
'link': link,
|
||||
'label': label,
|
||||
'current_value': current_value})
|
||||
|
||||
r += htmltext('<div class="splitcontent-left">')
|
||||
r += htmltext('<div class="bo-block">')
|
||||
r += htmltext('<h3>%s</h3>') % _('Workflow')
|
||||
r += htmltext('<ul class="biglist optionslist">')
|
||||
|
||||
if get_publisher().get_backoffice_root().is_accessible('workflows'):
|
||||
# custom option line to also include a link to the workflow itself.
|
||||
r += htmltext(
|
||||
'<li><a rel="popup" href="%(link)s">'
|
||||
'<span class="label">%(label)s</span> '
|
||||
'<span class="value offset">%(current_value)s</span>'
|
||||
'</a>'
|
||||
'<a class="extra-link" title="%(title)s" href="../../workflows/%(workflow_id)s/">↗</a>'
|
||||
'</li>' % {
|
||||
'link': 'workflow',
|
||||
'label': _('Workflow'),
|
||||
'title': _('Open workflow page'),
|
||||
'workflow_id': self.formdef.workflow.id,
|
||||
'current_value': self.formdef.workflow.name or '-'})
|
||||
else:
|
||||
r += add_option_line('workflow', _('Workflow'),
|
||||
self.formdef.workflow and self.formdef.workflow.name or '-')
|
||||
|
||||
if self.formdef.workflow_id:
|
||||
pristine_workflow = Workflow.get(self.formdef.workflow_id)
|
||||
if pristine_workflow.variables_formdef:
|
||||
r += add_option_line('workflow-variables', _('Options'), '')
|
||||
|
||||
r += add_option_line('backoffice-submission-roles',
|
||||
_('Creation Role'),
|
||||
self._get_roles_label('backoffice_submission_roles'))
|
||||
|
||||
if self.formdef.workflow.roles:
|
||||
if not self.formdef.workflow_roles:
|
||||
self.formdef.workflow_roles = {}
|
||||
for (wf_role_id, wf_role_label) in self.formdef.workflow.roles.items():
|
||||
role_id = self.formdef.workflow_roles.get(wf_role_id)
|
||||
if role_id:
|
||||
try:
|
||||
role = Role.get(role_id)
|
||||
role_label = role.name
|
||||
except KeyError:
|
||||
# removed role ?
|
||||
role_label = _('Unknown role (%s)') % role_id
|
||||
else:
|
||||
role_label = '-'
|
||||
r += add_option_line('role/%s' % wf_role_id,
|
||||
wf_role_label, role_label)
|
||||
|
||||
r += htmltext('</ul>')
|
||||
r += htmltext('</div>')
|
||||
r += htmltext('</div>')
|
||||
|
||||
r += htmltext('<div class="splitcontent-right">')
|
||||
r += htmltext('<div class="bo-block">')
|
||||
r += htmltext('<h3>%s</h3>') % _('Options')
|
||||
r += htmltext('<ul class="biglist optionslist">')
|
||||
|
||||
r += add_option_line('options/geolocations',
|
||||
_('Geolocation'),
|
||||
self.formdef.geolocations and
|
||||
C_('geolocation|Enabled') or C_('geolocation|Disabled'))
|
||||
|
||||
if self.formdef.digest_template:
|
||||
digest_template_status = C_('template|Custom')
|
||||
else:
|
||||
digest_template_status = C_('template|None')
|
||||
r += add_option_line('options/templates',
|
||||
_('Digest Template'), digest_template_status)
|
||||
|
||||
r += htmltext('</ul>')
|
||||
r += htmltext('</div>')
|
||||
r += htmltext('</div>')
|
||||
|
||||
r += htmltext('<div class="bo-block clear">')
|
||||
r += htmltext('<h3 class="clear">%s <span class="change">(<a href="fields/">%s</a>)</span></h3>') % (
|
||||
_('Fields'), _('edit'))
|
||||
r += self.get_preview()
|
||||
r += htmltext('</div>')
|
||||
return r.getvalue()
|
||||
|
||||
def get_sidebar(self):
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<ul id="sidebar-actions">')
|
||||
r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
|
||||
r += htmltext('<li><a href="duplicate">%s</a></li>') % _('Duplicate')
|
||||
r += htmltext('<li><a rel="popup" href="overwrite">%s</a></li>') % _(
|
||||
'Overwrite with new import')
|
||||
r += htmltext('<li><a href="export">%s</a></li>') % _('Export')
|
||||
r += htmltext('</ul>')
|
||||
|
||||
r += LoggedErrorsDirectory.errors_block(formdef_id=self.formdef.id)
|
||||
return r.getvalue()
|
||||
|
||||
|
||||
class CardsDirectory(FormsDirectory):
|
||||
_q_exports = ['', 'new', ('import', 'p_import')]
|
||||
formdef_class = CardDef
|
||||
formdef_page_class = CardDefPage
|
||||
formdef_ui_class = CardDefUI
|
||||
|
||||
import_title = N_('Import Card')
|
||||
import_submit_label = N_('Import Card')
|
||||
import_paragraph = N_(
|
||||
'You can install a new card by uploading a file '
|
||||
'or by pointing to the card URL.')
|
||||
import_loading_error_message = N_('Error loading card (%s).')
|
||||
import_success_message = N_(
|
||||
'This card has been successfully imported. ')
|
||||
import_error_message = N_(
|
||||
'Imported card contained errors and has been automatically fixed, '
|
||||
'you should nevertheless check everything is ok. ')
|
||||
|
||||
def html_top(self, title):
|
||||
return html_top('cards', title)
|
||||
|
||||
def _q_index(self):
|
||||
get_response().breadcrumb.append(('cards/', _('Cards')))
|
||||
self.html_top(title=_('Cards'))
|
||||
r = TemplateIO(html=True)
|
||||
|
||||
r += htmltext('<div id="appbar">')
|
||||
r += htmltext('<h2>%s</h2>') % _('Cards')
|
||||
r += htmltext('<span class="actions">')
|
||||
r += htmltext('<a href="import" rel="popup">%s</a>') % _('Import')
|
||||
r += htmltext('<a class="new-item" href="new" rel="popup">%s</a>') % _('New Card')
|
||||
r += htmltext('</span>')
|
||||
r += htmltext('</div>')
|
||||
|
||||
formdefs = self.formdef_class.select(order_by='name', ignore_errors=True, lightweight=True)
|
||||
r += self.form_list(formdefs, title='')
|
||||
return r.getvalue()
|
||||
|
||||
def new(self):
|
||||
get_response().breadcrumb.append(('cards/', _('Cards')))
|
||||
get_response().breadcrumb.append(('new', _('New')))
|
||||
formdefui = self.formdef_ui_class(None)
|
||||
form = formdefui.new_form_ui()
|
||||
if form.get_widget('cancel').parse():
|
||||
return redirect('.')
|
||||
|
||||
if form.is_submitted() and not form.has_errors():
|
||||
try:
|
||||
formdef = formdefui.submit_form(form)
|
||||
formdef.disabled = False
|
||||
formdef.store()
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
return redirect(str(formdef.id) + '/')
|
||||
|
||||
self.html_top(title=_('New Card'))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('New Card')
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
def import_submit(self, form):
|
||||
response = super(CardsDirectory, self).import_submit(form)
|
||||
if self.imported_formdef:
|
||||
self.imported_formdef.disabled = False
|
||||
self.imported_formdef.store()
|
||||
return response
|
||||
|
||||
def _q_lookup(self, component):
|
||||
get_response().breadcrumb.append(('cards/', _('Cards')))
|
||||
return self.formdef_page_class(component)
|
|
@ -0,0 +1,140 @@
|
|||
# w.c.s. - web application for online forms
|
||||
# Copyright (C) 2005-2019 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/>.
|
||||
|
||||
from quixote import get_request, get_response, get_session, redirect
|
||||
from quixote.html import TemplateIO, htmltext, htmlescape
|
||||
|
||||
from qommon import _
|
||||
from qommon.backoffice.menu import html_top
|
||||
|
||||
from wcs.carddef import CardDef
|
||||
from .management import ManagementDirectory, FormPage, FormFillPage, FormBackOfficeStatusPage
|
||||
|
||||
|
||||
class DataManagementDirectory(ManagementDirectory):
|
||||
_q_exports = ['']
|
||||
|
||||
def _q_traverse(self, path):
|
||||
get_response().breadcrumb.append(('data/', _('Cards Data')))
|
||||
return super(ManagementDirectory, self)._q_traverse(path)
|
||||
|
||||
def is_accessible(self, user):
|
||||
if not user.can_go_in_backoffice():
|
||||
return False
|
||||
# only include data management if there are accessible cards
|
||||
for carddef in CardDef.select(ignore_errors=True, lightweight=True, iterator=True):
|
||||
for role_id in (user.roles or []):
|
||||
if role_id in (carddef.backoffice_submission_roles or []):
|
||||
return True
|
||||
if role_id in (carddef.workflow_roles or {}).values():
|
||||
return True
|
||||
return False
|
||||
|
||||
def _q_index(self):
|
||||
html_top('data_management', _('Data'))
|
||||
formdefs = CardDef.select(order_by='name', ignore_errors=True, lightweight=True)
|
||||
if len(formdefs) == 0:
|
||||
return self.empty_site_message(_('Cards'))
|
||||
r = TemplateIO(html=True)
|
||||
r += get_session().display_message()
|
||||
|
||||
r += htmltext('<div id="appbar">')
|
||||
r += htmltext('<h2>%s</h2>') % _('Cards')
|
||||
r += htmltext('</div>')
|
||||
|
||||
user = get_request().user
|
||||
if user:
|
||||
cards = []
|
||||
for formdef in formdefs:
|
||||
if user.is_admin or formdef.is_of_concern_for_user(user):
|
||||
cards.append(formdef)
|
||||
r += htmltext('<ul class="biglist">')
|
||||
for carddef in cards:
|
||||
r += htmltext('<li><strong><a href="%s/">%s</a></strong></li>') % (carddef.url_name, carddef.name)
|
||||
r += htmltext('</ul>')
|
||||
|
||||
return r.getvalue()
|
||||
|
||||
def _q_lookup(self, component):
|
||||
return CardPage(component)
|
||||
|
||||
|
||||
class CardPage(FormPage):
|
||||
_q_exports = ['', 'csv', 'xls', 'ods', 'json', 'export', 'map', 'geojson', 'add']
|
||||
|
||||
def __init__(self, component):
|
||||
try:
|
||||
self.formdef = CardDef.get_by_urlname(component)
|
||||
except KeyError:
|
||||
raise errors.TraversalError()
|
||||
self.add = CardFillPage(component)
|
||||
|
||||
def listing_top_actions(self):
|
||||
if not self.formdef.backoffice_submission_roles:
|
||||
return ''
|
||||
for role in get_request().user.roles or []:
|
||||
if role in self.formdef.backoffice_submission_roles:
|
||||
break
|
||||
else:
|
||||
return ''
|
||||
return htmltext('<span class="actions"><a href="./add/">%s</a></span>') % _('Add')
|
||||
|
||||
def get_default_filters(self, mode):
|
||||
return ()
|
||||
|
||||
def get_default_columns(self):
|
||||
field_ids = ['id', 'time']
|
||||
for field in self.formdef.get_all_fields():
|
||||
if hasattr(field, 'get_view_value') and field.in_listing:
|
||||
field_ids.append(field.id)
|
||||
return field_ids
|
||||
|
||||
def get_filter_from_query(self, default='waiting'):
|
||||
return 'all'
|
||||
|
||||
def _q_lookup(self, component):
|
||||
try:
|
||||
filled = self.formdef.data_class().get(component)
|
||||
except KeyError:
|
||||
raise errors.TraversalError()
|
||||
return CardBackOfficeStatusPage(self.formdef, filled)
|
||||
|
||||
|
||||
class CardFillPage(FormFillPage):
|
||||
formdef_class = CardDef
|
||||
|
||||
def submitted(self, form, *args):
|
||||
super(CardFillPage, self).submitted(form, *args)
|
||||
if get_response().get_header('location').endswith('/backoffice/submission/'):
|
||||
return redirect('..')
|
||||
|
||||
|
||||
class CardBackOfficeStatusPage(FormBackOfficeStatusPage):
|
||||
form_page_class = CardFillPage
|
||||
|
||||
sidebar_recorded_message = N_(
|
||||
'The card has been recorded on %(date)s with the number %(number)s.')
|
||||
sidebar_recorded_by_agent_message = N_(
|
||||
'The card has been recorded on %(date)s with the number %(number)s by %(agent)s.')
|
||||
|
||||
def html_top(self, title=None):
|
||||
return html_top('data_management', title)
|
||||
|
||||
def should_fold_summary(self, mine, request_user):
|
||||
return False
|
||||
|
||||
def should_fold_history(self):
|
||||
return True
|
|
@ -1034,10 +1034,20 @@ class FormPage(Directory):
|
|||
if self.formdef.geolocations:
|
||||
r += htmltext(' <li><a data-base-href="map" href="map%s">%s</a></li>') % (
|
||||
qs, _('Plot on a Map'))
|
||||
r += htmltext(' <li class="stats"><a href="stats">%s</a></li>') % _('Statistics')
|
||||
if 'stats' in self._q_exports:
|
||||
r += htmltext(' <li class="stats"><a href="stats">%s</a></li>') % _('Statistics')
|
||||
r += htmltext('</ul>')
|
||||
return r.getvalue()
|
||||
|
||||
def get_default_filters(self, mode):
|
||||
if mode == 'listing':
|
||||
# enable status filter by default
|
||||
return ('status',)
|
||||
if mode == 'stats':
|
||||
# enable period filters by default
|
||||
return ('start', 'end')
|
||||
return ()
|
||||
|
||||
def get_filter_sidebar(self, selected_filter=None, mode='listing'):
|
||||
r = TemplateIO(html=True)
|
||||
|
||||
|
@ -1046,6 +1056,7 @@ class FormPage(Directory):
|
|||
FakeField('start', 'period-date', _('Start')),
|
||||
FakeField('end', 'period-date', _('End')),
|
||||
]
|
||||
default_filters = self.get_default_filters(mode)
|
||||
filter_fields = []
|
||||
for field in period_fake_fields + self.get_formdef_fields():
|
||||
field.enabled = False
|
||||
|
@ -1058,12 +1069,7 @@ class FormPage(Directory):
|
|||
if get_request().form:
|
||||
field.enabled = 'filter-%s' % field.id in get_request().form
|
||||
else:
|
||||
if mode == 'listing':
|
||||
# enable status filter by default
|
||||
field.enabled = (field.id in ('status',))
|
||||
elif mode == 'stats':
|
||||
# enable period filters by default
|
||||
field.enabled = (field.id in ('start', 'end'))
|
||||
field.enabled = (field.id in default_filters)
|
||||
if field.type in ('item', 'items'):
|
||||
field.enabled = field.in_filters
|
||||
|
||||
|
@ -1223,14 +1229,18 @@ class FormPage(Directory):
|
|||
|
||||
return fields
|
||||
|
||||
def get_default_columns(self):
|
||||
field_ids = ['id', 'time', 'last_update_time', 'user-label']
|
||||
for field in self.formdef.get_all_fields():
|
||||
if hasattr(field, 'get_view_value') and field.in_listing:
|
||||
field_ids.append(field.id)
|
||||
field_ids.append('status')
|
||||
return field_ids
|
||||
|
||||
def get_fields_from_query(self, ignore_form=False):
|
||||
field_ids = [x for x in get_request().form.keys()]
|
||||
if not field_ids or ignore_form:
|
||||
field_ids = ['id', 'time', 'last_update_time', 'user-label']
|
||||
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')
|
||||
field_ids = self.get_default_columns()
|
||||
|
||||
fields = []
|
||||
for field in self.get_formdef_fields():
|
||||
|
@ -1317,6 +1327,9 @@ class FormPage(Directory):
|
|||
|
||||
return criterias
|
||||
|
||||
def listing_top_actions(self):
|
||||
return ''
|
||||
|
||||
def _q_index(self):
|
||||
self.check_access()
|
||||
get_logger().info('backoffice - form %s - listing' % self.formdef.name)
|
||||
|
@ -1355,7 +1368,10 @@ class FormPage(Directory):
|
|||
|
||||
html_top('management', '%s - %s' % (_('Listing'), self.formdef.name))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<div id="appbar">')
|
||||
r += htmltext('<h2>%s - %s</h2>') % (self.formdef.name, _('Listing'))
|
||||
r += self.listing_top_actions()
|
||||
r += htmltext('</div>')
|
||||
r += table
|
||||
|
||||
get_response().filter['sidebar'] = self.get_formdata_sidebar(qs) + \
|
||||
|
@ -1992,6 +2008,11 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
('user-pending-forms', 'user_pending_forms')]
|
||||
form_page_class = FormFillPage
|
||||
|
||||
sidebar_recorded_message = N_(
|
||||
'The form has been recorded on %(date)s with the number %(number)s.')
|
||||
sidebar_recorded_by_agent_message = N_(
|
||||
'The form has been recorded on %(date)s with the number %(number)s by %(agent)s.')
|
||||
|
||||
def html_top(self, title = None):
|
||||
return html_top('management', title)
|
||||
|
||||
|
@ -2071,11 +2092,11 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
formdata.submission_context['agent_id'], ignore_errors=True)
|
||||
|
||||
if agent_user:
|
||||
r += _('The form has been recorded on %(date)s with the number %(number)s by %(agent)s.') % {
|
||||
r += _(self.sidebar_recorded_by_agent_message) % {
|
||||
'date': tm, 'number': formdata.get_display_id(),
|
||||
'agent': agent_user.get_display_name()}
|
||||
else:
|
||||
r += _('The form has been recorded on %(date)s with the number %(number)s.') % {
|
||||
r += _(self.sidebar_recorded_message) % {
|
||||
'date': tm, 'number': formdata.get_display_id()}
|
||||
r += htmltext('</p>')
|
||||
try:
|
||||
|
@ -2166,7 +2187,7 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
(get_publisher().get_backoffice_root().is_accessible('forms') or
|
||||
get_publisher().get_backoffice_root().is_accessible('workflows'))):
|
||||
r += htmltext('<div class="extra-context">')
|
||||
r += htmltext('<p><a href="inspect">%s</a></p>') % _('Form Inspector')
|
||||
r += htmltext('<p><a href="inspect">%s</a></p>') % _('Data Inspector')
|
||||
r += htmltext('</div>')
|
||||
|
||||
return r.getvalue()
|
||||
|
@ -2356,15 +2377,20 @@ class FormBackOfficeStatusPage(FormStatusPage):
|
|||
get_publisher().get_backoffice_root().is_accessible('workflows')):
|
||||
raise errors.AccessForbiddenError()
|
||||
charset = get_publisher().site_charset
|
||||
get_response().breadcrumb.append(('inspect', _('Form Inspector')))
|
||||
get_response().breadcrumb.append(('inspect', _('Data Inspector')))
|
||||
self.html_top(self.formdef.name)
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<div id="appbar">')
|
||||
r += htmltext('<h2>%s</h2>') % _('Form Inspector')
|
||||
r += htmltext('<h2>%s</h2>') % _('Data Inspector')
|
||||
r += htmltext('<span class="actions">')
|
||||
if get_publisher().get_backoffice_root().is_accessible('forms'):
|
||||
r += htmltext(' <a href="../../../forms/%s/">%s</a>') % (
|
||||
self.formdef.id, _('View Form'))
|
||||
if self.formdef._names == 'formdefs':
|
||||
if get_publisher().get_backoffice_root().is_accessible('forms'):
|
||||
r += htmltext(' <a href="../../../forms/%s/">%s</a>') % (
|
||||
self.formdef.id, _('View Form'))
|
||||
elif self.formdef._names == 'carddefs':
|
||||
if get_publisher().get_backoffice_root().is_accessible('cards'):
|
||||
r += htmltext(' <a href="../../../cards/%s/">%s</a>') % (
|
||||
self.formdef.id, _('View Card'))
|
||||
if get_publisher().get_backoffice_root().is_accessible('workflows'):
|
||||
r += htmltext(' <a href="../../../workflows/%s/">%s</a>') % (
|
||||
self.formdef.workflow.id, _('View Workflow'))
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.utils.translation import pgettext
|
||||
|
||||
from quixote import get_publisher, get_request, get_response, redirect
|
||||
from quixote.html import TemplateIO, htmltext
|
||||
|
||||
|
@ -35,8 +37,11 @@ import wcs.admin.settings
|
|||
import wcs.admin.users
|
||||
import wcs.admin.workflows
|
||||
|
||||
from . import studio
|
||||
from . import cards
|
||||
from . import submission
|
||||
from . import management
|
||||
from . import data_management
|
||||
|
||||
|
||||
class RootDirectory(BackofficeRootDirectory):
|
||||
|
@ -49,13 +54,19 @@ class RootDirectory(BackofficeRootDirectory):
|
|||
users = wcs.admin.users.UsersDirectory()
|
||||
workflows = wcs.admin.workflows.WorkflowsDirectory()
|
||||
management = management.ManagementDirectory()
|
||||
studio = studio.StudioDirectory()
|
||||
cards = cards.CardsDirectory()
|
||||
data = data_management.DataManagementDirectory()
|
||||
submission = submission.SubmissionDirectory()
|
||||
|
||||
menu_items = [
|
||||
('submission/', N_('Submission')),
|
||||
('management/', N_('Management')),
|
||||
('forms/', N_('Forms Workshop')),
|
||||
('workflows/', N_('Workflows Workshop')),
|
||||
('data/', N_('Cards Data'), {'check_display_function': studio.is_visible}),
|
||||
('studio/', N_('Studio'), {'check_display_function': studio.is_visible}),
|
||||
('forms/', N_('Forms Workshop'), {'sub': True}),
|
||||
('cards/', N_('Cards'), {'sub': True, 'check_display_function': studio.is_visible}),
|
||||
('workflows/', N_('Workflows Workshop'), {'sub': True}),
|
||||
('users/', N_('Users'), {'check_display_function': roles.is_visible}),
|
||||
('roles/', N_('Roles'), {'check_display_function': roles.is_visible}),
|
||||
('bounces/', N_('Bounces'), {'check_display_function': bounces.is_visible}),
|
||||
|
@ -170,9 +181,12 @@ class RootDirectory(BackofficeRootDirectory):
|
|||
|
||||
menu_items = self.get_menu_items()
|
||||
r += htmltext('<ul class="apps">')
|
||||
has_studio = self.studio.is_visible()
|
||||
for menu_item in menu_items:
|
||||
if not 'icon' in menu_item:
|
||||
continue
|
||||
if menu_item['icon'] == 'studio':
|
||||
continue
|
||||
r += htmltext('<li class="zone-%(icon)s"><a href="%(url)s">%(label)s</a></li>') % menu_item
|
||||
for menu_item in menu_items:
|
||||
if 'icon' in menu_item:
|
||||
|
@ -238,6 +252,7 @@ class RootDirectory(BackofficeRootDirectory):
|
|||
backoffice_url = get_publisher().get_backoffice_url()
|
||||
if not backoffice_url.endswith('/'):
|
||||
backoffice_url += '/'
|
||||
has_studio = self.studio.is_visible()
|
||||
for item in self.menu_items:
|
||||
if len(item) == 2:
|
||||
item = list(item) + [{}]
|
||||
|
@ -254,11 +269,22 @@ class RootDirectory(BackofficeRootDirectory):
|
|||
label = v()
|
||||
else:
|
||||
label = _(v)
|
||||
if has_studio:
|
||||
if slug == 'forms':
|
||||
label = misc.site_encode(pgettext('studio', 'Forms'))
|
||||
elif slug == 'cards':
|
||||
label = misc.site_encode(pgettext('studio', 'Cards'))
|
||||
elif slug == 'workflows':
|
||||
label = misc.site_encode(pgettext('studio', 'Workflows'))
|
||||
menu_items.append({
|
||||
'label': label,
|
||||
'slug': slug,
|
||||
'url': backoffice_url + k})
|
||||
'url': backoffice_url + k,
|
||||
'sub': (options.get('sub') and has_studio) or False,
|
||||
})
|
||||
|
||||
if slug in ('home', 'forms', 'workflows', 'users', 'roles',
|
||||
'categories', 'settings', 'management', 'submission'):
|
||||
'categories', 'settings', 'management', 'submission',
|
||||
'studio', 'cards', 'data'):
|
||||
menu_items[-1]['icon'] = k.strip('/')
|
||||
return menu_items
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# w.c.s. - web application for online forms
|
||||
# Copyright (C) 2005-2019 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/>.
|
||||
|
||||
from quixote import get_publisher
|
||||
from quixote.directory import Directory
|
||||
from qommon import _
|
||||
from qommon.backoffice.menu import html_top
|
||||
from qommon import template
|
||||
|
||||
|
||||
class StudioDirectory(Directory):
|
||||
_q_exports = ['']
|
||||
|
||||
def _q_index(self):
|
||||
html_top('studio', _('Studio'))
|
||||
return template.QommonTemplateResponse(
|
||||
templates=['wcs/backoffice/studio.html'],
|
||||
context={})
|
||||
|
||||
@classmethod
|
||||
def is_visible(cls, *args):
|
||||
return get_publisher().has_site_option('studio')
|
||||
|
||||
def is_accessible(self, user):
|
||||
backoffice_root = get_publisher().get_backoffice_root()
|
||||
return (backoffice_root.is_accessible('forms') or
|
||||
backoffice_root.is_accessible('workflows') or
|
||||
backoffice_root.is_accessible('cards'))
|
|
@ -0,0 +1,31 @@
|
|||
# w.c.s. - web application for online forms
|
||||
# Copyright (C) 2005-2019 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/>.
|
||||
|
||||
from wcs.formdata import FormData
|
||||
|
||||
|
||||
class CardData(FormData):
|
||||
def get_formdef(self):
|
||||
if self._formdef:
|
||||
return self._formdef
|
||||
from carddef import CardDef
|
||||
type, id = self._names.split('-', 1)
|
||||
try:
|
||||
self._formdef = CardDef.get_by_urlname(id)
|
||||
except KeyError:
|
||||
self._formdef = None
|
||||
return self._formdef
|
||||
formdef = property(get_formdef)
|
|
@ -0,0 +1,122 @@
|
|||
# w.c.s. - web application for online forms
|
||||
# Copyright (C) 2005-2019 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 new
|
||||
import sys
|
||||
|
||||
from quixote import get_publisher
|
||||
from qommon import _
|
||||
|
||||
from wcs.carddata import CardData
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.workflows import Workflow
|
||||
|
||||
|
||||
class CardDef(FormDef):
|
||||
_names = 'carddefs'
|
||||
data_sql_prefix = 'carddata'
|
||||
pickle_module_name = 'carddef'
|
||||
xml_root_node = 'carddef'
|
||||
|
||||
confirmation = False
|
||||
|
||||
def data_class(self, mode=None):
|
||||
if not 'carddef' in sys.modules:
|
||||
sys.modules['carddef'] = sys.modules[__name__]
|
||||
if hasattr(sys.modules['carddef'], self.url_name.title()):
|
||||
data_class = getattr(sys.modules['carddef'], self.url_name.title())
|
||||
# only use existing data class if it has a reference to this actual
|
||||
# carddef
|
||||
if data_class._formdef is self:
|
||||
return data_class
|
||||
if (get_publisher().is_using_postgresql() and not mode == 'files') or mode == 'sql':
|
||||
import sql
|
||||
table_name = sql.get_formdef_table_name(self)
|
||||
cls = new.classobj(self.url_name.title(), (sql.SqlCardData,),
|
||||
{'_formdef': self,
|
||||
'_table_name': table_name})
|
||||
actions = sql.do_formdef_tables(self)
|
||||
else:
|
||||
cls = new.classobj(self.url_name.title(), (CardData,),
|
||||
{'_names': 'card-%s' % self.internal_identifier,
|
||||
'_formdef': self})
|
||||
actions = []
|
||||
setattr(sys.modules['carddef'], self.url_name.title(), cls)
|
||||
setattr(sys.modules['wcs.carddef'], self.url_name.title(), cls)
|
||||
|
||||
if actions:
|
||||
for action in actions:
|
||||
getattr(cls, action)()
|
||||
|
||||
return cls
|
||||
|
||||
@classmethod
|
||||
def get_sql_new_id(cls, id_start):
|
||||
import sql
|
||||
return sql.get_carddef_new_id(id_start=id_start)
|
||||
|
||||
@classmethod
|
||||
def wipe(cls):
|
||||
super(CardDef, cls).wipe()
|
||||
if get_publisher().is_using_postgresql():
|
||||
import sql
|
||||
sql.carddef_wipe()
|
||||
|
||||
@classmethod
|
||||
def get_default_workflow(cls):
|
||||
from wcs.workflows import EditableWorkflowStatusItem, ChoiceWorkflowStatusItem
|
||||
import wf.remove
|
||||
workflow = Workflow(name=_('Default (cards)'))
|
||||
workflow.id = '_carddef_default'
|
||||
workflow.roles = {
|
||||
'_viewer': _('Viewer'),
|
||||
'_editor': _('Editor'),
|
||||
}
|
||||
status = workflow.add_status(_('Recorded'), 'recorded')
|
||||
deleted_status = workflow.add_status(_('Deleted'), 'deleted')
|
||||
|
||||
editable = EditableWorkflowStatusItem()
|
||||
editable.id = '_editable'
|
||||
editable.by = ['_editor']
|
||||
editable.label = _('Edit Card')
|
||||
editable.status = status.id
|
||||
editable.parent = status
|
||||
status.items.append(editable)
|
||||
|
||||
action_delete = ChoiceWorkflowStatusItem()
|
||||
action_delete.id = '_action_delete'
|
||||
action_delete.by = ['_editor']
|
||||
action_delete.label = _('Delete Card')
|
||||
action_delete.status = deleted_status.id
|
||||
action_delete.require_confirmation = True
|
||||
action_delete.parent = status
|
||||
status.items.append(action_delete)
|
||||
|
||||
remove = wf.remove.RemoveWorkflowStatusItem()
|
||||
remove.id = '_remove'
|
||||
remove.parent = deleted_status
|
||||
deleted_status.items.append(remove)
|
||||
|
||||
return workflow
|
||||
|
||||
def get_url(self, backoffice=False):
|
||||
# always return backoffice URL
|
||||
base_url = get_publisher().get_backoffice_url() + '/data'
|
||||
return '%s/%s/' % (base_url, self.url_name)
|
||||
|
||||
def store(self):
|
||||
self.roles = self.backoffice_submission_roles
|
||||
return super(CardDef, self).store()
|
|
@ -302,8 +302,8 @@ class FormData(StorableObject):
|
|||
# particular object, as it is required by pickle (or it will raise
|
||||
# "Can't pickle %r: it's not the same object as %s.%s" if the class
|
||||
# object has been changed in the course of the request).
|
||||
setattr(sys.modules['formdef'], self._formdef.url_name.title(), self.__class__)
|
||||
setattr(sys.modules['wcs.formdef'], self._formdef.url_name.title(), self.__class__)
|
||||
setattr(sys.modules[self._formdef.pickle_module_name], self._formdef.url_name.title(), self.__class__)
|
||||
setattr(sys.modules['wcs.%s' % self._formdef.pickle_module_name], self._formdef.url_name.title(), self.__class__)
|
||||
has_id = (self.id is not None)
|
||||
if has_id:
|
||||
self.set_auto_fields()
|
||||
|
|
|
@ -71,6 +71,9 @@ class FormDef(StorableObject):
|
|||
_names = 'formdefs'
|
||||
_indexes = ['url_name']
|
||||
_hashed_indexes = ['backoffice_submission_roles']
|
||||
data_sql_prefix = 'formdata'
|
||||
pickle_module_name = 'formdef'
|
||||
xml_root_node = 'formdef'
|
||||
|
||||
name = None
|
||||
description = None
|
||||
|
@ -324,8 +327,7 @@ class FormDef(StorableObject):
|
|||
if id == 0:
|
||||
id = len(keys)+1
|
||||
if get_publisher().is_using_postgresql():
|
||||
import sql
|
||||
id = sql.get_formdef_new_id(id_start=id)
|
||||
id = cls.get_sql_new_id(id_start=id)
|
||||
if create:
|
||||
objects_dir = cls.get_objects_dir()
|
||||
object_filename = os.path.join(objects_dir, fix_key(id))
|
||||
|
@ -336,6 +338,11 @@ class FormDef(StorableObject):
|
|||
os.close(fd)
|
||||
return str(id)
|
||||
|
||||
@classmethod
|
||||
def get_sql_new_id(cls, id_start):
|
||||
import sql
|
||||
return sql.get_formdef_new_id(id_start=id_start)
|
||||
|
||||
@classmethod
|
||||
def wipe(cls):
|
||||
super(FormDef, cls).wipe()
|
||||
|
@ -407,7 +414,11 @@ class FormDef(StorableObject):
|
|||
self._workflow = self.get_workflow_with_options(workflow)
|
||||
return self._workflow
|
||||
else:
|
||||
return Workflow.get_default_workflow()
|
||||
return self.get_default_workflow()
|
||||
|
||||
def get_default_workflow(self):
|
||||
from wcs.workflows import Workflow
|
||||
return Workflow.get_default_workflow()
|
||||
|
||||
def get_workflow_with_options(self, workflow):
|
||||
# this needs to be kept in sync with admin/forms.ptl,
|
||||
|
@ -843,7 +854,7 @@ class FormDef(StorableObject):
|
|||
|
||||
def export_to_xml(self, include_id=False):
|
||||
charset = get_publisher().site_charset
|
||||
root = ET.Element('formdef')
|
||||
root = ET.Element(self.xml_root_node)
|
||||
if include_id and self.id:
|
||||
root.attrib['id'] = str(self.id)
|
||||
for text_attribute in list(self.TEXT_ATTRIBUTES):
|
||||
|
@ -1021,8 +1032,8 @@ class FormDef(StorableObject):
|
|||
if not ET.iselement(tree):
|
||||
tree = tree.getroot()
|
||||
|
||||
if tree.tag != 'formdef':
|
||||
raise FormdefImportError(N_('Not a form'))
|
||||
if tree.tag != cls.xml_root_node:
|
||||
raise FormdefImportError(N_('Unexpected root node'))
|
||||
|
||||
if include_id and tree.attrib.get('id'):
|
||||
formdef.id = tree.attrib.get('id')
|
||||
|
@ -1539,8 +1550,9 @@ Substitutions.register('form_name', category=N_('Form'), comment=N_('Form Name')
|
|||
|
||||
def clean_drafts(publisher):
|
||||
import wcs.qommon.storage as st
|
||||
from .carddef import CardDef
|
||||
removal_date = datetime.date.today() - datetime.timedelta(days=100)
|
||||
for formdef in FormDef.select():
|
||||
for formdef in FormDef.select() + CardDef.select():
|
||||
for formdata in formdef.data_class().select(
|
||||
[st.Equal('status', 'draft'),
|
||||
st.Less('receipt_time', removal_date.timetuple())]):
|
||||
|
@ -1567,7 +1579,8 @@ def clean_unused_files(publisher):
|
|||
return obj.__class__.__name__ == 'AttachmentEvolutionPart'
|
||||
|
||||
def accumulate_filenames():
|
||||
for formdef in FormDef.select(ignore_migration=True):
|
||||
from .carddef import CardDef
|
||||
for formdef in FormDef.select(ignore_migration=True) + CardDef.select(ignore_migration=True):
|
||||
for option_data in (formdef.workflow_options or {}).values():
|
||||
if is_upload(option_data):
|
||||
yield option_data.get_filename()
|
||||
|
|
|
@ -302,7 +302,11 @@ class FormStatusPage(Directory, FormTemplateMixin):
|
|||
include_authors = get_request().is_in_backoffice() or include_authors_in_form_history
|
||||
return template.render(
|
||||
list(self.get_formdef_template_variants(self.history_templates)),
|
||||
{'formdata': self.filled, 'include_authors': include_authors})
|
||||
{
|
||||
'formdata': self.filled,
|
||||
'include_authors': include_authors,
|
||||
'view': self,
|
||||
})
|
||||
|
||||
def check_receiver(self):
|
||||
session = get_session()
|
||||
|
@ -316,6 +320,22 @@ class FormStatusPage(Directory, FormTemplateMixin):
|
|||
raise errors.AccessForbiddenError()
|
||||
return user
|
||||
|
||||
def should_fold_summary(self, mine, request_user):
|
||||
# fold the summary if the form has already been seen by the user, i.e.
|
||||
# if it's user own form or if the user is present in the formdata log
|
||||
# (evolution).
|
||||
if mine or (request_user and self.filled.is_submitter(request_user)):
|
||||
return True
|
||||
elif request_user and self.filled.evolution:
|
||||
for evo in self.filled.evolution:
|
||||
if (str(evo.who) == str(request_user.id) or
|
||||
(evo.who == '_submitter' and self.filled.is_submitter(request_user))):
|
||||
return True
|
||||
return False
|
||||
|
||||
def should_fold_history(self):
|
||||
return False
|
||||
|
||||
def receipt(self, always_include_user=False, show_status=True, form_url='', mine=True):
|
||||
request_user = user = get_request().user
|
||||
if not always_include_user and get_request().user and \
|
||||
|
@ -338,19 +358,7 @@ class FormStatusPage(Directory, FormTemplateMixin):
|
|||
|
||||
r = TemplateIO(html=True)
|
||||
klasses = 'foldable'
|
||||
# fold the summary if the form has already been seen by the user, i.e.
|
||||
# if it's user own form or if the user is present in the formdata log
|
||||
# (evolution).
|
||||
folded = False
|
||||
if mine or (request_user and self.filled.is_submitter(request_user)):
|
||||
folded = True
|
||||
elif request_user and self.filled.evolution:
|
||||
for evo in self.filled.evolution:
|
||||
if (str(evo.who) == str(request_user.id) or
|
||||
(evo.who == '_submitter' and self.filled.is_submitter(request_user))):
|
||||
folded = True
|
||||
break
|
||||
if folded:
|
||||
if self.should_fold_summary(mine, request_user):
|
||||
klasses += ' folded'
|
||||
|
||||
r += htmltext('<div class="section %s" id="summary">' % klasses)
|
||||
|
|
|
@ -198,10 +198,11 @@ class FormPage(Directory, FormTemplateMixin):
|
|||
filling_templates = ['wcs/front/formdata_filling.html', 'wcs/formdata_filling.html']
|
||||
validation_templates = ['wcs/front/formdata_validation.html', 'wcs/formdata_validation.html']
|
||||
steps_templates = ['wcs/front/formdata_steps.html', 'wcs/formdata_steps.html']
|
||||
formdef_class = FormDef
|
||||
|
||||
def __init__(self, component):
|
||||
try:
|
||||
self.formdef = FormDef.get_by_urlname(component)
|
||||
self.formdef = self.formdef_class.get_by_urlname(component)
|
||||
except KeyError:
|
||||
raise errors.TraversalError()
|
||||
|
||||
|
|
|
@ -163,7 +163,7 @@ class WcsPublisher(StubWcsPublisher):
|
|||
|
||||
def import_zip(self, fd):
|
||||
z = zipfile.ZipFile(fd)
|
||||
results = {'formdefs': 0, 'workflows': 0, 'categories': 0, 'roles': 0,
|
||||
results = {'formdefs': 0, 'carddefs': 0, 'workflows': 0, 'categories': 0, 'roles': 0,
|
||||
'settings': 0, 'datasources': 0, 'wscalls': 0}
|
||||
|
||||
def _decode_list(data):
|
||||
|
@ -195,7 +195,7 @@ class WcsPublisher(StubWcsPublisher):
|
|||
for f in z.namelist():
|
||||
if '.indexes' in f:
|
||||
continue
|
||||
if os.path.dirname(f) in ('formdefs_xml', 'workflows_xml'):
|
||||
if os.path.dirname(f) in ('formdefs_xml', 'carddefs_xml', 'workflows_xml'):
|
||||
continue
|
||||
path = os.path.join(self.app_dir, f)
|
||||
if not os.path.exists(os.path.dirname(path)):
|
||||
|
@ -233,15 +233,22 @@ class WcsPublisher(StubWcsPublisher):
|
|||
workflow.store()
|
||||
results['workflows'] += 1
|
||||
|
||||
# third pass, forms
|
||||
# third pass, forms and cards
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.carddef import CardDef
|
||||
formdefs = []
|
||||
carddefs = []
|
||||
for f in z.namelist():
|
||||
if os.path.dirname(f) == 'formdefs_xml' and os.path.basename(f):
|
||||
formdef = FormDef.import_from_xml(z.open(f), include_id=True)
|
||||
formdef.store()
|
||||
formdefs.append(formdef)
|
||||
results['formdefs'] += 1
|
||||
if os.path.dirname(f) == 'carddefs_xml' and os.path.basename(f):
|
||||
carddef = CardDef.import_from_xml(z.open(f), include_id=True)
|
||||
carddef.store()
|
||||
carddefs.append(carddef)
|
||||
results['carddefs'] += 1
|
||||
|
||||
# rebuild indexes for imported objects
|
||||
for k, v in results.items():
|
||||
|
@ -253,6 +260,9 @@ class WcsPublisher(StubWcsPublisher):
|
|||
if k == 'formdefs':
|
||||
from formdef import FormDef
|
||||
klass = FormDef
|
||||
elif k == 'carddefs':
|
||||
from carddef import CardDef
|
||||
klass = CardDef
|
||||
elif k == 'categories':
|
||||
from categories import Category
|
||||
klass = Category
|
||||
|
@ -270,6 +280,10 @@ class WcsPublisher(StubWcsPublisher):
|
|||
# are required.
|
||||
for formdef in (formdefs or FormDef.select()):
|
||||
formdef.store()
|
||||
elif k == 'carddefs':
|
||||
# ditto for cards
|
||||
for carddef in (carddefs or CardDef.select()):
|
||||
carddef.store()
|
||||
|
||||
z.close()
|
||||
return results
|
||||
|
@ -291,9 +305,10 @@ class WcsPublisher(StubWcsPublisher):
|
|||
sql.do_tracking_code_table()
|
||||
sql.do_meta_table()
|
||||
from formdef import FormDef
|
||||
from carddef import CardDef
|
||||
conn, cur = sql.get_connection_and_cursor()
|
||||
sql.drop_views(None, conn, cur)
|
||||
for formdef in FormDef.select():
|
||||
for formdef in FormDef.select() + CarDef.select():
|
||||
sql.do_formdef_tables(formdef)
|
||||
sql.migrate_global_views(conn, cur)
|
||||
conn.commit()
|
||||
|
|
|
@ -1106,6 +1106,12 @@ li.zone-home a:hover { background-image: url(icons/home.large-hover.png); }
|
|||
li.zone-submission a { background-image: url(icons/submission.large.png); }
|
||||
li.zone-submission a:hover { background-image: url(icons/submission.large-hover.png); }
|
||||
|
||||
li.zone-cards a { background-image: url(icons/categories.large.png); }
|
||||
li.zone-cards a:hover { background-image: url(icons/categories.large-hover.png); }
|
||||
|
||||
li.zone-data a { background-image: url(icons/data.large.png); }
|
||||
li.zone-data a:hover { background-image: url(icons/data.large-hover.png); }
|
||||
|
||||
ul.apps li.zone-no-icon a {
|
||||
height: 29px;
|
||||
padding-top: 24px;
|
||||
|
@ -1757,3 +1763,33 @@ ul#evolutions li.msg-in div.msg {
|
|||
ul#evolutions li.msg-out div.msg {
|
||||
background: #f1f8fe;
|
||||
}
|
||||
|
||||
div#studio {
|
||||
background: url(studio.svg) bottom right no-repeat;
|
||||
background-size: auto 90%;
|
||||
width: 100%;
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
a.button.button-paragraph {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
width: 40rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
a.button.button-paragraph p {
|
||||
font-weight: normal;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
a.button.button-paragraph p:last-child {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
a.button.button-paragraph:hover p {
|
||||
color: white;
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 729 B |
Binary file not shown.
After Width: | Height: | Size: 734 B |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 13 KiB |
53
wcs/sql.py
53
wcs/sql.py
|
@ -28,6 +28,7 @@ from qommon.substitution import invalidate_substitution_cache
|
|||
from qommon import get_cfg
|
||||
|
||||
import wcs.categories
|
||||
import wcs.carddata
|
||||
import wcs.formdata
|
||||
import wcs.tracking_code
|
||||
import wcs.users
|
||||
|
@ -309,8 +310,9 @@ def get_formdef_table_name(formdef):
|
|||
assert formdef.id is not None
|
||||
if hasattr(formdef, 'table_name') and formdef.table_name:
|
||||
return formdef.table_name
|
||||
formdef.table_name = 'formdata_%s_%s' % (formdef.id,
|
||||
get_name_as_sql_identifier(formdef.url_name)[:30])
|
||||
formdef.table_name = '%s_%s_%s' % (
|
||||
formdef.data_sql_prefix, formdef.id,
|
||||
get_name_as_sql_identifier(formdef.url_name)[:30])
|
||||
formdef.store()
|
||||
return formdef.table_name
|
||||
|
||||
|
@ -328,6 +330,20 @@ def get_formdef_new_id(id_start):
|
|||
cur.close()
|
||||
return new_id
|
||||
|
||||
def get_carddef_new_id(id_start):
|
||||
new_id = id_start
|
||||
conn, cur = get_connection_and_cursor()
|
||||
while True:
|
||||
cur.execute('''SELECT COUNT(*) FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name LIKE %s''', ('carddata\\_%s\\_%%' % new_id,))
|
||||
if cur.fetchone()[0] == 0:
|
||||
break
|
||||
new_id += 1
|
||||
conn.commit()
|
||||
cur.close()
|
||||
return new_id
|
||||
|
||||
def formdef_wipe():
|
||||
conn, cur = get_connection_and_cursor()
|
||||
cur.execute('''SELECT table_name FROM information_schema.tables
|
||||
|
@ -338,8 +354,21 @@ def formdef_wipe():
|
|||
conn.commit()
|
||||
cur.close()
|
||||
|
||||
def carddef_wipe():
|
||||
conn, cur = get_connection_and_cursor()
|
||||
cur.execute('''SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name LIKE %s''', ('carddata\\_%%\\_%%',))
|
||||
for table_name in [x[0] for x in cur.fetchall()]:
|
||||
cur.execute('''DROP TABLE %s CASCADE''' % table_name)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
|
||||
def get_formdef_view_name(formdef):
|
||||
return 'wcs_view_%s_%s' % (formdef.id,
|
||||
prefix = 'wcs_view'
|
||||
if formdef.data_sql_prefix != 'formdata':
|
||||
prefix = 'wcs_%s_view' % formdef.data_sql_prefix
|
||||
return '%s_%s_%s' % (prefix, formdef.id,
|
||||
get_name_as_sql_identifier(formdef.url_name)[:40])
|
||||
|
||||
def guard_postgres(func):
|
||||
|
@ -719,9 +748,12 @@ def drop_views(formdef, conn, cur):
|
|||
|
||||
if formdef:
|
||||
# remove the form view itself
|
||||
view_prefix = 'wcs\\_view\\_%s\\_%%' % formdef.id
|
||||
if formdef.data_sql_prefix != 'formdata':
|
||||
view_prefix = 'wcs\\_%s\\_view\\_%s\\_%%' % (formdef.data_sql_prefix, formdef.id)
|
||||
cur.execute('''SELECT table_name FROM information_schema.views
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name LIKE %s''', ('wcs\\_view\\_%s\\_%%' % formdef.id ,))
|
||||
AND table_name LIKE %s''', (view_prefix,))
|
||||
else:
|
||||
# if there's no formdef specified, remove all form views
|
||||
cur.execute('''SELECT table_name FROM information_schema.views
|
||||
|
@ -1193,7 +1225,7 @@ class SqlMixin(object):
|
|||
return ids
|
||||
|
||||
|
||||
class SqlFormData(SqlMixin, wcs.formdata.FormData):
|
||||
class SqlDataMixin(SqlMixin):
|
||||
_names = None # make sure StorableObject methods fail
|
||||
_formdef = None
|
||||
|
||||
|
@ -1621,6 +1653,14 @@ class SqlFormData(SqlMixin, wcs.formdata.FormData):
|
|||
do_tracking_code_table()
|
||||
|
||||
|
||||
class SqlFormData(SqlDataMixin, wcs.formdata.FormData):
|
||||
pass
|
||||
|
||||
|
||||
class SqlCardData(SqlDataMixin, wcs.carddata.CardData):
|
||||
pass
|
||||
|
||||
|
||||
class SqlUser(SqlMixin, wcs.users.User):
|
||||
_table_name = 'users'
|
||||
_table_static_fields = [
|
||||
|
@ -2243,7 +2283,8 @@ def set_reindex(index, value, conn=None, cur=None):
|
|||
def migrate_views(conn, cur):
|
||||
drop_views(None, conn, cur)
|
||||
from wcs.formdef import FormDef
|
||||
for formdef in FormDef.select():
|
||||
from wcs.carddef import CardDef
|
||||
for formdef in FormDef.select() + CardDef.select():
|
||||
# make sure all formdefs have up-to-date views
|
||||
do_formdef_tables(formdef, conn=conn, cur=cur, rebuild_views=True, rebuild_global_views=False)
|
||||
migrate_global_views(conn, cur)
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block body %}
|
||||
<div id="appbar" class="highlight">
|
||||
<h2>{% trans "Studio" %}</h2>
|
||||
</div>
|
||||
<div id="studio">
|
||||
{% if user.can_go_in_backoffice_forms %}
|
||||
<a class="button button-paragraph" href="../forms/">{% trans "Forms" context "studio" %}
|
||||
<p>{% trans "Forms are typically used to collect user demands." %}</p>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user.can_go_in_backoffice_cards %}
|
||||
<a class="button button-paragraph" href="../cards/">{% trans "Cards" context "studio" %}
|
||||
<p>{% trans "Cards are used to store list of structured data." %}</p>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user.can_go_in_backoffice_workflows %}
|
||||
<a class="button button-paragraph" href="../workflows/">{% trans "Workflows" context "studio" %}
|
||||
<p>{% trans "Workflows are used to add custom behaviours or actions to forms and cards." %}</p>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,5 +1,5 @@
|
|||
{% load i18n %}
|
||||
<div class="section foldable" id="evolution-log">
|
||||
<div class="section foldable {% if view.should_fold_history %}folded{% endif %}" id="evolution-log">
|
||||
<h2>{% trans "Log" %}</h2>
|
||||
<div>
|
||||
<ul id="evolutions">
|
||||
|
|
15
wcs/users.py
15
wcs/users.py
|
@ -122,6 +122,21 @@ class User(StorableObject):
|
|||
pass
|
||||
return False
|
||||
|
||||
def can_go_in_backoffice_section(self, section):
|
||||
authorised_roles = set(get_cfg('admin-permissions', {}).get(section) or [])
|
||||
if authorised_roles:
|
||||
return bool(set(self.roles or []).intersection(authorised_roles))
|
||||
return self.can_go_in_admin()
|
||||
|
||||
def can_go_in_backoffice_forms(self):
|
||||
return self.can_go_in_backoffice_section('forms')
|
||||
|
||||
def can_go_in_backoffice_cards(self):
|
||||
return self.can_go_in_backoffice_section('cards')
|
||||
|
||||
def can_go_in_backoffice_workflows(self):
|
||||
return self.can_go_in_backoffice_section('workflows')
|
||||
|
||||
@classmethod
|
||||
def get_available_roles(cls):
|
||||
from roles import get_user_roles
|
||||
|
|
Loading…
Reference in New Issue