563 lines
22 KiB
Python
563 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
|
|
import urllib.parse
|
|
|
|
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=_('Discard'))
|
|
|
|
form = Form(enctype='multipart/form-data')
|
|
form.widgets.append(HtmlWidget('<p>%s</p>' % _('You are about to discard this form.')))
|
|
form.add_submit('delete', _('Discard'))
|
|
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>') % (_('Discarding 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()
|
|
|
|
def restore_draft(self):
|
|
# redirect to draft and keep extra query parameters so {{request.GET}} can be used in form.
|
|
params = {'mt': self.get_restore_draft_magictoken()}
|
|
params.update(get_request().form or {})
|
|
return redirect('../?' + urllib.parse.urlencode(params))
|
|
|
|
|
|
class FormFillPage(PublicFormFillPage):
|
|
_q_exports = [
|
|
'',
|
|
'tempfile',
|
|
'autosave',
|
|
'code',
|
|
('remove', 'remove_draft'),
|
|
'live',
|
|
('lateral-block', 'lateral_block'),
|
|
('go-to-backoffice', 'go_to_backoffice'),
|
|
]
|
|
|
|
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)
|
|
redirect_url = '%s/' % formdata.id
|
|
extra_query_params = {
|
|
x: y
|
|
for x, y in get_request().form.items()
|
|
if x not in ('channel', 'NameID', 'ReturnURL', 'caller')
|
|
}
|
|
if extra_query_params:
|
|
redirect_url += '?' + urllib.parse.urlencode(extra_query_params)
|
|
return redirect(redirect_url)
|
|
|
|
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,
|
|
_('Discard 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)
|