wcs/wcs/backoffice/deprecations.py

294 lines
13 KiB
Python

# w.c.s. - web application for online forms
# Copyright (C) 2005-2022 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 datetime
import json
import os
import re
from quixote import get_publisher, get_request, get_response, redirect
from quixote.directory import Directory
from wcs.data_sources import NamedDataSource
from wcs.formdef import get_formdefs_of_all_kinds
from wcs.qommon import _, ezt, template
from wcs.qommon.afterjobs import AfterJob
from wcs.wf.export_to_model import UploadValidationError
from wcs.workflows import Workflow
from wcs.wscalls import NamedWsCall
class DeprecationsDirectory(Directory):
do_not_call_in_templates = True
_q_exports = ['', 'scan']
def _q_index(self):
report_path = os.path.join(get_publisher().app_dir, 'deprecations.json')
if not os.path.exists(report_path):
# create report if necessary
return self.scan()
get_response().set_title(_('Deprecations Report'))
get_response().breadcrumb.append(('deprecations/', _('Deprecations Report')))
context = {'has_sidebar': False, 'view': self}
with open(report_path) as fd:
context['report'] = json.load(fd)
context['report']['report_lines'].sort(key=lambda x: x['category'])
return template.QommonTemplateResponse(
templates=['wcs/backoffice/deprecations.html'], context=context, is_django_native=True
)
def scan(self):
job = get_response().add_after_job(
DeprecationsScanAfterJob(
label=_('Scanning for deprecations'),
user_id=get_request().user.id,
return_url='/backoffice/studio/deprecations/',
)
)
job.store()
return redirect(job.get_processing_url())
@property
def titles(self):
return {
'ezt': _('EZT text'),
'jsonp': _('JSONP data source'),
'python-condition': _('Python condition'),
'python-expression': _('Python expression'),
'python-prefill': _('Python prefill'),
'python-data-source': _('Python data source'),
'rtf': _('RTF Documents'),
'script': _('Filesystem Script'),
'fields': _('Obsolete field types'),
'actions': _('Obsolete action types'),
}
@property
def short_docs(self):
return {
'ezt': _('Use Django templates.'),
'jsonp': _('Use JSON sources with id and query parameters.'),
'python-condition': _('Use Django condition.'),
'python-expression': _('Use Django templates.'),
'python-prefill': _('Use Django templates.'),
'python-data-source': _('Use cards.'),
'rtf': _('Use OpenDocument format.'),
'script': _('Use a dedicated template tags application.'),
'fields': _('Use block fields to replace tables and ranked order fields.'),
'actions': '',
}
@property
def help_urls(self):
return {
'ezt': 'https://doc-publik.entrouvert.com/admin-fonctionnel/elements-deprecies/',
'jsonp': 'https://doc-publik.entrouvert.com/admin-fonctionnel/elements-deprecies/',
'python-condition': 'https://doc-publik.entrouvert.com/admin-fonctionnel/elements-deprecies/',
'python-expression': 'https://doc-publik.entrouvert.com/admin-fonctionnel/elements-deprecies/',
'python-prefill': 'https://doc-publik.entrouvert.com/admin-fonctionnel/elements-deprecies/',
'python-data-source': 'https://doc-publik.entrouvert.com/admin-fonctionnel/elements-deprecies/',
'rtf': 'https://doc-publik.entrouvert.com/admin-fonctionnel/elements-deprecies/',
'script': 'https://doc-publik.entrouvert.com/admin-fonctionnel/elements-deprecies/',
'fields': 'https://doc-publik.entrouvert.com/admin-fonctionnel/elements-deprecies/',
'actions': 'https://doc-publik.entrouvert.com/admin-fonctionnel/elements-deprecies/',
}
class DeprecationsScanAfterJob(AfterJob):
def done_action_url(self):
return self.kwargs['return_url']
def done_action_label(self):
return _('Go to deprecation report')
def done_button_attributes(self):
return {'data-redirect-auto': 'true'}
def execute(self):
self.report_lines = []
formdefs = get_formdefs_of_all_kinds()
workflows = Workflow.select(ignore_errors=True, ignore_migration=True)
named_data_sources = NamedDataSource.select(ignore_errors=True, ignore_migration=True)
named_ws_calls = NamedWsCall.select(ignore_errors=True, ignore_migration=True)
# extra step to build report file
self.total_count = len(formdefs) + len(workflows) + len(named_data_sources) + len(named_ws_calls) + 1
self.store()
for formdef in formdefs:
for field in formdef.fields or []:
location_label = _('%(name)s / Field "%(label)s"') % {
'name': formdef.name,
'label': field.ellipsized_label,
}
url = formdef.get_field_admin_url(field)
self.check_data_source(
getattr(field, 'data_source', None), location_label=location_label, url=url
)
prefill = getattr(field, 'prefill', None)
if prefill:
if prefill.get('type') == 'formula':
self.add_report_line(
location_label=location_label,
url=url,
category='python-prefill',
)
else:
self.check_string(
prefill.get('value'), location_label=location_label, url=url, python_check=False
)
if field.type == 'page':
for condition in field.get_conditions():
if condition and condition.get('type') == 'python':
self.add_report_line(
location_label=location_label,
url=url,
category='python-condition',
)
break
if field.type in ('title', 'subtitle', 'comment'):
self.check_string(field.label, location_label=location_label, url=url, python_check=False)
if field.type in ('table', 'table-select', 'tablerows', 'ranked-items'):
self.add_report_line(
location_label=location_label,
url=url,
category='fields',
)
self.increment_count()
for workflow in workflows:
for action in workflow.get_all_items():
location_label = '%s / %s' % (workflow.name, action.description)
url = action.get_admin_url()
for string in action.get_computed_strings():
self.check_string(string, location_label=location_label, url=url)
if getattr(action, 'condition', None):
if action.condition.get('type') == 'python':
self.add_report_line(
location_label=location_label,
url=url,
category='python-condition',
css_class='important' if (action.key == 'jump' and action.timeout) else '',
)
if action.key == 'export_to_model':
try:
kind = action.model_file_validation(action.model_file)
except UploadValidationError:
pass
else:
if kind == 'rtf':
self.add_report_line(location_label=location_label, url=url, category='rtf')
if action.key in ('aggregationemail',):
self.add_report_line(location_label=location_label, url=url, category='actions')
if action.key in ('register-comment', 'sendmail'):
for attachment in getattr(action, 'attachments', None) or []:
if attachment and not ('{%' in attachment or '{{' in attachment):
self.add_report_line(
location_label=location_label, url=url, category='python-expression'
)
break
for global_action in workflow.global_actions or []:
location_label = '%s / %s' % (workflow.name, _('trigger in %s') % global_action.name)
for trigger in global_action.triggers or []:
url = '%striggers/%s/' % (global_action.get_admin_url(), trigger.id)
if trigger.key == 'timeout' and trigger.anchor == 'python':
self.add_report_line(
location_label=location_label, url=url, category='python-expression'
)
break
self.increment_count()
for named_data_source in named_data_sources:
location_label = _('%(title)s "%(name)s"') % {
'title': _('Data source'),
'name': named_data_source.name,
}
url = named_data_source.get_admin_url()
self.check_data_source(
getattr(named_data_source, 'data_source', None), location_label=location_label, url=url
)
self.increment_count()
for named_ws_call in named_ws_calls:
location_label = _('%(title)s "%(name)s"') % {
'title': _('Webservice'),
'name': named_ws_call.name,
}
url = named_ws_call.get_admin_url()
for string in named_ws_call.get_computed_strings():
self.check_string(string, location_label=location_label, url=url)
self.increment_count()
self.build_report_file()
self.increment_count()
def check_data_source(self, data_source, location_label, url):
if not data_source:
return
if data_source.get('type') == 'jsonp':
self.add_report_line(
location_label=location_label,
url=url,
category='jsonp',
)
if data_source.get('type') == 'formula':
self.add_report_line(
location_label=location_label,
url=url,
category='python-data-source',
)
if data_source.get('type') == 'json':
self.check_string(data_source.get('value'), location_label, url, python_check=False)
def check_string(self, string, location_label, url, python_check=True):
if not isinstance(string, str):
return
if python_check and string.startswith('='): # python expression
self.add_report_line(location_label=location_label, url=url, category='python-expression')
else:
if template.Template(string).format == 'ezt':
try:
ezt.Template().parse(string)
except ezt.EZTException:
pass
else:
if not re.match(r'\[[^]]*[A-Z][^]]*\]', string):
# don't warn on leading [] expression if it has uppercases,
# this typically happens as initial "tag" in an email subjet.
self.add_report_line(location_label=location_label, url=url, category='ezt')
if re.findall(r'\Wscript\.\w', string):
self.add_report_line(location_label=location_label, url=url, category='script')
def add_report_line(self, **kwargs):
if kwargs not in self.report_lines:
self.report_lines.append(kwargs)
def build_report_file(self):
with open(os.path.join(get_publisher().app_dir, 'deprecations.json'), 'w') as fd:
json.dump(
{
'now': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'report_lines': self.report_lines,
},
fd,
indent=2,
)