general: add support for custom scripts in context variables (#12457)
This commit is contained in:
parent
42f84a6139
commit
8c6bd68a76
|
@ -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>
|
|
@ -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>'
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
Loading…
Reference in New Issue