wcs/wcs/backoffice/submission.py

547 lines
22 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 wcs.categories import Category
from wcs.formdata import FormData
from wcs.formdef import FormDef
from wcs.forms.common import FormStatusPage
from wcs.forms.root import FormPage as PublicFormFillPage
from ..qommon import _, errors, misc
from ..qommon.backoffice.menu import html_top
from ..qommon.form import Form, HtmlWidget
from ..qommon.storage import Equal, StrictNotEqual
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()._q_index()
class FormFillPage(PublicFormFillPage):
_q_exports = [
'',
'tempfile',
'autosave',
'code',
('remove', 'remove_draft'),
'live',
('lateral-block', 'lateral_block'),
]
filling_templates = ['wcs/formdata_filling.html']
popup_filling_templates = ['wcs/formdata_popup_filling.html']
validation_templates = ['wcs/formdata_validation.html']
steps_templates = ['wcs/formdata_steps.html']
has_channel_support = True
has_user_support = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.selected_submission_channel = None
self.selected_user_id = None
self.remove_draft = RemoveDraftDirectory(self)
if get_publisher().get_site_option('welco_url', 'options'):
# when welco is deployed, do not let agent manually change the
# submission channel
self.has_channel_support = False
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')
caller = get_request().form.get('caller')
if name_id or submission_channel or return_url or caller:
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
else:
get_session().message = (
'warning',
_('The target user was not found, this form is anonymous.'),
)
if return_url:
formdata.submission_context['return_url'] = return_url
if submission_channel == 'phone' and caller:
formdata.submission_context['caller'] = caller
formdata.store()
self.set_tracking_code(formdata)
return redirect('%s/' % formdata.id)
self.selected_submission_channel = get_request().form.get('channel') or get_request().form.get(
'submission_channel'
)
self.selected_user_id = get_request().form.get('user_id')
return super()._q_index(*args, **kwargs)
def lateral_block(self):
get_response().filter = {'raw': True}
response = self.get_lateral_block()
return response
def get_default_return_url(self):
return '%s/submission/' % get_publisher().get_backoffice_url()
def get_transient_formdata(self, magictoken=Ellipsis):
formdata = super().get_transient_formdata(magictoken=magictoken)
if self.selected_user_id:
formdata.user_id = self.selected_user_id
elif get_request().form.get('user_id'):
# when used via /live endpoint
formdata.user_id = get_request().form['user_id']
return formdata
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()
context['user_has_already_one_such_form'] = bool(
data_class.count([StrictNotEqual('status', 'draft'), Equal('user_id', formdata.user_id)])
)
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 formdata and self.selected_user_id:
formdata.user_id = self.selected_user_id
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>')
if formdata and self.on_validation_page:
if self.has_channel_support and self.selected_submission_channel:
formdata.submission_channel = self.selected_submission_channel
if self.has_user_support and self.selected_user_id:
formdata.user_id = self.selected_user_id
from .management import FormBackOfficeStatusPage
if self.on_validation_page or self.edit_mode:
if formdata:
r += FormBackOfficeStatusPage(self.formdef, formdata).get_extra_context_bar(parent=self)
else:
if (
formdata
and formdata.submission_context
and set(formdata.submission_context.keys()) != {'return_url'}
):
r += FormBackOfficeStatusPage(self.formdef, formdata).get_extra_submission_context_bar()
if formdata and formdata.submission_channel:
r += FormBackOfficeStatusPage(self.formdef, formdata).get_extra_submission_channel_bar()
elif self.has_channel_support:
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>')
if formdata and formdata.user_id:
r += FormBackOfficeStatusPage(self.formdef, formdata).get_extra_submission_user_id_bar(
parent=self
)
elif self.has_user_support:
r += FormBackOfficeStatusPage(self.formdef, formdata).get_extra_submission_user_selection_bar(
parent=self
)
if self.formdef.submission_lateral_template:
r += htmltext(
'<div data-async-url="%slateral-block"></div>' % self.formdef.get_backoffice_submission_url()
)
return r.getvalue()
def get_lateral_block(self):
r = TemplateIO(html=True)
lateral_block = self.formdef.get_submission_lateral_block()
if lateral_block:
r += htmltext('<div class="lateral-block">')
r += htmltext(lateral_block)
r += htmltext('</div>')
return r.getvalue()
def create_view_form(self, *args, **kwargs):
form = super().create_view_form(*args, **kwargs)
if self.has_channel_support:
form.add_hidden('submission_channel', self.selected_submission_channel)
if self.has_user_support:
form.add_hidden('user_id', self.selected_user_id)
return form
def create_form(self, *args, **kwargs):
form = super().create_form(*args, **kwargs)
form.attrs['data-live-url'] = self.formdef.get_backoffice_submission_url() + 'live'
if not get_publisher().has_site_option('backoffice-autosave'):
form.attrs['data-autosave'] = 'false'
if self.has_channel_support:
form.add_hidden('submission_channel', self.selected_submission_channel)
if self.has_user_support:
form.add_hidden('user_id', self.selected_user_id)
return form
def form_side(self, 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 and not getattr(self, 'is_popup', False):
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()
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(self.get_default_return_url())
filled.just_created()
filled.data = self.formdef.get_data(form)
magictoken = get_request().form['magictoken']
computed_values = get_session().get_by_magictoken('%s-computed' % magictoken, {})
filled.data.update(computed_values)
filled.backoffice_submission = True
if not filled.submission_context:
filled.submission_context = {}
if self.has_channel_support and self.selected_submission_channel:
filled.submission_channel = self.selected_submission_channel
if self.has_user_support and self.selected_user_id:
filled.user_id = self.selected_user_id
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()
return self.redirect_after_submitted(form, filled)
def redirect_after_submitted(self, form, filled):
url = filled.perform_workflow(event='backoffice-created')
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 and filled.submission_context.get('return_url'):
url = filled.submission_context['return_url']
else:
url = self.get_default_return_url()
else:
url = filled.get_url(backoffice=True)
return redirect(url)
def cancelled(self):
url = self.get_default_return_url()
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, where=None):
formdata = super().save_draft(data, page_no=page_no, where=where)
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)