wcs/wcs/backoffice/submission.py

447 lines
18 KiB
Python

# 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/>.
import time
from django.utils.safestring import mark_safe
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 import _
from ..qommon.backoffice.menu import html_top
from ..qommon.form import *
from ..qommon.storage import Equal, NotEqual
from ..qommon import errors, misc
from wcs.formdata import FormData
from wcs.formdef import FormDef
from wcs.categories import Category
from wcs.forms.common import FormStatusPage
from wcs.forms.root import FormPage as PublicFormFillPage
class RemoveDraftDirectory(Directory):
def __init__(self, parent_directory):
self.parent_directory = parent_directory
self.formdef = parent_directory.formdef
def _q_lookup(self, component):
try:
formdata = self.formdef.data_class().get(component)
except KeyError:
raise errors.TraversalError()
if not formdata.is_draft():
raise errors.AccessForbiddenError()
if not formdata.backoffice_submission:
raise errors.AccessForbiddenError()
self.parent_directory.check_role()
if self.parent_directory.edit_mode:
raise errors.AccessForbiddenError()
self.parent_directory.html_top(title=_('Remove'))
form = Form(enctype='multipart/form-data')
form.widgets.append(HtmlWidget('<p>%s</p>' % _(
'You are about to irrevocably delete this form.')))
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():
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % (_('Deleting Form'))
r += form.render()
return r.getvalue()
if formdata.tracking_code:
get_publisher().tracking_code_class.remove_object(formdata.tracking_code)
return_url = '../..'
if formdata.submission_context:
return_url = formdata.submission_context.get('return_url', return_url)
formdata.remove_self()
return redirect(return_url)
class SubmissionFormStatusPage(FormStatusPage):
_q_exports_orig = ['', 'download', 'live']
def _q_index(self):
if not self.filled.is_draft():
get_session().message = ('error', _('This form has already been submitted.'))
return redirect(get_publisher().get_backoffice_url() + '/submission/')
return super(SubmissionFormStatusPage, self)._q_index()
class FormFillPage(PublicFormFillPage):
_q_exports = ['', 'tempfile', 'autosave', 'code',
('remove', 'remove_draft'), 'live']
filling_templates = ['wcs/formdata_filling.html']
validation_templates = ['wcs/formdata_validation.html']
steps_templates = ['wcs/formdata_steps.html']
def __init__(self, *args, **kwargs):
super(FormFillPage, self).__init__(*args, **kwargs)
self.selected_submission_channel = None
self.remove_draft = RemoveDraftDirectory(self)
def _q_index(self, *args, **kwargs):
# if NameID, return URL or submission channel are in query string,
# create a new draft with these parameters, and redirect to it
submission_channel = get_request().form.get('channel')
name_id = get_request().form.get('NameID')
return_url = get_request().form.get('ReturnURL')
if name_id or submission_channel or return_url:
formdata = self.formdef.data_class()()
formdata.data = {}
formdata.backoffice_submission = True
formdata.submission_channel = submission_channel or ''
formdata.submission_agent_id = str(get_request().user.id)
formdata.submission_context = {}
formdata.status = 'draft'
formdata.receipt_time = time.localtime()
if name_id:
users = list(get_publisher().user_class.get_users_with_name_identifier(name_id))
if users:
formdata.user_id = users[0].id
if return_url:
formdata.submission_context['return_url'] = return_url
formdata.store()
self.set_tracking_code(formdata)
return redirect('%s/' % formdata.id)
self.selected_submission_channel = get_request().form.get('submission_channel') or ''
return super(FormFillPage, self)._q_index(*args, **kwargs)
def html_top(self, *args, **kwargs):
return html_top('submission', *args, **kwargs)
@classmethod
def get_status_page_class(cls):
return SubmissionFormStatusPage
def check_authentication_context(self):
pass
def check_role(self):
if self.edit_mode:
return True
if not self.formdef.backoffice_submission_roles:
raise errors.AccessUnauthorizedError()
for role in get_request().user.get_roles():
if role in self.formdef.backoffice_submission_roles:
break
else:
raise errors.AccessUnauthorizedError()
def check_unique_submission(self):
return None
def modify_filling_context(self, context, page, data):
if not self.formdef.only_allow_one:
return
try:
formdata = self.formdef.data_class().get(data['draft_formdata_id'])
except KeyError: # it may not exist
return
data_class = self.formdef.data_class()
if get_publisher().is_using_postgresql():
has = bool(data_class.count([NotEqual('status', 'draft'), Equal('user_id', formdata.user_id)]))
else:
has = any(x for x in data_class.get_with_indexed_value('user_id', formdata.user_id) if not x.is_draft())
context['user_has_already_one_such_form'] = has
def get_sidebar(self, data):
r = TemplateIO(html=True)
formdata = None
if self.edit_mode:
formdata = self.edited_data
else:
draft_formdata_id = data.get('draft_formdata_id')
if draft_formdata_id:
try:
formdata = self.formdef.data_class().get(draft_formdata_id)
except KeyError: # it may not exist
pass
if self.formdef.enable_tracking_codes and not self.edit_mode:
r += htmltext('<h3>%s</h3>') % _('Tracking Code')
if formdata and formdata.tracking_code:
r += htmltext('<p>%s</p>') % formdata.tracking_code
else:
r += htmltext('<p>-</p>')
welco_url = get_publisher().get_site_option('welco_url', 'options')
if formdata and (formdata.submission_context or formdata.user_id):
from .management import FormBackOfficeStatusPage
r += FormBackOfficeStatusPage(self.formdef, formdata).get_extra_context_bar()
elif not welco_url and not self.edit_mode:
r += htmltext('<div class="submit-channel-selection" style="display: none;">')
r += htmltext('<h3>%s</h3>') % _('Channel')
r += htmltext('<select>')
for channel_key, channel_label in [('', '-')] + list(FormData.get_submission_channels().items()):
selected = ''
if self.selected_submission_channel == channel_key:
selected = 'selected="selected"'
r += htmltext('<option value="%s" %s>' % (channel_key, selected))
r += htmltext('%s</option>') % channel_label
r += htmltext('</select>')
r += htmltext('</div>')
return r.getvalue()
def create_view_form(self, *args, **kwargs):
form = super(FormFillPage, self).create_view_form(*args, **kwargs)
welco_url = get_publisher().get_site_option('welco_url', 'options')
if not welco_url:
form.add_hidden('submission_channel', self.selected_submission_channel)
return form
def create_form(self, *args, **kwargs):
form = super(FormFillPage, self).create_form(*args, **kwargs)
form.attrs['data-live-url'] = self.formdef.get_backoffice_submission_url() + 'live'
welco_url = get_publisher().get_site_option('welco_url', 'options')
if not welco_url:
form.add_hidden('submission_channel', self.selected_submission_channel)
return form
def form_side(self, step_no, page, data=None, magictoken=None):
r = TemplateIO(html=True)
get_response().filter['sidebar'] = self.get_sidebar(data)
r += htmltext('<div id="appbar">')
r += htmltext('<h2>%s</h2>') % self.formdef.name
if not self.edit_mode:
draft_formdata_id = data.get('draft_formdata_id')
if draft_formdata_id:
r += htmltext('<a rel="popup" href="remove/%s">%s</a>') % (draft_formdata_id, _('Delete this form'))
r += htmltext('</div>')
r += htmltext('<div id="side">')
r += self.step(step_no, page)
r += htmltext('</div> <!-- #side -->')
return mark_safe(str(r.getvalue()))
def submitted(self, form, *args):
filled = self.get_current_draft() or self.formdef.data_class()()
if filled.id and filled.status != 'draft':
get_session().message = ('error', _('This form has already been submitted.'))
return redirect(get_publisher().get_backoffice_url() + '/submission/')
filled.just_created()
filled.data = self.formdef.get_data(form)
filled.backoffice_submission = True
if not filled.submission_context:
filled.submission_context = {}
if self.selected_submission_channel:
filled.submission_channel = self.selected_submission_channel
filled.submission_agent_id = str(get_request().user.id)
filled.store()
self.set_tracking_code(filled)
get_session().remove_magictoken(get_request().form.get('magictoken'))
self.clean_submission_context()
url = filled.perform_workflow()
if url:
pass # always redirect to an URL the workflow returned
elif 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 defined return URL or to the submission
# homepage
if filled.submission_context.get('return_url'):
url = filled.submission_context['return_url']
else:
url = get_publisher().get_backoffice_url() + '/submission/'
else:
url = filled.get_url(backoffice=True)
return redirect(url)
def cancelled(self):
url = get_publisher().get_backoffice_url() + '/submission/'
formdata = self.get_current_draft() or self.formdef.data_class()()
if formdata.submission_context and formdata.submission_context.get('return_url'):
url = formdata.submission_context.get('return_url')
if formdata.id:
formdata.remove_self()
return redirect(url)
def save_draft(self, data, page_no=None):
formdata = super(FormFillPage, self).save_draft(data, page_no=page_no)
formdata.backoffice_submission = True
if not formdata.submission_context:
formdata.submission_context = {}
formdata.submission_agent_id = str(get_request().user.id)
formdata.store()
return formdata
class SubmissionDirectory(Directory):
_q_exports = ['', 'count']
def is_accessible(self, user):
if not user.can_go_in_backoffice():
return False
# check user has at least one role set for backoffice submission
for role_id in (user.roles or []):
ids = FormDef.get_ids_with_indexed_value('backoffice_submission_roles', role_id)
if ids:
return True
return False
def get_submittable_formdefs(self):
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.get_roles():
if role in formdef.backoffice_submission_roles:
break
else:
continue
list_forms.append(formdef)
return list_forms
def _q_index(self):
get_response().breadcrumb.append(('submission/', _('Submission')))
html_top('submission', _('Submission'))
list_forms = self.get_submittable_formdefs()
cats = Category.select()
Category.sort_by_position(cats)
for cat in cats:
cat.formdefs = [x for x in list_forms if str(x.category_id) == str(cat.id)]
misc_cat = Category(name=_('Misc'))
misc_cat.formdefs = [x for x in list_forms if not x.category]
cats.append(misc_cat)
welco_url = get_publisher().get_site_option('welco_url', 'options')
r = TemplateIO(html=True)
r += get_session().display_message()
modes = ['empty', 'create', 'existing']
if welco_url:
modes.remove('create')
empty = True
for mode in modes:
list_content = TemplateIO()
for cat in cats:
if not cat.formdefs:
continue
list_content += self.form_list(cat.formdefs, title=cat.name, mode=mode)
if not list_content.getvalue().strip():
continue
empty = False
r += htmltext('<h2>%s</h2>') % {
'create': _('New submission'),
'existing': _('Running submission'),
'empty': _('Submission to complete'),
}.get(mode)
r += htmltext('<ul class="biglist">')
r += htmltext(list_content.getvalue())
r += htmltext('</ul>')
if empty and welco_url:
return redirect(welco_url)
return r.getvalue()
def form_list(self, formdefs, title=None, mode='create'):
r = TemplateIO(html=True)
if mode != 'create':
skip = True
for formdef in formdefs:
if not hasattr(formdef, '_formdatas'):
data_class = formdef.data_class()
formdata_ids = data_class.get_ids_with_indexed_value('status', 'draft')
formdef._formdatas = [x for x in data_class.get_ids(formdata_ids)
if x.backoffice_submission is True]
skip &= not(bool(formdef._formdatas))
if skip:
return
first = True
for formdef in formdefs:
if mode != 'create':
formdatas = formdef._formdatas[:]
if mode == 'empty':
formdatas = [x for x in formdatas if x.has_empty_data()]
elif mode == 'existing':
formdatas = [x for x in formdatas if not x.has_empty_data()]
if not formdatas:
continue
if first and title:
r += htmltext('<li><h3>%s</h3></li>') % title
first = False
r += htmltext('<li>')
if mode == 'create':
r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (
formdef.url_name, formdef.name)
else:
r += htmltext('<strong class="label"><a class="fake">%s</a></strong>') % formdef.name
r += htmltext('</li>')
if mode == 'create':
continue
for formdata in formdatas:
r += htmltext('<li class="smallitem">')
label = ''
if formdata.submission_channel:
label = '%s ' % formdata.get_submission_channel_label()
label += _('#%(id)s, %(time)s') % {'id': formdata.id,
'time': misc.localstrftime(formdata.receipt_time)}
if formdata.submission_agent_id:
agent_user = get_publisher().user_class.get(
formdata.submission_agent_id, ignore_errors=True)
if agent_user:
label += ' (%s)' % agent_user.display_name
r += htmltext('<a href="%s/%s/">%s</a>') % (
formdef.url_name, formdata.id, label)
r += htmltext('</li>')
return r.getvalue()
def count(self):
formdefs = self.get_submittable_formdefs()
count = 0
mode = get_request().form.get('mode')
for formdef in formdefs:
if not hasattr(formdef, '_formdatas'):
data_class = formdef.data_class()
formdata_ids = data_class.get_ids_with_indexed_value('status', 'draft')
formdatas = [x for x in data_class.get_ids(formdata_ids)
if x.backoffice_submission is True]
if mode == 'empty':
formdatas = [x for x in formdatas if x.has_empty_data()]
elif mode == 'existing':
formdatas = [x for x in formdatas if not x.has_empty_data()]
count += len(formdatas)
return misc.json_response({'count': count})
def _q_lookup(self, component):
get_response().breadcrumb.append(('submission/', _('Submission')))
return FormFillPage(component)