Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

""" 

Python-JS interface to dynamically create JS function calls from your widgets. 

 

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 

**calls** can be generated programatically. 

""" 

import re 

import sys 

import six 

 

import logging 

from six.moves import map 

import json.encoder 

 

__all__ = ["js_callback", "js_function", "js_symbol", "encode"] 

 

log = logging.getLogger(__name__) 

 

 

class TWEncoder(json.encoder.JSONEncoder): 

    """A JSON encoder that can encode Widgets, js_calls, js_symbols and 

    js_callbacks. 

 

    Example:: 

 

        >>> 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 __init__(self, *args, **kw): 

        # This makes encoded objects be prettily formatted.  It is very nice 

        # for debugging and should be made configurable at some point. 

        # TODO -- make json encoding pretty-printing configurable 

        #kw['indent'] = '  ' 

 

        self.unescape_pattern = re.compile('"TW2Encoder_unescape_([0-9]*)"') 

        self.pass_through = (_js_call, js_callback, js_symbol, js_function) 

        super(TWEncoder, self).__init__(*args, **kw) 

 

        # This is required to get encoding of _js_call to work 

        self.namedtuple_as_object = False 

 

    def default(self, obj): 

        if isinstance(obj, self.pass_through): 

            result = self.mark_for_escape(obj) 

            return result 

 

        if hasattr(obj, '__json__'): 

            return obj.__json__() 

 

        if hasattr(obj, 'id'): 

            return str(obj.id) 

 

        return super(TWEncoder, self).default(obj) 

 

    def encode(self, obj): 

        self.unescape_symbols = {} 

        encoded = super(TWEncoder, self).encode(obj) 

        unescaped = self.unescape_marked(encoded) 

        self.unescape_symbols = {} 

        return unescaped 

 

        encoded = super(TWEncoder, self).encode(obj) 

        return self.unescape_marked(encoded) 

 

    def mark_for_escape(self, obj): 

        self.unescape_symbols[id(obj)] = obj 

        return 'TW2Encoder_unescape_' + str(id(obj)) 

 

    def unescape_marked(self, encoded): 

        def unescape(match): 

            obj_id = int(match.group(1)) 

            obj = self.unescape_symbols[obj_id] 

            return str(obj) 

 

        return self.unescape_pattern.sub(unescape, encoded) 

 

 

encoder = None  # This gets reset at the bottom of the file. 

 

 

class js_symbol(object): 

    """ An unquoted js symbol like ``document`` or ``window`` """ 

 

    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): 

    """A js function that can be passed as a callback to be called 

    by another JS function 

 

    Examples: 

 

    >>> str(js_callback("update_div")) 

    'update_div' 

 

    >>> str(js_callback("function (event) { .... }")) 

    'function (event) { .... }' 

 

    Can also create callbacks for deferred js calls 

 

    >>> str(js_callback(js_function('foo')(1,2,3))) 

    'function(){foo(1, 2, 3)}' 

 

    Or equivalently 

 

    >>> str(js_callback(js_function('foo'), 1,2,3)) 

    '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)})}) 

 

    """ 

    def __init__(self, cb, *args): 

        if isinstance(cb, six.string_types): 

            self.cb = cb 

        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 added to 

    a widget by widget.add_call() so it get's called every time the widget 

    is rendered. 

 

    Used to create a callable object that can be called from your widgets to 

    trigger actions in the browser. It's used primarily to initialize JS code 

    programatically. Calls can be chained and parameters are automatically 

    json-encoded into something JavaScript undersrtands. Example:: 

 

    >>> jQuery = js_function('jQuery') 

    >>> call = jQuery('#foo').datePicker({'option1': 'value1'}) 

    >>> str(call) 

    'jQuery("#foo").datePicker({"option1": "value1"})' 

 

    Calls are added to the widget call stack with the ``add_call`` method. 

 

    If made at Widget initialization those calls will be placed in 

    the template for every request that renders the widget. 

 

    >>> import tw2.core as twc 

    >>> class SomeWidget(twc.Widget): 

    ...     pickerOptions = twc.Param(default={}) 

    >>> SomeWidget.add_call( 

    ...     jQuery('#%s' % SomeWidget.id).datePicker(SomeWidget.pickerOptions) 

    ... ) 

 

    More likely, we will want to dynamically make calls on every 

    request.  Here we will call add_calls inside the ``prepare`` method. 

 

    >>> class SomeWidget(Widget): 

    ...     pickerOptions = twc.Param(default={}) 

    ...     def prepare(self): 

    ...         super(SomeWidget, self).prepare() 

    ...         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 

    we might target are available. 

 

    Examples: 

 

    >>> call = js_function('jQuery')("a .async") 

    >>> str(call) 

    'jQuery("a .async")' 

 

    js_function calls can be chained: 

 

    >>> call = js_function('jQuery')("a .async").foo().bar() 

    >>> str(call) 

    'jQuery("a .async").foo().bar()' 

 

    """ 

 

    def __init__(self, name): 

        self.__name = name 

 

    def __call__(self, *args): 

        return _js_call(self.__name, [], args, called=True) 

 

    def __str__(self): 

        return self.__name 

 

 

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): 

        self.__args = args 

        self.__called = True 

        return self 

 

    def __get_js_repr(self): 

        if self.__called: 

            args = self.__args 

            rep = '%s(%s)' % ( 

                self.__name, 

                ', '.join(map(encoder.encode, args)) 

            ) 

            return rep\ 

                    .replace('\\"', '"')\ 

                    .replace("\\'", "'")\ 

                    .replace('\\n', '\n') 

        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()