general: add support for custom scripts in context variables (#12457)

This commit is contained in:
Frédéric Péters 2016-08-12 12:01:56 +02:00
parent 42f84a6139
commit 8c6bd68a76
7 changed files with 186 additions and 2 deletions

54
help/fr/dev-scripts.page Normal file
View File

@ -0,0 +1,54 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" id="dev-scripts" xml:lang="fr">
<info>
<link type="guide" xref="index#dev" />
<revision docversion="0.1" date="2016-08-12" status="draft"/>
<credit type="author">
<name>Frédéric Péters</name>
<email>fpeters@entrouvert.com</email>
</credit>
<desc>Utilisation de scripts externes dans les calculs</desc>
</info>
<title>Scripts externes</title>
<p>
Il est possible d'étendre les capacités des champs calculés et des expressions
utilisées dans les modèles au moyen de scripts externes. Pour cela il suffit
de déposer dans le répertoire système du site, dans un sous-répertoire
<code>scripts</code>, un fichier Python, dont le résultat doit être posé dans
une variable nommée <code>result</code>.
</p>
<p>
Par exemple <file>/var/lib/wcs/www.example.net/scripts/hello.py</file> pourrait
être créé avec le contenu suivant :
</p>
<code mime="text/python">
"""
Salue l'usager (quand un nom est en argument), ou le monde.
"""
if args:
result = "Hello %s" % args[0]
else:
result = "Hello world"
</code>
<p>
Dans un champ calculé, cela serait appelé comme <code>script.hello()</code> ou
<code>script.hello('earth')</code>; dans un modèle, ce serait
<code>[script.hello]</code> ou <code>[script.hello "earth"]</code>.
</p>
<note>
<p>
Il est également possible de placer ces scripts dans un sous-répertoire
<code>scripts</code> du répertoire général des instances, pour rendre ceux-ci
disponibles depuis l'ensemble des instances.
</p>
</note>
</page>

View File

@ -1,6 +1,15 @@
import pytest
import os
from StringIO import StringIO
from quixote import cleanup
from wcs.qommon.ezt import Template, UnclosedBlocksError, UnmatchedEndError, UnmatchedElseError
from wcs.scripts import ScriptsSubstitutionProxy
from utilities import get_app, create_temporary_pub
def setup_module(module):
cleanup()
def test_simple_qualifier():
template = Template()
@ -125,3 +134,24 @@ def test_dict_index():
output = StringIO()
template.generate(output, {'foo': {'a': 'bar'}})
assert output.getvalue() == '<p>[foo.b]</p>'
def test_ezt_script():
pub = create_temporary_pub()
os.mkdir(os.path.join(pub.app_dir, 'scripts'))
fd = open(os.path.join(pub.app_dir, 'scripts', 'hello_world.py'), 'w')
fd.write('''result = "Hello %s" % ("world" if not args else args[0])''')
fd.close()
vars = {'script': ScriptsSubstitutionProxy()}
template = Template()
template.parse('<p>[script.hello_world]</p>')
output = StringIO()
template.generate(output, vars)
assert output.getvalue() == '<p>Hello world</p>'
vars = {'script': ScriptsSubstitutionProxy()}
template = Template()
template.parse('<p>[script.hello_world "fred"]</p>')
output = StringIO()
template.generate(output, vars)
assert output.getvalue() == '<p>Hello fred</p>'

View File

@ -12,6 +12,7 @@ from wcs.qommon.form import FileSizeWidget
from wcs.qommon.humantime import humanduration2seconds, seconds2humanduration
from wcs.qommon.misc import simplify, json_loads, parse_isotime
from wcs.admin.settings import FileTypesDirectory
from wcs.scripts import Script
from utilities import get_app, create_temporary_pub
@ -107,6 +108,35 @@ def test_parse_isotime():
with pytest.raises(ValueError):
parse_isotime('2015-01-0110:10:19Z')
def test_script_substitution_variable():
pub = create_temporary_pub()
pub.substitutions.feed(pub)
pub.substitutions.feed(Script)
variables = pub.substitutions.get_context_variables()
with pytest.raises(AttributeError):
assert variables['script'].hello_world()
os.mkdir(os.path.join(pub.app_dir, 'scripts'))
fd = open(os.path.join(pub.app_dir, 'scripts', 'hello_world.py'), 'w')
fd.write('"""docstring"""\nresult = "hello world"')
fd.close()
assert variables['script'].hello_world() == 'hello world'
assert Script('hello_world').__doc__ == 'docstring'
os.mkdir(os.path.join(pub.APP_DIR, 'scripts'))
fd = open(os.path.join(pub.APP_DIR, 'scripts', 'hello_world.py'), 'w')
fd.write('result = "hello global world"')
fd.close()
assert variables['script'].hello_world() == 'hello world'
os.unlink(os.path.join(pub.app_dir, 'scripts', 'hello_world.py'))
assert variables['script'].hello_world() == 'hello global world'
fd = open(os.path.join(pub.app_dir, 'scripts', 'hello_world.py'), 'w')
fd.write('result = site_url')
fd.close()
assert variables['script'].hello_world() == 'http://example.net'
def test_default_charset():
pub = create_temporary_pub()

View File

@ -92,6 +92,11 @@ def create_temporary_pub(sql_mode=False):
if os.path.exists(os.path.join(pub.app_dir, 'wcs.log')):
os.unlink(os.path.join(pub.app_dir, 'wcs.log'))
if os.path.exists(os.path.join(pub.APP_DIR, 'scripts')):
shutil.rmtree(os.path.join(pub.APP_DIR, 'scripts'))
if os.path.exists(os.path.join(pub.app_dir, 'scripts')):
shutil.rmtree(os.path.join(pub.app_dir, 'scripts'))
if os.path.exists(pub.app_dir):
pub.cfg = {}
if sql_mode:

View File

@ -677,9 +677,14 @@ def _write_value(valrefs, fp, ctx, format=lambda s: s):
break
fp.write(format(chunk))
# value is a callback function: call with file pointer and extra args
# value is a callback function
elif callable(value):
apply(value, [fp] + args)
if getattr(value, 'ezt_call_mode', None) == 'simple':
# simple call mode, call with args and write the result
fp.write(apply(value, args))
else:
# default, call with file pointer and extra args
apply(value, [fp] + args)
# value is a substitution pattern
elif args:

View File

@ -52,6 +52,7 @@ from wscalls import NamedWsCall
from wcs.api import ApiDirectory
from myspace import MyspaceDirectory
from forms.preview import PreviewDirectory
from wcs.scripts import Script
from wcs import file_validation
@ -323,6 +324,7 @@ class RootDirectory(Directory):
get_publisher().substitutions.feed(get_request().user)
get_publisher().substitutions.feed(NamedDataSource)
get_publisher().substitutions.feed(NamedWsCall)
get_publisher().substitutions.feed(Script)
def _q_traverse(self, path):
self.feed_substitution_parts()

58
wcs/scripts.py Normal file
View File

@ -0,0 +1,58 @@
# w.c.s. - web application for online forms
# Copyright (C) 2005-2016 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 ast
import os
from quixote import get_publisher
class Script(object):
ezt_call_mode = 'simple'
def __init__(self, script_name):
self.script_name = script_name + '.py'
paths = [os.path.join(get_publisher().app_dir, 'scripts'),
os.path.join(get_publisher().APP_DIR, 'scripts')]
for path in paths:
script_path = os.path.join(path, script_name + '.py')
if os.path.exists(script_path):
self.code = open(script_path).read()
break
else:
raise ValueError()
@classmethod
def get_substitution_variables(cls):
return {'script': ScriptsSubstitutionProxy()}
@property
def __doc__(self):
return ast.get_docstring(ast.parse(self.code, self.script_name))
def __call__(self, *args):
data = get_publisher().substitutions.get_context_variables().copy()
data['args'] = args
code_object = compile(self.code, self.script_name, 'exec')
eval(code_object, data)
return data.get('result')
class ScriptsSubstitutionProxy(object):
def __getattr__(self, attr):
try:
return Script(attr)
except ValueError:
raise AttributeError()