#!/usr/bin/env python
import pyflag.HTMLUI as HTMLUI
import pyflag.DB as DB
import pyflag.conf
import pyflag.pyflaglog as pyflaglog
import pyflag.FlagFramework as FlagFramework
config=pyflag.conf.ConfObject()
import time,re
import pyflag.TableObj as TableObj
import pyflag.parser as parser
entities = { " ": " ", "<":"<", ">":">", "&":"&" }
def escape_entities(data):
for k,v in entities.items():
data = data.replace(v,k)
return data
def unescape_entities(data):
for k,v in entities.items():
data = data.replace(k,v)
return data
class AJAXUI(HTMLUI.HTMLUI):
""" An AJAX driven web framework for PyFlag """
preamble=''
def __init__(self,default = None,query=None):
HTMLUI.HTMLUI.__init__(self, default,query)
self.floats = []
def __str__(self):
## Ensure that floats occur _after_ everything else - this is
## required if they need to have forms later:
result = HTMLUI.HTMLUI.__str__(self)
if self.floats:
result += "\n".join(self.floats)
return result
def const_selector(self,description,name,keys,values,**options):
if options:
opt_str = self.opt_to_str(options)
else: opt_str = ''
## Convert the keys and values to json:
def const_selector_cb(query,result):
out = [ [k,v] for k,v in zip(keys,values) ]
result.decoration='raw'
result.result = "%s" % out
cb = self.store_callback(const_selector_cb)
try:
default = self.defaults[name]
except:
default = ''
tmp = '\n' % (name,opt_str,cb, default);
#Remove this from the form_parms
if self.form_parms.has_key(name):
del self.form_parms[name]
#Draw in a nice table format
self.row(description,tmp)
def notebook(self,names=[],context="notebook",callbacks=[],
descriptions=[], callback_args=[]):
""" Draw a notebook like UI with tabs.
If no tab is selected, the first tab will be selected.
@arg names: A list of names for each tab
@arg callbacks: A list of callbacks to call for each name
@arg context: A context variable used to allow the selection of names in queries
@arg descriptions: A list of descriptions to assign to each tab. The description should not be longer than 1 line.
"""
query=self.defaults.clone()
out=''
selectedTab = None
for i in range(len(names)):
id = "PagePane%s" % self.get_unique_id()
if selectedTab==None: selectedTab=id
try:
if query['mode']==names[i]:
selectedTab = id
except:
pass
new_query = query.clone()
new_query['callback_stored'] = self.store_callback(callbacks[i])
out+='''
''' % (selectedTab,out)
def tree(self, tree_cb = None, pane_cb=None, branch = None, layout=None):
""" A tree widget.
This implementation uses javascript/iframes extensively.
"""
id = self.get_unique_id()
def right(query,result):
result.decoration = "raw"
result.content_type = "text/html"
try:
path=FlagFramework.normpath(query['open_tree'])
except KeyError:
path='/'
pane_cb(path,result)
def tree(query,result):
result.decoration = "raw"
result.content_type = "text/html"
## I think this is secure enough??? This should really be
## json.parse but do we need to pull in a whole module
## just for this???
data = eval(query['data'],{'__builtins__':None, 'true':True, 'false':False})
path=FlagFramework.normpath(data['node']['objectId'])
r=[]
for x in tree_cb(path):
if not x[0] or len(x[0])==0: continue
tmp = dict(title = x[0], objectId="/%s/%s" % (path,x[1]))
if x[2]=='branch':
tmp['isFolder']='true'
else:
tmp['isFolder']='false'
r.append(tmp)
result.result=r
t=self.store_callback(tree)
r=self.store_callback(right)
query = self.defaults.clone()
## Calculate the default tree structure which is obtained from query['open_tree']
try:
branch = FlagFramework.splitpath(self.defaults['open_tree'])
except KeyError:
branch = ['']
def do_node(depth,path):
if depth>=len(branch): return ''
result=''
for x in tree_cb('/'+'/'.join(branch[:depth])):
if len(x[0])==0: continue
if not x[1]: continue
if x[2]=='branch':
isFolder='true'
else:
isFolder='false'
children=''
opened='0'
if x[1]==branch[depth]:
children = do_node(depth+1,path+'/'+branch[depth])
opened='1'
result+='
%s
' % (isFolder,x[0],'/'.join((path,x[1])),opened,children)
return result
tree_nodes='
%s
' % do_node(0,'')
## Calculate the initial contents of the right pane:
right_ui = self.__class__(self)
right(query,right_ui)
del query['open_tree']
self.result+="""
%(tree_nodes)s
%(right_pane)s
""" % {'query':query,'t':t,'id':id, 'r':r, 'right_pane': right_ui,
'tree_nodes':tree_nodes}
## Populate the initial tree state: FIXME: This needs to be a
## lot more specific.
self.result+="""
""" % {'r':r, 'query':query, 'id':id }
def start_form(self,target, **hiddens):
""" start a new form with a local scope for parameters.
@arg target: A query_type object which is the target to the form. All parameters passed through this object are passed to the form's action.
"""
self.form_parms=target.clone()
self.form_id=self.get_unique_id()
try:
self.form_target = hiddens['pane']
del hiddens['pane']
except KeyError:
self.form_target = 'self'
#Append the hidden params to the object
for k,v in hiddens.items():
self.form_parms[k]=v
self.result += '"
def filter_string(self,filter_str):
## Remove any HTML tags which may be present:
filter_str = re.sub("<[^>]*>",'',filter_str)
## Unescape any entities:
return unescape_entities(filter_str)
## This is a re-implementation of the table widget.
def table(self,elements=[],table='',where='',groupby = None,case=None, **opts):
""" The Table widget.
In order to create a table, we need to accept a list of elements. The elements are objects derived from the ColumnType class:
result.table(
elements = [ ColumnType(name = 'TimeStamp',
sql = 'from_unixtime(time)',
link = query),
ColumnType('Data', 'data'),
]
table = 'TestTable',
)
"""
id = self.get_unique_id()
def table_cb(query,result):
## Building up the args list in this way ensure that defaults
## can be specified in _make_sql itself and not be overwritten
## by our defaults.
try:
order = int(query.get('order',0))
except: order=0
try: limit = int(query.get('limit',0))
except: limit = 0
args = dict( elements = elements, table = table, case=case,
groupby = groupby, order = order, limit = limit)
if where: args['where'] = where
try: args['filter'] = self.filter_string(query['filter'])
except: pass
try: args['direction'] = query['direction']
except: pass
sql = self._make_sql(**args)
print sql
result.result+='''
''' % (id)
## Make the table headers with suitable order by links:
for e in range(len(elements)):
new_query = query.clone()
n = elements[e].name
if order==e:
if query.get('direction','1')=='1':
del new_query['direction']
del new_query['order']
result.result+="
%s
\n" % (n,id, new_query,e, n)
else:
del new_query['direction']
del new_query['order']
result.result+="
%s
\n" % (n,id, new_query,e,n)
else:
del new_query['order']
del new_query['direction']
result.result+="
%s
\n" % (n,id, new_query,e,n)
result.result+='''
'''
## Now do the rows:
dbh = DB.DBO(case)
dbh.execute(sql)
old_sorted = None
old_sorted_style = ''
## Total number of rows
row_count=0
for row in dbh:
row_elements = []
tds = ''
## Render each row at a time:
for i in range(len(elements)):
## Give the row to the column element to allow it
## to translate the output suitably:
value = elements[i].display(row[elements[i].name],row,self)
## Render the row styles so that equal values on
## the sorted column have the same style
if i==order and value!=old_sorted:
old_sorted=value
if old_sorted_style=='':
old_sorted_style='alternateRow'
else:
old_sorted_style=''
## Render the sorted column with a different style
if i==order:
tds+="
"
new_id = self.get_unique_id()
## Now we add the paging toolbar icons
## The next button allows user to page to the next page
if row_count
%s%s
''' % (previous_button, next_button)
return
cb=self.store_callback(table_cb)
self.result += '''
\n''' % {'id':id}
## This callback will render the filter GUI popup. There is some raw
## javascript in here to make life a little easier.
def filter_gui(query, result):
result.heading("Filter Table")
try:
filter_str = self.filter_string(query['filter'])
result.para(filter_str)
## Check the current filter string for errors by attempting to parse it:
try:
sql = parser.parse_to_sql(filter_str,elements)
## This is good if we get here - lets refresh to it now:
if query.has_key('__submit__'):
result.refresh(0,query,pane='parent')
return
except Exception,e:
result.text('Error parsing expression: %s' % e, color='red')
result.text('\n',color='black')
except KeyError:
pass
result.start_form(query, pane="self")
result.textarea("Search Query", 'filter')
result.result += """
The following can be used to insert text rapidly into the search string
""" % "\n".join(["" % (e.name,e.name) for e in elements])
## Round up all the possible methods from all colmn types:
operators = {}
for e in elements:
for method in e.operators():
operators[method]=1
methods = operators.keys()
methods.sort()
result.result+="""
""" % "\n".join(["" % (m,m) for m in methods])
result.end_form()
## Add a toolbar icon for the filter:
self.toolbar(toolbar="tabletoolbar%s" % id,
cb=filter_gui, pane='popup', icon='filter.png'
)
## Update the table with its initial view
self.result+='''''' % (id, self.defaults,cb)
def xxxtable(self,sql="select ",columns=[],names=[],links=[],types={},table='',where='',groupby = None,case=None,callbacks={}, **opts):
names = list(names)
columns = list(columns)
id=self.get_unique_id()
def table_cb(query,result):
""" This callback is used to render the actual table in
its requested pane
"""
menus = []
new_id = self.get_unique_id()
## May only offer to group by if the report does not issue
## its own
if not groupby:
if query.has_key("group_by"):
q=query.clone()
del q['group_by']
menus.append('\n' % (id,q))
else:
pane = result._calculate_js_for_pane(None, None, "self")
menus.append('\n' % (id,pane))
menus.append('\n' % id)
menus.append('\n')
## Now present the user with options of removing conditions:
having=[]
for d,v in query:
if d.startswith('where_'):
#Find the column for that name
try:
index=names.index(d[len('where_'):])
except ValueError:
## If we dont know about this name, we ignore it.
continue
condition_text = FlagFramework.make_sql_from_filter(v, having, columns[index],d[len('where_'):])
q=query.clone()
q.remove(d,v)
menus.append('\n' % (condition_text,id,q))
result.result +='''
%s
''' % (id,id, id,''.join(menus))
## If no ordering is specified we order by the first column
if not query.has_key('order') and not query.has_key('dorder'):
query['order']=names[0]
order = names[0]
try:
limit = int(query['limit'])
except:
limit = 0
## Clean up the filter string if possible:
try:
print "cleaning out filter string %s" % query['filter']
filter_str = query['filter']
del query['filter']
query['filter'] = self.filter_string(filter_str)
print "Got %s" % query['filter']
except KeyError:
pass
dbh,new_query,new_names,new_columns,new_links = self._make_sql(
sql=sql,
columns=columns,
names=names,
links=links,
table=table,
where=where,
groupby=groupby,
case=case,
callbacks=callbacks,
limit = limit,
types = types,
query=query)
if not new_query.has_key('callback_stored'):
new_query['callback_stored'] = cb
result.result+='''
''' % (id, new_query)
## Now make the table headers:
for n in new_names:
try:
if query['dorder']==n:
result.result+="
%s
\n" % (n,id, new_query,n,n)
order = query['dorder']
self.tooltip("th_%s" % n, "Sort by %s" % n)
continue
except KeyError:
try:
if query['order']==n:
result.result+="
%s
\n" % (n,id, new_query,n,n)
order = query['order']
self.tooltip("th_%s" % n, "Reverse sort by %s" % n)
continue
except KeyError:
pass
result.result+="
'''
## Now the contents:
old_sorted = None
old_sorted_style = ''
## Total number of rows
row_count=0
for row in dbh:
row_elements = []
tds = ''
## Render each row at a time:
for i in range(len(new_names)):
value = row[new_names[i]].__str__()
## Check if the user specified a callback for this column
if callbacks.has_key(new_names[i]):
value=callbacks[new_names[i]](value)
else:
## Sanitise the value to make it HTML safe. Note that
## callbacks are required to ensure they sanitise
## their output if they need.
value=escape_entities(value)
## If the value is the same as above we do not need to flip it:
if new_names[i]==order and value!=old_sorted:
old_sorted=value
if old_sorted_style=='':
old_sorted_style='alternateRow'
else:
old_sorted_style=''
## Now add links if they are required
try:
if new_links[i]:
q = new_links[i]
try:
q=q.clone()
q.FillQueryTarget(value)
#No __target__ specified go straight here
finally:
tmp = self.__class__(self)
tmp.link(value, q)
value=tmp
#links array is too short
except IndexError:
pass
if value==' ': value=" "
if new_names[i]==order:
tds+="
"
## Add the various toolbar icons:
## The next button allows user to page to the next page
if row_count
%s%s
''' % (previous_button, next_button)
cb=self.store_callback(table_cb)
self.result += '''
\n''' % {'id':id}
## Work out the types:
for n in names:
if not types.has_key(n):
types[n] = TableObj.ColumnType()
## This callback will render the filter GUI popup. There is some raw
## javascript in here to make life a little easier.
def filter_gui(query, result):
result.heading("Filter Table")
try:
filter_str = self.filter_string(query['filter'])
result.para(filter_str)
## Check the current filter string for errors by attempting to parse it:
try:
sql = parser.parse_to_sql(filter_str,types)
## This is good - lets refresh to it now:
if query.has_key('__submit__'):
result.refresh(0,query,pane='parent')
return
except Exception,e:
result.text('Error parsing expression: %s' % e, color='red')
result.text('\n',color='black')
except KeyError:
pass
result.start_form(query, pane="self")
result.textarea("Search Query", 'filter', cols=60, rows=5)
result.result += """
The following can be used to insert text rapidly into the search string
""" % "\n".join(["" % (n,n) for n in names])
## Round up all the possible methods from all colmn types:
operators = {}
for t in types.values():
for method in t.operators():
operators[method]=1
methods = operators.keys()
methods.sort()
result.result+="""
""" % "\n".join(["" % (m,m) for m in methods])
result.end_form()
## Add a toolbar icon for the filter:
self.toolbar(toolbar="tabletoolbar%s" % id,
cb=filter_gui, pane='popup', icon='filter.png'
)
self.result+='''''' % (id, self.defaults,cb)
def _calculate_js_for_pane(self, element_id=None, target=None, pane="'main'"):
""" Returns the JS string required to facilitate opening in the requested pane
Modifies query to remove stored callbacks if needed.
element_id: The ID of the element we are trying to
create. This will be used to calculate the container we are
in, if that was not supplied.
target: The query we should link to. We will delete callbacks from it if needed.
pane: Where we want to open the link can be:
main (default): refresh to the main pane (default).
parent: refresh to the pane that contains this pane. (useful for popups etc).
popup: open a new popup window and draw the target in that.
self: refresh to the current pane (useful for internal links in popups etc).
"""
## Open to the container we live in
if pane=='parent':
if target:
target.poparray('callback_stored')
if self.defaults.has_key("__pane__"):
pane = "find_widget_type_above('ContentPane',%r)" % self.defaults['__pane__']
else:
pane = '"main"'
# open to the current container:
elif pane=='self':
if self.defaults.has_key("__pane__"):
pane = "'%s'" % self.defaults['__pane__']
elif element_id:
pane = "find_widget_type_above('ContentPane',%r)" % element_id
else:
pane="'main'"
elif pane=='main':
if target:
target.poparray('callback_stored')
pane = "'main'"
elif pane=='popup':
popup_id = self.get_unique_id()
self.add_to_top_ui('''''' % (popup_id))
pane = "'float%s'" % (popup_id)
return pane
def link(self,string,target=None,options=None,icon=None,tooltip='',pane='main', **target_options):
""" The user can specify which pane the link will open in by using the pane variable:
pane can be:
main (default): refresh to the main pane (default).
parent: refresh to the pane that contains this pane. (useful for popups etc).
popup: open a new popup window and draw the target in that.
self: refresh to the current pane (useful for internal links in popups etc).
"""
## If the user specified a URL, we just use it as is:
try:
self.result+="%s" % (target_options['url'],string)
return
except KeyError:
pass
## The target query can over ride the pane specification
try:
pane = target['__targetpane__']
del target['__targetpane__']
except:
pass
if target==None:
target=FlagFramework.query_type(())
if not options:
options={}
q=target.clone()
if target_options:
for k,v in target_options.items():
del q[k]
q[k]=v
pane = self._calculate_js_for_pane("Link%s" % self.id, target=q, pane=pane)
if icon:
tmp = self.__class__(self)
tmp.icon(icon,tooltip=string+tooltip,border=0)
string=tmp
if pane=='popup':
def popup_cb(query, result):
self.refresh(0, target)
self.popup(popup_cb, string, icon=icon, tooltip=tooltip)
return
else:
## This has a valid href so that it is possible to right
## click and open in new tab or save the link in a normal
## bookmark
base = '%s' % (self.opt_to_str(options),self.id, pane, q, q, string)
if tooltip:
self.tooltip("Link%s" % self.id, tooltip)
self.result+=base
def icon(self, path, tooltip=None, **options):
id = self.get_unique_id()
option_str = self.opt_to_str(options)
self.result += "" % (id, path, option_str)
if tooltip:
self.tooltip("img%s" % id, tooltip)
def _dojo_delayed_execution(self,string):
self.result+='''''' % string
def new_toolbar(self):
""" Creates a new toolbar in the current UI to allow private
buttons to be added to it
Returns the toolbar ID which may be used as an option for
toolbar().
"""
id = "Toolbar%s" % self.get_unique_id()
self.result+='''
'''
self.result+='''
''' % dict(id=id)
self.add_to_top_ui("
")
return id
def toolbar(self,cb=None,text='',icon=None,popup=True,tooltip='',
link=None, pane="'main'", toolbar="toolbar"):
""" Create a toolbar button.
When the user clicks on the toolbar button, a popup window is
created which the callback function then uses to render on.
pane specifies the target of the toolbar's action:
main (default): refresh to the main pane (default).
parent: refresh to the pane that contains this pane. (useful for popups etc).
popup: open a new popup window and draw the target in that.
self: refresh to the current pane (useful for internal links in popups etc).
toolbar is the name of the toolbar we want to add to. Its normally left as the default main toolbar.
"""
id = self.id
## Find out the value of the current container we are at
try:
container = "'%s'" % self.defaults['__pane__']
except:
container = "'main'"
## We delay execution to add_toolbar* functions in case a
## local toolbar was created
if link:
pane = self._calculate_js_for_pane(target=link, pane=pane)
self._dojo_delayed_execution("add_toolbar_link('/images/%s','f?%s',%s, %s, 'toolbarbutton%s', %r);" % (icon, link, pane, container, id, toolbar))
elif cb:
cb_key = self.store_callback(cb)
target = self.defaults.clone()
#target.poparray('callback_stored')
target['callback_stored'] = cb_key
pane = self._calculate_js_for_pane(target=target, pane=pane)
self._dojo_delayed_execution("add_toolbar_link('/images/%s','f?%s',%s, %s, 'toolbarbutton%s', %r);" % (icon, target, pane, container, id, toolbar))
## Button is disabled:
else:
pane = self._calculate_js_for_pane(pane=pane)
self._dojo_delayed_execution("add_toolbar_disabled('/images/%s',%s, %s, %r);" % (icon, pane, container, toolbar))
## FIXME: This needs to be done using js so it can be delayed
## until the delayed execution clauses are done.
# if tooltip or text:
# self.tooltip("toolbarbutton%s" % id, tooltip+text)
def download(self,file):
def Download_file(query,result):
magic=FlagFramework.Magic(mode='mime')
file.seek(0)
data=file.read(1000)
result.generator.content_type=magic.buffer(data)
try:
result.generator.headers=[("Content-Disposition","attachment; filename=%s" % file.inode),]
except AttributeError:
result.generator.headers=[("Content-Disposition","attachment; filename=%s" % file.name),]
file.seek(0)
result.generator.generator=file
cb=self.store_callback(Download_file)
self.result = "Click to Download file" % (self.defaults,cb)
def refresh(self,interval,query, pane='self', **options):
""" Refreshes the given content pane into the specified query in a certain time.
if interval is 0 we do it immediately.
"""
pane = self._calculate_js_for_pane(None, query, pane)
## Do we want to do this immediately?
if interval==0:
self.result+="""""" % (pane, query)
else:
## We mark the current container as pending an update, and
## then schedule an update to it later on. If it has been
## updated by some other mechanism, it will be marked as
## not longer pending, and we ignore it.
self.result+="""""" % (pane,query, interval*1000)
def add_to_top_ui(self, data):
s = self
while s.parent:
s=s.parent
s.floats.append(data)
def tooltip(self, widget, text):
""" Inserts a tooltip on a widgetId. Mostly used from within AJAXUI """
self.add_to_top_ui('''%s\n''' % (widget, text))
def popup(self,callback, label,icon=None,toolbar=0, menubar=0, tooltip=None, **options):
if not tooltip: tooltip = label
image_id = self.get_unique_id()
cb = self.store_callback(callback)
self.add_to_top_ui('''''' % (image_id,tooltip))
if icon:
label = "" % (label, icon)
if tooltip:
self.tooltip("popup%s" % image_id, tooltip)
self.result+='''%s\n''' % (image_id,image_id, "%s&callback_stored=%s" % (self.defaults,cb), label)
def date_selector(self, description, variable):
try:
date = self.defaults[variable]
except KeyError:
date = time.strftime("%Y-%m-%d")
text = '\n' % (date,variable)
self.row(description, text)
## And remove if from the form
if self.form_parms.has_key(variable):
del self.form_parms[variable]
def wizard(self,names=[],context="wizard",callbacks=[],title=''):
tmp = []
for i in range(len(names)):
tmp.append('' % (i,names[i]))
self.result+='''
%s
''' % '\n'.join(tmp)
cb = [ self.store_callback(c) for c in callbacks ]
self.result+='''''' % (self.defaults, cb[0])
def textarea(self,description,name, **options):
""" Draws a text area with the default content
This is very similar to the textfield above.
"""
try:
default = self.sanitise_data(self.defaults[name])
except (KeyError,AttributeError):
default =''
## And remove if from the form
if self.form_parms.has_key(name):
del self.form_parms[name]
# option_str = self.opt_to_str(options)
left = description
right='''