backoffice: make it possible to submit forms from the backoffice (#7052)

This commit is contained in:
Frédéric Péters 2015-04-30 20:17:37 +02:00
parent cf43ef783c
commit d1884d84f7
8 changed files with 327 additions and 30 deletions

View File

@ -460,11 +460,11 @@ def test_form_roles():
app = login(get_app(pub))
resp = app.get('/backoffice/forms/1/')
resp = resp.click('change', href='roles')
resp = resp.click('change', href='roles', index=0)
resp = resp.forms[0].submit('cancel')
resp = app.get('/backoffice/forms/1/')
resp = resp.click('change', href='roles')
resp = resp.click('change', href='roles', index=0)
resp.forms[0]['roles$element0'].value = role.name
resp = resp.forms[0].submit('submit')
assert FormDef.get(1).roles == [role.id]
@ -827,7 +827,7 @@ def test_form_legacy_int_id():
assert resp.forms[0]['workflow_id'].value
resp = app.get('/backoffice/forms/1/')
resp = resp.click('change', href='roles')
resp = resp.click('change', href='roles', index=0)
assert resp.forms[0]['roles$element0'].value == 'Logged Users'
assert resp.forms[0]['roles$element1'].value == 'ZAB'

View File

@ -38,6 +38,7 @@ def create_user(pub, is_admin=False):
if pub.user_class.select(lambda x: x.name == 'admin'):
user1 = pub.user_class.select(lambda x: x.name == 'admin')[0]
user1.is_admin = is_admin
user1.roles = [x.id for x in Role.select() if x.name == 'foobar']
user1.store()
return user1
user1 = pub.user_class(name='admin')
@ -453,3 +454,105 @@ def test_global_statisticspub(pub):
resp.forms[0]['end'] = '2014-12-31'
resp = resp.forms[0].submit()
assert 'Total count: 20' in resp.body
def test_backoffice_submission(pub):
user = create_user(pub)
create_environment(pub)
app = login(get_app(pub))
resp = app.get('/backoffice/')
assert 'Submission' in resp.body
resp = resp.click('Submission', index=0)
formdef = FormDef.select()[0]
assert not formdef.url_name in resp.body
formdef.backoffice_submission_roles = user.roles[:]
formdef.store()
resp = app.get('/backoffice/submission/')
assert formdef.url_name in resp.body
resp = resp.click(formdef.name)
resp.form['f1'] = 'test submission'
resp.form['f2'] = 'baz'
resp.form['f3'] = 'C'
resp = resp.form.submit('submit')
assert 'Check values then click submit.' in resp.body
# going back to first page, to check
resp = resp.form.submit('previous')
assert resp.form['f1'].value == 'test submission'
resp = resp.form.submit('submit')
# final submit
resp = resp.form.submit('submit')
formdata_no = resp.location.split('/')[-2]
data_class = formdef.data_class()
assert data_class.get(formdata_no).data['1'] == 'test submission'
assert data_class.get(formdata_no).data['2'] == 'baz'
assert data_class.get(formdata_no).status == 'wf-new'
assert data_class.get(formdata_no).user is None
resp = resp.follow() # get to the formdata page
formdata_count = data_class.count()
# test submission when agent is not receiver
formdef.workflow_roles = {}
formdef.store()
resp = app.get('/backoffice/submission/')
resp = resp.click(formdef.name)
resp.form['f1'] = 'test submission'
resp.form['f2'] = 'baz'
resp.form['f3'] = 'C'
resp = resp.form.submit('submit') # to validation screen
resp = resp.form.submit('submit') # final submit
assert resp.location == 'http://example.net/backoffice/submission/'
resp = resp.follow() # should go back to submission screen
assert data_class.count() == formdata_count + 1
def test_backoffice_submission_tracking_code(pub):
user = create_user(pub)
create_environment(pub)
app = login(get_app(pub))
resp = app.get('/backoffice/')
assert 'Submission' in resp.body
resp = resp.click('Submission', index=0)
formdef = FormDef.select()[0]
assert not formdef.url_name in resp.body
formdef.enable_tracking_codes = True
formdef.backoffice_submission_roles = user.roles[:]
formdef.store()
data_class = formdef.data_class()
data_class.wipe()
resp = app.get('/backoffice/submission/')
assert formdef.url_name in resp.body
resp = resp.click(formdef.name)
resp.form['f1'] = 'test submission'
resp.form['f2'] = 'baz'
resp.form['f3'] = 'C'
resp = resp.form.submit('submit')
assert 'Check values then click submit.' in resp.body
# stop here, don't validate, let user finish it.
assert data_class.count() == 1
formdata_no = data_class.select()[0].id
tracking_code = data_class.select()[0].tracking_code
assert data_class.get(formdata_no).data['1'] == 'test submission'
assert data_class.get(formdata_no).data['2'] == 'baz'
assert data_class.get(formdata_no).status == 'draft'
assert data_class.get(formdata_no).user is None
resp = get_app(pub).get('/code/%s/load' % data_class.select()[0].tracking_code)
resp = resp.follow()
assert resp.location.startswith('http://example.net/form-title/?mt=')
resp = resp.follow()
assert 'Check values then click submit.' in resp.body
assert 'test submission' in resp.body

View File

@ -175,7 +175,8 @@ class FormDefPage(Directory):
('workflow-status-remapping', 'workflow_status_remapping'),
'roles', 'title', 'options', ('acl-read', 'acl_read'),
'overwrite', 'qrcode', 'information',
('public-url', 'public_url')]
('public-url', 'public_url'),
('backoffice-submission-roles', 'backoffice_submission_roles'),]
def __init__(self, component):
try:
@ -299,24 +300,10 @@ class FormDefPage(Directory):
r += htmltext('</ul>')
r += htmltext('</li>')
r += htmltext('<li>%s ') % _('User Roles:')
if self.formdef.roles:
roles = []
for x in self.formdef.roles:
if x == logged_users_role().id:
roles.append(logged_users_role().name)
else:
try:
roles.append(htmltext('<a href="../../roles/%s/">%s</a>') % (x, Role.get(x).name))
except KeyError:
# removed role ?
roles.append(htmltext('<em>%s</em>') % _('Unknown role (%s)') % role_id)
r += htmltext(', ').join(roles)
else:
r += '-'
r += ' '
r += htmltext('(<a href="roles" rel="popup">%s</a>)') % _('change')
r += htmltext('</li>')
r += self._display_roles(_('User Roles:'), 'roles', 'roles')
r += self._display_roles(_('Backoffice Submission Roles:'),
'backoffice_submission_roles',
'backoffice-submission-roles')
r += htmltext('<li>%s ') % _('Read Access:')
r += '%s' % {'none': _('None'),
@ -368,6 +355,29 @@ class FormDefPage(Directory):
r += htmltext('</div>')
return r.getvalue()
def _display_roles(self, title, attribute, change_url):
r = TemplateIO(html=True)
r += htmltext('<li>%s ') % title
if getattr(self.formdef, attribute):
roles = []
for x in getattr(self.formdef, attribute):
if x == logged_users_role().id:
roles.append(logged_users_role().name)
else:
try:
roles.append(htmltext('<a href="../../roles/%s/">%s</a>') % (x, Role.get(x).name))
except KeyError:
# removed role ?
roles.append(htmltext('<em>%s</em>') % _('Unknown role (%s)') % role_id)
r += htmltext(', ').join(roles)
else:
r += '-'
r += ' '
r += htmltext('(<a href="%s" rel="popup">%s</a>)') % (
change_url, _('change'))
r += htmltext('</li>')
return r.getvalue()
def get_sidebar(self):
r = TemplateIO(html=True)
r += htmltext('<ul id="sidebar-actions">')
@ -483,6 +493,14 @@ class FormDefPage(Directory):
attribute='roles',
description=_('Select the roles that can access this form.'))
def backoffice_submission_roles(self):
return self._roles_selection(
title=_('Backoffice Submission Roles'),
attribute='backoffice_submission_roles',
include_logged_users_role=False,
description=_('Select the roles that will be allowed to '
'fill out forms of this kind in the backoffice.'))
def title(self):
form = Form(enctype='multipart/form-data')
form.add(StringWidget, 'name', title=_('Form Title'), required=True,

View File

@ -39,6 +39,7 @@ import wcs.admin.settings
import wcs.admin.users
import wcs.admin.workflows
from . import submission
from . import management
@ -54,9 +55,11 @@ class RootDirectory(BackofficeRootDirectory):
users = wcs.admin.users.UsersDirectory()
workflows = wcs.admin.workflows.WorkflowsDirectory()
management = management.ManagementDirectory()
submission = submission.SubmissionDirectory()
menu_items = [
('management/', N_('Management')),
('submission/', N_('Submission')),
('forms/', N_('Forms Workshop')),
('workflows/', N_('Workflows Workshop')),
('users/', N_('Users')),
@ -104,7 +107,7 @@ class RootDirectory(BackofficeRootDirectory):
# for some subdirectories, the user needs to be part of a role allowed
# to go in the backoffice
if subdirectory in ('management',):
if subdirectory in ('management', 'submission'):
return get_request().user.can_go_in_backoffice()
# for the other directories, an extra level is required, the user needs
@ -272,6 +275,6 @@ class RootDirectory(BackofficeRootDirectory):
'slug': slug,
'url': backoffice_url + k})
if slug in ('home', 'forms', 'workflows', 'users', 'roles',
'categories', 'settings', 'management'):
'categories', 'settings', 'management', 'submission'):
menu_items[-1]['icon'] = k.strip('/')
return menu_items

View File

@ -0,0 +1,155 @@
# w.c.s. - web application for online forms
# Copyright (C) 2005-2015 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, get_request, get_response, get_session, redirect
from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
from qommon.backoffice.menu import html_top
from qommon import errors
from wcs.formdef import FormDef
from wcs.categories import Category
from wcs.forms.root import FormPage as PublicFormFillPage
class FormFillPage(PublicFormFillPage):
def html_top(self, *args, **kwargs):
return html_top('submission', *args, **kwargs)
def check_role(self):
if not self.formdef.backoffice_submission_roles:
raise errors.AccessUnauthorizedError()
for role in get_request().user.roles or []:
if role in self.formdef.backoffice_submission_roles:
break
else:
raise errors.AccessUnauthorizedError()
def get_sidebar(self, data):
r = TemplateIO(html=True)
if self.formdef.enable_tracking_codes:
draft_formdata_id = data.get('draft_formdata_id')
r += htmltext('<h3>%s</h3>') % _('Tracking Code')
tracking_code = None
if draft_formdata_id:
formdata = self.formdef.data_class().get(draft_formdata_id)
if formdata.tracking_code:
tracking_code = formdata.tracking_code
if tracking_code:
r += htmltext('<p>%s</p>') % tracking_code
else:
r += htmltext('<p>-</p>')
return r.getvalue()
def form_side(self, step_no, page_no=0, log_detail=None, data=None, editing=None):
r = TemplateIO(html=True)
get_response().filter['sidebar'] = self.get_sidebar(data)
r += htmltext('<div id="side">')
r += self.step(step_no, page_no, log_detail, data=data, editing=editing)
r += htmltext('</div> <!-- #side -->')
return r.getvalue()
def submitted(self, form, *args):
filled = self.formdef.data_class()()
filled.just_created()
filled.data = self.formdef.get_data(form)
filled.store()
self.keep_tracking_code(filled)
get_session().remove_magictoken(get_request().form.get('magictoken'))
url = filled.perform_workflow()
if not url:
url = filled.get_url(backoffice=True)
if not self.formdef.is_of_concern_for_user(self.user, filled):
# if the agent is not allowed to see the submitted formdef,
# redirect to the submission homepage
return redirect(get_publisher().get_backoffice_url() + '/submission/')
return redirect(url)
def save_draft(self, data, page_no):
formdata = super(FormFillPage, self).save_draft(data, page_no)
formdata.user_id = None
formdata.store()
return formdata
class SubmissionDirectory(Directory):
_q_exports = ['']
def _q_index(self):
get_response().breadcrumb.append(('submission/', _('Submission')))
html_top('submission', _('Submission'))
user = get_request().user
list_forms = []
for formdef in FormDef.select(order_by='name', ignore_errors=True):
if formdef.is_disabled():
continue
if not formdef.backoffice_submission_roles:
continue
for role in user.roles or []:
if role in formdef.backoffice_submission_roles:
break
else:
continue
list_forms.append(formdef)
r = TemplateIO(html=True)
cats = Category.select()
Category.sort_by_position(cats)
one = False
for c in cats:
l2 = [x for x in list_forms if str(x.category_id) == str(c.id)]
if l2:
r += self.form_list(l2, title=c.name)
one = True
l2 = [x for x in list_forms if not x.category]
if l2:
if one:
title = _('Misc')
else:
title = None
r += self.form_list(l2, title=title)
return r.getvalue()
def form_list(self, formdefs, title=None):
r = TemplateIO(html=True)
r += htmltext('<div class="bo-block">')
if title:
r += htmltext('<h2>%s</h2>') % title
r += htmltext('<ul class="biglist">')
for formdef in formdefs:
r += htmltext('<li>')
r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (
formdef.url_name, formdef.name)
r += htmltext('</li>')
r += htmltext('</ul>')
r += htmltext('</div>')
return r.getvalue()
def _q_lookup(self, component):
get_response().breadcrumb.append(('submission/', _('Submission')))
return FormFillPage(component)

View File

@ -67,6 +67,7 @@ class FormDef(StorableObject):
workflow_options = None
workflow_roles = None
roles = None
backoffice_submission_roles = None
discussion = False
confirmation = True
detailed_emails = True

View File

@ -222,6 +222,9 @@ class FormPage(Directory):
self.user = get_request().user
get_response().breadcrumb.append( (component + '/', self.formdef.name) )
def html_top(self, *args, **kwargs):
html_top(*args, **kwargs)
def get_substitution_variables(self):
return self.substvars
@ -375,6 +378,11 @@ class FormPage(Directory):
v = data[k]
elif field.prefill:
v = field.get_prefill_value()
if get_request().get_path().startswith('/backoffice/') and (
field.prefill and field.prefill.get('type') in ('user', 'geoloc')):
# turn off prefilling from user and geolocation
# attributes if the form is filled from the backoffice
v = None
if v:
prefilled = True
form.get_widget('f%s' % k).set_message(
@ -395,7 +403,7 @@ class FormPage(Directory):
if not one:
req.form = {}
html_top(self.formdef.name)
self.html_top(self.formdef.name)
r += self.form_side(0, page_no, log_detail=log_detail, data=data, editing=editing)
form.add_hidden('step', '0')
@ -505,7 +513,7 @@ class FormPage(Directory):
if data.has_key('page_no'):
page_no = int(data['page_no'])
del data['page_no']
if page_no == -1:
if page_no == -1 or page_no >= self.page_number:
req = get_request()
for k, v in data.items():
req.form['f%s' % k] = v
@ -799,6 +807,9 @@ class FormPage(Directory):
except AttributeError:
filled.user_id = get_request().user.id
if get_request().get_path().startswith('/backoffice/'):
filled.user_id = None
if self.formdef.only_allow_one:
# this is already checked in _q_index but it's done a second time
# just before a new form is to be stored.
@ -819,7 +830,10 @@ class FormPage(Directory):
get_session().mark_anonymous_formdata(filled)
if not url:
url = filled.get_url()
if get_request().get_path().startswith('/backoffice/'):
url = filled.get_url(backoffice=True)
else:
url = filled.get_url()
return redirect(url)
def keep_tracking_code(self, formdata):
@ -869,7 +883,7 @@ class FormPage(Directory):
return get_session().get_tempfile_content(t).get_file_pointer().read()
def validating(self, data):
html_top(self.formdef.name)
self.html_top(self.formdef.name)
r = TemplateIO(html=True)
r += htmltext('<div class="form-validation">')
r += self.form_side(step_no=1, data=data)
@ -892,7 +906,7 @@ class FormPage(Directory):
return r.getvalue()
def error(self, msg):
html_top(self.formdef.name)
self.html_top(self.formdef.name)
homepage = get_publisher().get_root_url()
r = TemplateIO(html=True)
r += htmltext('<div class="errornotice">%s</div>') % msg
@ -903,7 +917,7 @@ class FormPage(Directory):
if not self.formdef.is_user_allowed_read(get_request().user):
raise errors.AccessForbiddenError()
get_response().breadcrumb.append( ('listing', _('Listing')) )
html_top('%s - %s' % (_('Listing'), self.formdef.name))
self.html_top('%s - %s' % (_('Listing'), self.formdef.name))
r = TemplateIO(html=True)
fields = []

View File

@ -854,6 +854,9 @@ li.zone-management a:hover { background-image: url(icons/management.large-hover.
li.zone-home a { background-image: url(icons/home.large.png); }
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); }
ul.apps li.zone-no-icon a {
height: 29px;
padding-top: 24px;