#!/usr/bin/env python # -*- coding: utf-8 -*- """ py2jquery (Python 2 jQuery) Developed by Thadeus Burgess License: GPL v2 Inspired by Nathan Freeze's Client Tools for web2py This is a set of classes and functions for managing client events and resources from a python server. """ import urllib import os import string from gluon.html import * from gluon.http import * from gluon.validators import * from gluon.sqlhtml import * __all__ = [ #GENERAL FUNCTIONS 'S', 'valid_filename', 'is_valid_selector', 'get_selector', #JAVASCRIPT GENERATORS 'embed', 'var', 'setvar', 'function', 'alert', 'ajax', 'jQuery', #SCRIPT CLASSES 'Manager', 'Var', 'VarDict', 'Script', 'Confirm', 'Delay', 'Interval', 'StopTimer', 'Call', 'Event', #WIDGETS 'Stars', ] __events__ = ['blur', 'focus', 'load', 'resize', 'scroll', 'unload', 'beforeunload', 'click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave', 'change', 'select', 'submit', 'keydown', 'keypress', 'keyup', 'error'] __events_live_notsupported__ = ['blur', 'focus', 'mouseenter', 'mouseleave', 'change', 'submit'] """#################################### GENERAL FUNCTIONS """#################################### def S(filename, request): return URL(r=request, c='static', f=filename) def valid_filename(filename): """ This checks to make sure the string is a valid filename that does not include restricted characters. Invalid characters are stripped. Returns str with only valid filename characters. """ import string valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits) return ''.join(c for c in filename if c in valid_chars) def is_valid_selector(obj, r=True): """ Checks an object to determine if it is a valid selector. Object must either be a string (css selector), Script instance, or object with an xml() function and _id attribute. Returns True/False """ if isinstance(obj, str) or \ isinstance(obj, Script) or \ (hasattr(obj, 'xml') and obj['_id']): return True elif r: if not hasattr(obj, 'xml'): raise TypeError('Invalid object for event. No XML Function.') if not obj['_id']: raise ValueError('Invalid object for event. No ID Attribute.') else: raise Error('Object must be Script, or string, or object with xml() function and id attribute.') else: return False def get_selector(obj, css=False): """ Returns the css selector for given object. Appends # if css is True """ selector = None if isinstance(obj, str): selector = obj elif isinstance(obj, Script): selector = obj.name else: selector = '%s' % (obj['_id']) if css: selector = '#' + selector return selector """#################################### JAVASCRIPT GENERATORS """#################################### def embed(path): """ Takes a path to file and returns a string for inclusion in XML Wraps path """ % path elif path.endswith(".css"): return LINK(_href=path, _rel="stylesheet", _type="text/css").xml() else: return path def var(name, value=None): """ Returns a javascript variable TODO: Add the ability to assign a javascript variable a value. """ js = "var %s" % name if value: if isinstance(value, int) or isinstance(value, float): js += ' = %s' % value else: js += ' = "%s"' % value js += ';' return js def setvar(name, value): """ Sets a javascript variable """ js = "%s = " % name if isinstance(value, int) or isinstance(value, float): js += '%s' % value else: js += '"%s"' % value js += ';' return js def function(name, xml): """ Wraps xml in javascript function with name """ return 'function %s(){%s};' % (name, xml) def alert(message): """ Returns javascript alert string. """ return 'alert("%s");' % message def ajax(type="POST", url="#", extra_data="", data="form:first", success="eval(msg);"): """ Returns jQuery.ajax() string with the parameters Success can be eval(msg); or the object of the html to replace. Extra data can be any string to include in the POST vars. """ if success != "eval(msg);": success = 'jQuery("%s").html(msg);' % success return 'jQuery.ajax({type:"%s", url:"%s", data:%s jQuery("%s").serialize(), '\ 'success: function(msg){%s} });' % (type, url, extra_data, data, success) class jQuery: """ A helper designed to reduce the amount of string substitutions when returning scripts. It won't handle complex scripts but can be useful to avoid nested qoutes and string substitutions. """ def __init__(self,name,attr=None,*args): self.name=name self.attr=attr self.args=args def __str__(self): import gluon.contrib.simplejson as json def encode(obj): if isinstance(obj,jQuery): return str(obj) return json.dumps(obj) if not self.attr: return ('jQuery("%s")' % self.name).replace('"this"',"this").replace('"document"',"document") args=', '.join([encode(a) for a in self.args]) return '%s.%s(%s)' % (self.name, self.attr, args) def __repr__(self): return str(self) def xml(self): raise AttributeError def __getattr__(self,attr): def f(*args): return JQuery(self,attr,*args) return f def __call__(self,*args): if not args: jq = str(JQuery(self)) jq = jq[8:-2] return jq + ";" def __add__(self,other): return str(self)+str(other) """#################################### SCRIPT CLASSES """#################################### class Var(object): """ Represents a JavaScript variable. name - Name of variable in javascript value - current value of variable. """ def __init__(self, name, value=None): self.name = name self.value = value def __str__(self): return self.name class VarDict(dict): """ Represents a dictionary of JavaScript variables. Allows you to reference the javascript variable by a python only name. This way you can have unique javascript variables. Example: vd = VarDict() vd.timer_id = Var('mytimer_uuid_timerid', 200) >>>vd {'timer_id': Var('mytimer_uuid_timerid', 200) >>>vd.timer_id 'mytimer_uuid_timerid' >>>vd.timer_id.value 200 """ def init(self, *args, **kwargs): dict.__init__(self, *args, **kwargs) def __getattr__(self, name): return self.__getitem__(name) def __setattr__(self, name, value): self.__setitem__(name, value) class Manager(object): """ The Manager object is used to dynamically include resources and scripts on a page. include - downloads a resource and/or adds a reference to it on the page google_load - adds a reference to a google hosted library example: manager.google_load("jqueryui", "1.7.2") more here: http://code.google.com/apis/ajaxlibs/ add - Adds a script to the page, the script may be a function, variable, or string. If on_ready is True, then the script or function call will be placed in the jQuery(document).on_ready(); event. xml - Renders all resources and scripts. If minify is False, it will add line breaks. Each time you call this function, it resets any added scripts. You can call this function as many times as you like. This way, you can call the script again at the end of your page, in case you need to add scripts from a view. """ def __init__(self, environment): self.environment = Storage(environment) self.resources = [] self.on_ready = [] self.scripts = [] self.misc = [] self.vars = VarDict() self.called = 0 def include(self, path, download=False, overwrite=False, subfolder=None): request = self.environment.request out = path if hasattr(path, 'xml'): out = path.xml() if download and path.startswith("http://"): if not request.env.web2py_runtime_gae: fname = valid_filename(path.split("/")[-1]) if len(fname): pieces = (request.folder, 'static', subfolder) fld = os.path.join(*(x for x in pieces if x)) if not os.path.exists(fld): os.mkdir(fld) fpath = os.path.join(fld,fname) exists = os.path.exists(fpath) if (not exists) or (exists and overwrite): urllib.urlretrieve(path, fpath) out = URL(r=request,c='/'.join( x for x in ['static', subfolder] if x),f=fname) self.resources.append(out) def google_load(self, lib, version): gsapi = '' if not gsapi in self.resources: self.include(gsapi) self.include('' % \ (lib,version)) def add(self, script, on_ready=False): if isinstance(script, Script): self.scripts.append(script) if on_ready: self.on_ready.append(script.name) elif isinstance(script, str): self.misc.append(script) else: raise TypeError('Must be of class Script') def add_var(self, var): if not isinstance(var, Var): raise TypeError('Invalid type. var must be of type Var') raise Error('Not yet implemented.') def xml(self, minify=True): xml = "" var_xml = [] functions = [] on_ready = [] misc = self.misc if minify: minify = '' else: minify = '\n' for r in self.resources: xml += embed(r) xml += minify for script in self.scripts: if script.is_function: functions.append(script.xml()) if script.name in self.on_ready: on_ready.append(script.name + '();') for v in script.var.values(): var_xml.append(var(v.name, v.value)) elif script.name in self.on_ready: on_ready.append(script.xml()) else: misc.append(script.xml()) for v in self.vars.values(): var_xml.append(var(v.name, v.value)) page_on_ready = "page_on_ready_%d" % self.called xml += '''''' self.resources = [] self.on_ready = [] self.scripts = [] self.called += 1 return XML(xml) class Script(object): """ A base script class. txt - Optional txt value, useful to turn a str that is javascript into a instance of Script. uuid - Unique identifier for this script. This is required if you are using multiple subscriptions to an event/object. is_function - If this is True, the xml output will be wrapped in function self.name(){self.xml()} wrap - Adds function to a string if is_function dewrap - Returns the call to the script, if a function () is appended, else just the text. vars - Represents variables in javascript. example >>>var['timer_id'] = 24 'var timer_id = 24;' Returns defaults for variables needed in javascript (such as timer_id) xml - Renders the script. """ def __init__(self, txt='', uuid='', is_function=False, **kwargs): self.txt = txt self.uuid = uuid self.is_function = is_function self.name = "%s__script" % uuid self.var = VarDict() def __str__(self): return self.xml() def wrap(self, xml): if self.is_function: xml = function(self.name, xml) return xml def dewrap(self, script): if isinstance(script, str): return script elif script.is_function: return '%s();' % (script.name) else: return script.xml() def xml(self): xml = self.txt xml = self.wrap(xml) return xml class Confirm(Script): """ Creates a confirm dialog. message - message to display in dialog if_ok - Script to run if ok is selected if_cancel - Script to run if cancel is selected """ def __init__(self, message='', if_ok='', if_cancel='', **kwargs): Script.__init__(self, **kwargs) self.name = '%s__confirm' % self.uuid self.var.answer = Var(self.name + "__answer") if not isinstance(if_ok, Script) and not isinstance(if_ok, str): raise TypeError('if_ok must be type Script or string') if not isinstance(if_cancel, Script) and not isinstance(if_cancel, str): raise TypeError('if_cancel must be type Script or string') self.message = message self.if_ok = if_ok self.if_cancel = if_cancel def xml(self): xml = '' if_ok_xml = self.dewrap(self.if_ok) if_cancel_xml = self.dewrap(self.if_cancel) xml += '%s = confirm("%s");' % (self.var.answer, self.message) xml += 'if(%s==true){%s}else{%s}' % (self.var.answer, if_ok_xml, if_cancel_xml) xml = self.wrap(xml) return xml class Delay(Script): """ Adds a timeout timer to the page. Delays the execution of Script until the specified timeout. script - Script to execute when timer ends. timeout - Time in ms to delay execution. global var delay%s_timerid (self.uuid) """ def __init__(self, script, timeout, **kwargs): Script.__init__(self, **kwargs) if not isinstance(script, Script): raise TypeError('script must be of instance Script.') self.script = script self.timeout = int(timeout) self.name = "%s__delay" % (self.uuid) self.var.timer_id = Var("%s__timerid" % (self.name)) def xml(self): xml = '' script_xml = self.dewrap(self.script) xml += "%s = setTimeout('%s', %s)" % (self.var.timer_id, script_xml, self.timeout) xml = self.wrap(xml) return xml class Interval(Delay): """ Adds a interval timer to the page. Executes Script at each specified interval script - Script to execute at each interval timeout - Time in ms between executions. global var interval%s_timerid (self.uuid) """ def __init__(self, *args, **kwargs): Delay.__init__(self, *args, **kwargs) self.name = "%s__interval" % (self.uuid) self.var.timer_id = Var("%s_timerid" % self.name) def xml(self): xml = '' script_xml = self.dewrap(self.script) xml += "%s = setInterval('%s', %s);" % (self.var.timer_id, script_xml, self.timeout) xml = self.wrap(xml) return xml class StopTimer(Script): """ Stops a running timer by using its timer_id timer - Delay or Interval instance """ def __init__(self, timer, **kwargs): Script.__init__(self, **kwargs) if not isinstance(timer, Delay): raise TypeError('script must be of type Delay or Interval.') self.timer = timer self.name = "%s__stop" % (self.uuid) def xml(self): xml = '' xml += 'clearTimeout(%s);' % (self.timer.var.timer_id) xml = self.wrap(xml) return xml class Call(Script): """ Makes an AJAX request to the server with jQuery. callback - serverside function to call args - arguments to function success - if specified, replaces dom with server response html. Else it evaluates the response as javascript data - form data to add to POST extra_data - any other data that should be appended to the POST """ def __init__(self, callback=None, args=[], success="eval(msg);", data="form:first", extra_data="", **kwargs): Script.__init__(self, **kwargs) self.callback = callback self.args = args if not isinstance(callback, str): if not hasattr(callback, '__call__'): raise TypeError('Invalid function. Object not callable') if kwargs['request']: request = kwargs['request'] elif not self.manager: raise AttributeError('Call must be supplied either a reference to a request object or Manager object.') self.url = URL(r=request, f=callback.__name__, args=args) else: self.url = callback if is_valid_selector(success): self.success = success if is_valid_selector(data): self.data = data self.extra_data = extra_data if isinstance(self.callback, str): n = self.callback.split('/')[-1] else: n = self.callback.__name__ self.name = '%s__call%s' % (n, self.uuid) def xml(self): xml = "" xml += ajax(type="POST", url=self.url, extra_data=self.extra_data, data=self.data, success=self.success) xml = self.wrap(xml) return xml class Event(Script): """ Listens for an event on the selected object. Calls a script when event is detected. event - jQuery event type, must be in in __events__. event_obj - object or css selector to listen for event on. call - script to execute when event is detected. event_args - if True, returns arguments for event ['event_target_id', 'event_target_html', 'event_pageX', 'event_pageY', 'event_timeStamp'] rebind - if True, it will use jQuery.live() else it will use jQuery.bind() """ def __init__(self, event="click", event_obj=None, call=None, extra_data="", event_args=False, rebind=False, **kwargs): Script.__init__(self, **kwargs) if event in __events__: self.event = event else: raise ValueError('Invalid event name.') if is_valid_selector(event_obj): self.event_obj = event_obj if isinstance(call, Script): self.call = call else: raise TypeError('Invalid call object. Must be instance of Script.') call.extra_data = extra_data if event_args: call.extra_data += '"event_target_id=" + encodeURIComponent(e.target.id) + "&event_target_html=" + '\ 'encodeURIComponent(jQuery(e.target).wrap("
").parent().html()) + '\ '"&event_pageX=" + e.pageX + "&event_pageY=" + e.pageY + '\ '"&event_timeStamp=" + e.timeStamp + "&" +' if rebind: self.bind = 'live' if self.event in __events_live_notsupported__: raise ValueError('Event not supported for jQuery.live()') else: self.bind = 'bind' self.name = '%s__%s%s' % (get_selector(self.event_obj), self.event, self.uuid) def xml(self): xml = "" call_xml = self.dewrap(self.call) xml += 'jQuery("%s").%s("%s", function(e){%s});' % (get_selector(self.event_obj, True), self.bind, self.event, call_xml) xml = self.wrap(xml) return xml """#################################### WIDGETS """#################################### class Stars(object): """ A class for implementing a star rating widget. value - Current value for this set of stars. call - Script to call when star is clicked. num - Number of stars uuid - REQUIRED name for this set of stars. dom - Renders all required HTML and JavaScript events for this set of stars js - Returns JavaScript to alter the src attribute of the images in this star set. Example: def stars(): callback = Call(rate_stars, request=request) # OR Call(URL(r=request, f='rate_stars', args='') # In case args need to be passed along as well. # Create blank set of stars stars = Stars(-1, callback, uuid='mystars') # Generate the stars, you may need to call XML() on this. return dict(stars=stars.dom(manager)) def rate_stars(): # Passed from POST uuid = request.vars.star_uuid val = request.vars.star_val # Notice we are using the same uuid, and no callback. # Set the value to the selected star. stars = Stars(int(val), uuid=uuid) # Renders JavaScript that will get eval'd return stars.js(request) """ full = 'images/star_full.gif' half = 'images/star_half.gif' void = 'images/star_void.gif' vote = 'images/star_vote.gif' def __init__(self, value, call=None, num=5, uuid=''): if not isinstance(value, int): raise TypeError('Value must be integer') if not isinstance(call, Call) and call != None: raise TypeError('Invalid call object. Must be instance of Script.') if uuid == '': raise ValueError('Must be assigned a uuid.') self.value = value self.call = call self.num = num self.uuid = uuid self.name = "stars%s" % (self.uuid) def dom(self, manager): if not isinstance(manager, Manager): raise TypeError('manager must be instance of Manager') if not isinstance(self.call, Call): raise TypeError('Invalid call object. Must be instance of Script.') r = manager.environment.request dom = "" stars = [] for i in range(1, self.num + 1): if i <= self.value: img = Stars.full if i > self.value: img = Stars.void id = "%s_%d" % (self.name, i) sdom = IMG(_src=S(img, r), _id=id) stars.append(sdom) dom += "%s" % sdom js_var = "%s_src_val" % id extdata = '"star_uuid=%s&star_val=%d&"+' % (self.uuid, i) call = Call(callback=self.call.url, args=self.call.args, success=self.call.success, data=self.call.data, extra_data=self.call.extra_data, uuid=self.call.uuid, name=self.call.name, is_function=self.call.is_function, ) click = Event("click", sdom, call, extra_data=extdata) mouseover = "" mouseout = "" for star in stars: mouseover += 'jQuery("#%s").attr("src", "%s");' % (star['_id'], S(Stars.vote, r)) mouseout += 'jQuery("#%s").attr("src", %s);' % (star['_id'], "%s_src_val" % star['_id']) mouseover = Event("mouseover", sdom, Script(mouseover), is_function=True) mouseout = Event("mouseout", sdom, Script(mouseout), is_function=True) manager.vars[js_var] = Var(js_var, star['_src']) manager.add(mouseover, True) manager.add(mouseout, True) manager.add(click, True) return dom def js(self, request): js = "" for i in range(1, self.num + 1): if i <= self.value: img = S(Stars.full, request) if i > self.value: img = S(Stars.void, request) id = "%s_%d" % (self.name, i) js_var = "%s_src_val" % id js += setvar(js_var, "%s" % img) js += 'jQuery("#%s").attr("src", "%s");' % (id, img) return js