Permalink
Browse files

Merge branch 'feature/chained-js-calls' into develop

  • Loading branch information...
ralphbean committed Mar 14, 2012
2 parents f63a37c + eb7ef50 commit 612d52a88e1c8128615b70a43afe90d370a4d3d6
Showing with 189 additions and 95 deletions.
  1. +1 −1 setup.py
  2. +28 −5 tests/test_resources.py
  3. +16 −3 tw2/core/__init__.py
  4. +110 −34 tw2/core/js.py
  5. +2 −2 tw2/core/middleware.py
  6. +27 −49 tw2/core/resources.py
  7. +5 −1 tw2/core/widgets.py
@@ -32,7 +32,7 @@ def get_description(fname='README.rst'):
setup(
name='tw2.core',
version='2.0rc3',
version='2.0rc4',
description="Web widget creation toolkit based on TurboGears widgets",
long_description = get_description(),
install_requires=[
@@ -257,7 +257,7 @@ def _test_repr_(self):
class TestJsFuncall(tb.WidgetTest):
widget = twr.JSFuncCall
widget = twr._JSFuncCall
attrs = {'function':'foo', 'args':['a', 'b']}
expected = None
@@ -292,10 +292,10 @@ def testJSSymbol(self):
self.assert_(s.src=="source")
def testEncoderDefault(self):
enc = twr.TW2Encoder()
enc = twc.encoder
enc.encode("")
res = enc.default(twr.JSSymbol("X"))
self.assert_(res.startswith(enc.__class__.__name__))
self.assert_(res.startswith("*#*"))
try:
res = enc.default(None)
@@ -304,7 +304,7 @@ def testEncoderDefault(self):
self.assert_(te.message.endswith("is not JSON serializable"))
def testUnEscapeMarked(self):
enc = twr.TW2Encoder()
enc = twc.encoder
data = dict(foo=twr.JSSymbol("foo"),
bar=twr.JSSymbol("bar"))
res = enc.unescape_marked(enc.encode(data))
@@ -340,10 +340,33 @@ def testJSSource(self):
res = str(s.req())
self.assert_(token in res, res)
def testJSFuncCallChained(self):
rl = testapi.request(1, mw)
options = {'foo': 20}
jQuery = twc.js_function('jQuery')
when_ready = lambda func: twc.js_callback(
'$(document).ready(function(){' + str(func) + '});'
)
class Dummy(twc.Widget):
id = 'dummy'
template = "tw2.core.test_templates.display_only_test_widget"
def prepare(self):
super(Dummy, self).prepare()
self.add_call(when_ready(
jQuery('.%s' % self.id).buildMenu(options)
))
output = Dummy.display()
eq_(len(rl['resources']), 1)
eq_(str(rl['resources'][0]), '$(document).ready(function(){jQuery(".dummy").buildMenu({"foo": 20})});')
def testJSFuncCallDictArgs(self):
args = dict(foo="foo", bar="bar")
function = "jquery"
s = twr.JSFuncCall(function=function, args=args).req()
s = twr._JSFuncCall(function=function, args=args).req()
s.prepare()
self.assert_(function in str(s))
for k in args:
@@ -11,8 +11,16 @@
from widgets import (Widget, CompoundWidget, RepeatingWidget,
DisplayOnlyWidget, Page)
from resources import (JSSymbol, Link, JSLink, CSSLink, CSSSource, JSSource,
JSFuncCall, inject_resources, DirLink, encoder)
from resources import (
JSSymbol,
Link,
JSLink,
CSSLink,
CSSSource,
JSSource,
inject_resources,
DirLink,
)
from validation import (
Validator, LengthValidator,
@@ -32,7 +40,12 @@
register_resource,
)
from js import js_symbol, js_callback, js_function
from js import (
js_symbol,
js_callback,
js_function,
encoder
)
from i18n import _, tw2_translation_string
@@ -4,28 +4,77 @@
This moudle doesn't aim to serve as a Python-JS "translator". You should code
your client-side code in JavaScript and make it available in static files which
you include as JSLinks or inline using JSSources. This module is only intended
as a "bridge" or interface between Python and JavaScript so JS function
as a "bridge" or interface between Python and JavaScript so JS function
**calls** can be generated programatically.
"""
import sys
import sys
import logging
from itertools import imap
import simplejson.encoder
import warnings
__all__ = ["js_callback", "js_function", "js_symbol", "encode"]
from tw2.core import JSFuncCall
log = logging.getLogger(__name__)
__all__ = ["js_callback", "js_function", "js_symbol"]
class TWEncoder(simplejson.encoder.JSONEncoder):
"""A JSON encoder that can encode Widgets, js_calls, js_symbols and
js_callbacks.
Example::
log = logging.getLogger(__name__)
>> encode = TWEncoder().encode
>> print encode({'onLoad': js_function("do_something")(js_symbol("this"))})
{"onLoad": do_something(this)}
>> from tw2.core.api import Widget
>> w = Widget("foo")
>> args = {'onLoad': js_callback(js_function('jQuery')(w).click(js_symbol('onClick')))}
>> print encode(args)
{"onLoad": function(){jQuery(\\"foo\\").click(onClick)}}
>> print encode({'args':args})
{"args": {"onLoad": function(){jQuery(\\"foo\\").click(onClick)}}}
def js_symbol(name):
warnings.warn("js_symbol will soon be deprecated, use JSSymbol instead.",
DeprecationWarning)
from resources import JSSymbol
return JSSymbol(name)
"""
def __init__(self, *args, **kw):
self.pass_through = (_js_call, js_callback, js_symbol, js_function)
super(TWEncoder, self).__init__(*args, **kw)
def default(self, obj):
if isinstance(obj, self.pass_through):
return self.mark_for_escape(obj)
elif hasattr(obj, '_id'):
return str(obj.id)
return super(TWEncoder, self).default(obj)
def encode(self, obj):
encoded = super(TWEncoder, self).encode(obj)
return self.unescape_marked(encoded)
def mark_for_escape(self, obj):
return '*#*%s*#*' % obj
def unescape_marked(self, encoded):
return encoded.replace('"*#*','').replace('*#*"', '')
encoder = None # This gets reset at the bottom of the file.
class js_symbol(object):
def __init__(self, name=None, src=None):
if name == None and src == None:
raise ValueError("js_symbol must be given name or src")
if name and src:
raise ValueError("js_symbol must not be given name and src")
if src != None:
self._name = src
else:
self._name = name
def __str__(self):
return str(self._name)
class js_callback(object):
@@ -53,40 +102,31 @@ class js_callback(object):
'function(){foo(1, 2, 3)}'
# A more realistic example
>> jQuery = js_function('jQuery')
>> my_cb = js_callback('function() { alert(this.text)}')
>> on_doc_load = jQuery('#foo').bind('click', my_cb)
>> call = jQuery(js_callback(on_doc_load))
>> print call
jQuery(function(){jQuery(\\"#foo\\").bind(\\"click\\", \
function() { alert(this.text)})})
jQuery(function(){jQuery(\\"#foo\\").bind(\\"click\\", function() { alert(this.text)})})
"""
def __init__(self, cb, *args):
warnings.warn('js_callback is being deprecated in future releases.',
DeprecationWarning)
if isinstance(cb, basestring):
self.cb = cb
elif isinstance(cb, js_function) or 'JSFuncCall' in repr(cb):
if args:
cbs = cb.req(args=args)
else:
cbs = cb.req()
cbs.prepare()
self.cb = "function(){%s}" % str(cbs)
elif isinstance(cb, js_function):
self.cb = "function(){%s}" % cb(*args)
elif isinstance(cb, _js_call):
self.cb = "function(){%s}" % cb
else:
self.cb = ''
def __call__(self, *args):
raise TypeError("A js_callback cannot be called from Python")
def __str__(self):
return self.cb
class js_function(object):
"""A JS function that can be "called" from python and and added to
a widget by widget.add_call() so it get's called every time the widget
@@ -108,7 +148,7 @@ class js_function(object):
If made at Widget initialization those calls will be placed in
the template for every request that renders the widget.
.. sourcecode:: python
>> from tw.api import Widget
@@ -122,7 +162,7 @@ class js_function(object):
... )
If we want to dynamically make calls on every request, we ca also add_calls
inside the ``prepare`` method.
inside the ``prepare`` method.
.. sourcecode:: python
@@ -134,12 +174,12 @@ class js_function(object):
... self.add_call(
... jQuery('#%s' % d.id).datePicker(d.pickerOptions)
... )
This would allow to pass different options to the datePicker on every
display.
JS calls are rendered by the same mechanisms that render required css and
js for a widget and places those calls at bodybottom so DOM elements which
JS calls are rendered by the same mechanisms that render required css and
js for a widget and places those calls at bodybottom so DOM elements which
we might target are available.
Examples:
@@ -158,10 +198,46 @@ class js_function(object):
"""
def __init__(self, name):
warnings.warn('js_function is being deprecated in future releases.' + \
'Please update your widgets to use JSFuncCall.',
DeprecationWarning)
self.__name = name
def __call__(self, *args):
return _js_call(self.__name, [], args, called=True)
class _js_call(object):
__slots__ = ('__name', '__call_list', '__args', '__called')
def __init__(self, name, call_list, args=None, called=False):
self.__name = name
self.__args = args
call_list.append(self)
self.__call_list = call_list
self.__called = called
def __getattr__(self, name):
return self.__class__(name, self.__call_list)
def __call__(self, *args):
return JSFuncCall(function=self.__name, args=args)
self.__args = args
self.__called = True
return self
def __get_js_repr(self):
if self.__called:
args = self.__args
return '%s(%s)' % (
self.__name,
', '.join(imap(encoder.encode, args))
)
else:
return self.__name
def __str__(self):
if not self.__called:
raise TypeError('Last element in the chain has to be called')
return '.'.join(c.__get_js_repr() for c in self.__call_list)
def __unicode__(self):
return str(self).decode(sys.getdefaultencoding())
encoder = TWEncoder()
@@ -180,8 +180,8 @@ def __init__(self, app, controllers=None, **config):
rl['queued_controllers'] = []
# Load up resources that wanted to be registered before we were ready
for modname, filename in rl.get('queued_resources', []):
self.resources.register(modname, filename)
for modname, filename, whole_dir in rl.get('queued_resources', []):
self.resources.register(modname, filename, whole_dir)
rl['queued_resources'] = []
Oops, something went wrong.

0 comments on commit 612d52a

Please sign in to comment.