appadmin.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. # -*- coding: utf-8 -*-
  2. # ##########################################################
  3. # ## make sure administrator is on localhost
  4. # ###########################################################
  5. import os
  6. import socket
  7. import datetime
  8. import copy
  9. import gluon.contenttype
  10. import gluon.fileutils
  11. try:
  12. import pygraphviz as pgv
  13. except ImportError:
  14. pgv = None
  15. is_gae = request.env.web2py_runtime_gae or False
  16. # ## critical --- make a copy of the environment
  17. global_env = copy.copy(globals())
  18. global_env['datetime'] = datetime
  19. http_host = request.env.http_host.split(':')[0]
  20. remote_addr = request.env.remote_addr
  21. try:
  22. hosts = (http_host, socket.gethostname(),
  23. socket.gethostbyname(http_host),
  24. '::1', '127.0.0.1', '::ffff:127.0.0.1')
  25. except:
  26. hosts = (http_host, )
  27. if request.is_https:
  28. session.secure()
  29. elif (remote_addr not in hosts) and (remote_addr != "127.0.0.1") and \
  30. (request.function != 'manage'):
  31. raise HTTP(200, T('appadmin is disabled because insecure channel'))
  32. if request.function == 'manage':
  33. if not 'auth' in globals() or not request.args:
  34. redirect(URL(request.controller, 'index'))
  35. manager_action = auth.settings.manager_actions.get(request.args(0), None)
  36. if manager_action is None and request.args(0) == 'auth':
  37. manager_action = dict(role=auth.settings.auth_manager_role,
  38. heading=T('Manage Access Control'),
  39. tables=[auth.table_user(),
  40. auth.table_group(),
  41. auth.table_permission()])
  42. manager_role = manager_action.get('role', None) if manager_action else None
  43. if not (gluon.fileutils.check_credentials(request) or auth.has_membership(manager_role)):
  44. raise HTTP(403, "Not authorized")
  45. menu = False
  46. elif (request.application == 'admin' and not session.authorized) or \
  47. (request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
  48. redirect(URL('admin', 'default', 'index',
  49. vars=dict(send=URL(args=request.args, vars=request.vars))))
  50. else:
  51. response.subtitle = T('Database Administration (appadmin)')
  52. menu = True
  53. ignore_rw = True
  54. response.view = 'appadmin.html'
  55. if menu:
  56. response.menu = [[T('design'), False, URL('admin', 'default', 'design',
  57. args=[request.application])], [T('db'), False,
  58. URL('index')], [T('state'), False,
  59. URL('state')], [T('cache'), False,
  60. URL('ccache')]]
  61. # ##########################################################
  62. # ## auxiliary functions
  63. # ###########################################################
  64. if False and request.tickets_db:
  65. from gluon.restricted import TicketStorage
  66. ts = TicketStorage()
  67. ts._get_table(request.tickets_db, ts.tablename, request.application)
  68. def get_databases(request):
  69. dbs = {}
  70. for (key, value) in global_env.items():
  71. try:
  72. cond = isinstance(value, GQLDB)
  73. except:
  74. cond = isinstance(value, SQLDB)
  75. if cond:
  76. dbs[key] = value
  77. return dbs
  78. databases = get_databases(None)
  79. def eval_in_global_env(text):
  80. exec ('_ret=%s' % text, {}, global_env)
  81. return global_env['_ret']
  82. def get_database(request):
  83. if request.args and request.args[0] in databases:
  84. return eval_in_global_env(request.args[0])
  85. else:
  86. session.flash = T('invalid request')
  87. redirect(URL('index'))
  88. def get_table(request):
  89. db = get_database(request)
  90. if len(request.args) > 1 and request.args[1] in db.tables:
  91. return (db, request.args[1])
  92. else:
  93. session.flash = T('invalid request')
  94. redirect(URL('index'))
  95. def get_query(request):
  96. try:
  97. return eval_in_global_env(request.vars.query)
  98. except Exception:
  99. return None
  100. def query_by_table_type(tablename, db, request=request):
  101. keyed = hasattr(db[tablename], '_primarykey')
  102. if keyed:
  103. firstkey = db[tablename][db[tablename]._primarykey[0]]
  104. cond = '>0'
  105. if firstkey.type in ['string', 'text']:
  106. cond = '!=""'
  107. qry = '%s.%s.%s%s' % (
  108. request.args[0], request.args[1], firstkey.name, cond)
  109. else:
  110. qry = '%s.%s.id>0' % tuple(request.args[:2])
  111. return qry
  112. # ##########################################################
  113. # ## list all databases and tables
  114. # ###########################################################
  115. def index():
  116. return dict(databases=databases)
  117. # ##########################################################
  118. # ## insert a new record
  119. # ###########################################################
  120. def insert():
  121. (db, table) = get_table(request)
  122. form = SQLFORM(db[table], ignore_rw=ignore_rw)
  123. if form.accepts(request.vars, session):
  124. response.flash = T('new record inserted')
  125. return dict(form=form, table=db[table])
  126. # ##########################################################
  127. # ## list all records in table and insert new record
  128. # ###########################################################
  129. def download():
  130. import os
  131. db = get_database(request)
  132. return response.download(request, db)
  133. def csv():
  134. import gluon.contenttype
  135. response.headers['Content-Type'] = \
  136. gluon.contenttype.contenttype('.csv')
  137. db = get_database(request)
  138. query = get_query(request)
  139. if not query:
  140. return None
  141. response.headers['Content-disposition'] = 'attachment; filename=%s_%s.csv'\
  142. % tuple(request.vars.query.split('.')[:2])
  143. return str(db(query, ignore_common_filters=True).select())
  144. def import_csv(table, file):
  145. table.import_from_csv_file(file)
  146. def select():
  147. import re
  148. db = get_database(request)
  149. dbname = request.args[0]
  150. try:
  151. is_imap = db._uri.startswith("imap://")
  152. except (KeyError, AttributeError, TypeError):
  153. is_imap = False
  154. regex = re.compile('(?P<table>\w+)\.(?P<field>\w+)=(?P<value>\d+)')
  155. if len(request.args) > 1 and hasattr(db[request.args[1]], '_primarykey'):
  156. regex = re.compile('(?P<table>\w+)\.(?P<field>\w+)=(?P<value>.+)')
  157. if request.vars.query:
  158. match = regex.match(request.vars.query)
  159. if match:
  160. request.vars.query = '%s.%s.%s==%s' % (request.args[0],
  161. match.group('table'), match.group('field'),
  162. match.group('value'))
  163. else:
  164. request.vars.query = session.last_query
  165. query = get_query(request)
  166. if request.vars.start:
  167. start = int(request.vars.start)
  168. else:
  169. start = 0
  170. nrows = 0
  171. step = 100
  172. fields = []
  173. if is_imap:
  174. step = 3
  175. stop = start + step
  176. table = None
  177. rows = []
  178. orderby = request.vars.orderby
  179. if orderby:
  180. orderby = dbname + '.' + orderby
  181. if orderby == session.last_orderby:
  182. if orderby[0] == '~':
  183. orderby = orderby[1:]
  184. else:
  185. orderby = '~' + orderby
  186. session.last_orderby = orderby
  187. session.last_query = request.vars.query
  188. form = FORM(TABLE(TR(T('Query:'), '', INPUT(_style='width:400px',
  189. _name='query', _value=request.vars.query or '',
  190. requires=IS_NOT_EMPTY(
  191. error_message=T("Cannot be empty")))), TR(T('Update:'),
  192. INPUT(_name='update_check', _type='checkbox',
  193. value=False), INPUT(_style='width:400px',
  194. _name='update_fields', _value=request.vars.update_fields
  195. or '')), TR(T('Delete:'), INPUT(_name='delete_check',
  196. _class='delete', _type='checkbox', value=False), ''),
  197. TR('', '', INPUT(_type='submit', _value=T('submit')))),
  198. _action=URL(r=request, args=request.args))
  199. tb = None
  200. if form.accepts(request.vars, formname=None):
  201. regex = re.compile(request.args[0] + '\.(?P<table>\w+)\..+')
  202. match = regex.match(form.vars.query.strip())
  203. if match:
  204. table = match.group('table')
  205. try:
  206. nrows = db(query, ignore_common_filters=True).count()
  207. if form.vars.update_check and form.vars.update_fields:
  208. db(query, ignore_common_filters=True).update(
  209. **eval_in_global_env('dict(%s)' % form.vars.update_fields))
  210. response.flash = T('%s %%{row} updated', nrows)
  211. elif form.vars.delete_check:
  212. db(query, ignore_common_filters=True).delete()
  213. response.flash = T('%s %%{row} deleted', nrows)
  214. nrows = db(query, ignore_common_filters=True).count()
  215. if is_imap:
  216. fields = [db[table][name] for name in
  217. ("id", "uid", "created", "to",
  218. "sender", "subject")]
  219. if orderby:
  220. rows = db(query, ignore_common_filters=True).select(
  221. *fields, limitby=(start, stop),
  222. orderby=eval_in_global_env(orderby))
  223. else:
  224. rows = db(query, ignore_common_filters=True).select(
  225. *fields, limitby=(start, stop))
  226. except Exception, e:
  227. import traceback
  228. tb = traceback.format_exc()
  229. (rows, nrows) = ([], 0)
  230. response.flash = DIV(T('Invalid Query'), PRE(str(e)))
  231. # begin handle upload csv
  232. csv_table = table or request.vars.table
  233. if csv_table:
  234. formcsv = FORM(str(T('or import from csv file')) + " ",
  235. INPUT(_type='file', _name='csvfile'),
  236. INPUT(_type='hidden', _value=csv_table, _name='table'),
  237. INPUT(_type='submit', _value=T('import')))
  238. else:
  239. formcsv = None
  240. if formcsv and formcsv.process().accepted:
  241. try:
  242. import_csv(db[request.vars.table],
  243. request.vars.csvfile.file)
  244. response.flash = T('data uploaded')
  245. except Exception, e:
  246. response.flash = DIV(T('unable to parse csv file'), PRE(str(e)))
  247. # end handle upload csv
  248. return dict(
  249. form=form,
  250. table=table,
  251. start=start,
  252. stop=stop,
  253. step=step,
  254. nrows=nrows,
  255. rows=rows,
  256. query=request.vars.query,
  257. formcsv=formcsv,
  258. tb=tb
  259. )
  260. # ##########################################################
  261. # ## edit delete one record
  262. # ###########################################################
  263. def update():
  264. (db, table) = get_table(request)
  265. keyed = hasattr(db[table], '_primarykey')
  266. record = None
  267. db[table]._common_filter = None
  268. if keyed:
  269. key = [f for f in request.vars if f in db[table]._primarykey]
  270. if key:
  271. record = db(db[table][key[0]] == request.vars[key[
  272. 0]]).select().first()
  273. else:
  274. record = db(db[table].id == request.args(
  275. 2)).select().first()
  276. if not record:
  277. qry = query_by_table_type(table, db)
  278. session.flash = T('record does not exist')
  279. redirect(URL('select', args=request.args[:1],
  280. vars=dict(query=qry)))
  281. if keyed:
  282. for k in db[table]._primarykey:
  283. db[table][k].writable = False
  284. form = SQLFORM(
  285. db[table], record, deletable=True, delete_label=T('Check to delete'),
  286. ignore_rw=ignore_rw and not keyed,
  287. linkto=URL('select',
  288. args=request.args[:1]), upload=URL(r=request,
  289. f='download', args=request.args[:1]))
  290. if form.accepts(request.vars, session):
  291. session.flash = T('done!')
  292. qry = query_by_table_type(table, db)
  293. redirect(URL('select', args=request.args[:1],
  294. vars=dict(query=qry)))
  295. return dict(form=form, table=db[table])
  296. # ##########################################################
  297. # ## get global variables
  298. # ###########################################################
  299. def state():
  300. return dict()
  301. def ccache():
  302. if is_gae:
  303. form = FORM(
  304. P(TAG.BUTTON(T("Clear CACHE?"), _type="submit", _name="yes", _value="yes")))
  305. else:
  306. cache.ram.initialize()
  307. cache.disk.initialize()
  308. form = FORM(
  309. P(TAG.BUTTON(
  310. T("Clear CACHE?"), _type="submit", _name="yes", _value="yes")),
  311. P(TAG.BUTTON(
  312. T("Clear RAM"), _type="submit", _name="ram", _value="ram")),
  313. P(TAG.BUTTON(
  314. T("Clear DISK"), _type="submit", _name="disk", _value="disk")),
  315. )
  316. if form.accepts(request.vars, session):
  317. session.flash = ""
  318. if is_gae:
  319. if request.vars.yes:
  320. cache.ram.clear()
  321. session.flash += T("Cache Cleared")
  322. else:
  323. clear_ram = False
  324. clear_disk = False
  325. if request.vars.yes:
  326. clear_ram = clear_disk = True
  327. if request.vars.ram:
  328. clear_ram = True
  329. if request.vars.disk:
  330. clear_disk = True
  331. if clear_ram:
  332. cache.ram.clear()
  333. session.flash += T("Ram Cleared")
  334. if clear_disk:
  335. cache.disk.clear()
  336. session.flash += T("Disk Cleared")
  337. redirect(URL(r=request))
  338. try:
  339. from guppy import hpy
  340. hp = hpy()
  341. except ImportError:
  342. hp = False
  343. import shelve
  344. import os
  345. import copy
  346. import time
  347. import math
  348. from gluon import portalocker
  349. ram = {
  350. 'entries': 0,
  351. 'bytes': 0,
  352. 'objects': 0,
  353. 'hits': 0,
  354. 'misses': 0,
  355. 'ratio': 0,
  356. 'oldest': time.time(),
  357. 'keys': []
  358. }
  359. disk = copy.copy(ram)
  360. total = copy.copy(ram)
  361. disk['keys'] = []
  362. total['keys'] = []
  363. def GetInHMS(seconds):
  364. hours = math.floor(seconds / 3600)
  365. seconds -= hours * 3600
  366. minutes = math.floor(seconds / 60)
  367. seconds -= minutes * 60
  368. seconds = math.floor(seconds)
  369. return (hours, minutes, seconds)
  370. if is_gae:
  371. gae_stats = cache.ram.client.get_stats()
  372. try:
  373. gae_stats['ratio'] = ((gae_stats['hits'] * 100) /
  374. (gae_stats['hits'] + gae_stats['misses']))
  375. except ZeroDivisionError:
  376. gae_stats['ratio'] = T("?")
  377. gae_stats['oldest'] = GetInHMS(time.time() - gae_stats['oldest_item_age'])
  378. total.update(gae_stats)
  379. else:
  380. for key, value in cache.ram.storage.iteritems():
  381. if isinstance(value, dict):
  382. ram['hits'] = value['hit_total'] - value['misses']
  383. ram['misses'] = value['misses']
  384. try:
  385. ram['ratio'] = ram['hits'] * 100 / value['hit_total']
  386. except (KeyError, ZeroDivisionError):
  387. ram['ratio'] = 0
  388. else:
  389. if hp:
  390. ram['bytes'] += hp.iso(value[1]).size
  391. ram['objects'] += hp.iso(value[1]).count
  392. ram['entries'] += 1
  393. if value[0] < ram['oldest']:
  394. ram['oldest'] = value[0]
  395. ram['keys'].append((key, GetInHMS(time.time() - value[0])))
  396. for key in cache.disk.storage:
  397. value = cache.disk.storage[key]
  398. if isinstance(value, dict):
  399. disk['hits'] = value['hit_total'] - value['misses']
  400. disk['misses'] = value['misses']
  401. try:
  402. disk['ratio'] = disk['hits'] * 100 / value['hit_total']
  403. except (KeyError, ZeroDivisionError):
  404. disk['ratio'] = 0
  405. else:
  406. if hp:
  407. disk['bytes'] += hp.iso(value[1]).size
  408. disk['objects'] += hp.iso(value[1]).count
  409. disk['entries'] += 1
  410. if value[0] < disk['oldest']:
  411. disk['oldest'] = value[0]
  412. disk['keys'].append((key, GetInHMS(time.time() - value[0])))
  413. ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses']
  414. ram_keys.remove('ratio')
  415. ram_keys.remove('oldest')
  416. for key in ram_keys:
  417. total[key] = ram[key] + disk[key]
  418. try:
  419. total['ratio'] = total['hits'] * 100 / (total['hits'] +
  420. total['misses'])
  421. except (KeyError, ZeroDivisionError):
  422. total['ratio'] = 0
  423. if disk['oldest'] < ram['oldest']:
  424. total['oldest'] = disk['oldest']
  425. else:
  426. total['oldest'] = ram['oldest']
  427. ram['oldest'] = GetInHMS(time.time() - ram['oldest'])
  428. disk['oldest'] = GetInHMS(time.time() - disk['oldest'])
  429. total['oldest'] = GetInHMS(time.time() - total['oldest'])
  430. def key_table(keys):
  431. return TABLE(
  432. TR(TD(B(T('Key'))), TD(B(T('Time in Cache (h:m:s)')))),
  433. *[TR(TD(k[0]), TD('%02d:%02d:%02d' % k[1])) for k in keys],
  434. **dict(_class='cache-keys',
  435. _style="border-collapse: separate; border-spacing: .5em;"))
  436. if not is_gae:
  437. ram['keys'] = key_table(ram['keys'])
  438. disk['keys'] = key_table(disk['keys'])
  439. total['keys'] = key_table(total['keys'])
  440. return dict(form=form, total=total,
  441. ram=ram, disk=disk, object_stats=hp != False)
  442. def table_template(table):
  443. from gluon.html import TR, TD, TABLE, TAG
  444. def FONT(*args, **kwargs):
  445. return TAG.font(*args, **kwargs)
  446. def types(field):
  447. f_type = field.type
  448. if not isinstance(f_type,str):
  449. return ' '
  450. elif f_type == 'string':
  451. return field.length
  452. elif f_type == 'id':
  453. return B('pk')
  454. elif f_type.startswith('reference') or \
  455. f_type.startswith('list:reference'):
  456. return B('fk')
  457. else:
  458. return ' '
  459. # This is horribe HTML but the only one graphiz understands
  460. rows = []
  461. cellpadding = 4
  462. color = "#000000"
  463. bgcolor = "#FFFFFF"
  464. face = "Helvetica"
  465. face_bold = "Helvetica Bold"
  466. border = 0
  467. rows.append(TR(TD(FONT(table, _face=face_bold, _color=bgcolor),
  468. _colspan=3, _cellpadding=cellpadding,
  469. _align="center", _bgcolor=color)))
  470. for row in db[table]:
  471. rows.append(TR(TD(FONT(row.name, _color=color, _face=face_bold),
  472. _align="left", _cellpadding=cellpadding,
  473. _border=border),
  474. TD(FONT(row.type, _color=color, _face=face),
  475. _align="left", _cellpadding=cellpadding,
  476. _border=border),
  477. TD(FONT(types(row), _color=color, _face=face),
  478. _align="center", _cellpadding=cellpadding,
  479. _border=border)))
  480. return "< %s >" % TABLE(*rows, **dict(_bgcolor=bgcolor, _border=1,
  481. _cellborder=0, _cellspacing=0)
  482. ).xml()
  483. def bg_graph_model():
  484. graph = pgv.AGraph(layout='dot', directed=True, strict=False, rankdir='LR')
  485. subgraphs = dict()
  486. for tablename in db.tables:
  487. if hasattr(db[tablename],'_meta_graphmodel'):
  488. meta_graphmodel = db[tablename]._meta_graphmodel
  489. else:
  490. meta_graphmodel = dict(group=request.application, color='#ECECEC')
  491. group = meta_graphmodel['group'].replace(' ', '')
  492. if not subgraphs.has_key(group):
  493. subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
  494. subgraphs[group]['tables'].append(tablename)
  495. graph.add_node(tablename, name=tablename, shape='plaintext',
  496. label=table_template(tablename))
  497. for n, key in enumerate(subgraphs.iterkeys()):
  498. graph.subgraph(nbunch=subgraphs[key]['tables'],
  499. name='cluster%d' % n,
  500. style='filled',
  501. color=subgraphs[key]['meta']['color'],
  502. label=subgraphs[key]['meta']['group'])
  503. for tablename in db.tables:
  504. for field in db[tablename]:
  505. f_type = field.type
  506. if isinstance(f_type,str) and (
  507. f_type.startswith('reference') or
  508. f_type.startswith('list:reference')):
  509. referenced_table = f_type.split()[1].split('.')[0]
  510. n1 = graph.get_node(tablename)
  511. n2 = graph.get_node(referenced_table)
  512. graph.add_edge(n1, n2, color="#4C4C4C", label='')
  513. graph.layout()
  514. if not request.args:
  515. response.headers['Content-Type'] = 'image/png'
  516. return graph.draw(format='png', prog='dot')
  517. else:
  518. response.headers['Content-Disposition']='attachment;filename=graph.%s'%request.args(0)
  519. if request.args(0) == 'dot':
  520. return graph.string()
  521. else:
  522. return graph.draw(format=request.args(0), prog='dot')
  523. def graph_model():
  524. return dict(databases=databases, pgv=pgv)
  525. def manage():
  526. tables = manager_action['tables']
  527. if isinstance(tables[0], str):
  528. db = manager_action.get('db', auth.db)
  529. db = globals()[db] if isinstance(db, str) else db
  530. tables = [db[table] for table in tables]
  531. if request.args(0) == 'auth':
  532. auth.table_user()._plural = T('Users')
  533. auth.table_group()._plural = T('Roles')
  534. auth.table_membership()._plural = T('Memberships')
  535. auth.table_permission()._plural = T('Permissions')
  536. if request.extension != 'load':
  537. return dict(heading=manager_action.get('heading',
  538. T('Manage %(action)s') % dict(action=request.args(0).replace('_', ' ').title())),
  539. tablenames=[table._tablename for table in tables],
  540. labels=[table._plural.title() for table in tables])
  541. table = tables[request.args(1, cast=int)]
  542. formname = '%s_grid' % table._tablename
  543. linked_tables = orderby = None
  544. if request.args(0) == 'auth':
  545. auth.table_group()._id.readable = \
  546. auth.table_membership()._id.readable = \
  547. auth.table_permission()._id.readable = False
  548. auth.table_membership().user_id.label = T('User')
  549. auth.table_membership().group_id.label = T('Role')
  550. auth.table_permission().group_id.label = T('Role')
  551. auth.table_permission().name.label = T('Permission')
  552. if table == auth.table_user():
  553. linked_tables=[auth.settings.table_membership_name]
  554. elif table == auth.table_group():
  555. orderby = 'role' if not request.args(3) or '.group_id' not in request.args(3) else None
  556. elif table == auth.table_permission():
  557. orderby = 'group_id'
  558. kwargs = dict(user_signature=True, maxtextlength=1000,
  559. orderby=orderby, linked_tables=linked_tables)
  560. smartgrid_args = manager_action.get('smartgrid_args', {})
  561. kwargs.update(**smartgrid_args.get('DEFAULT', {}))
  562. kwargs.update(**smartgrid_args.get(table._tablename, {}))
  563. grid = SQLFORM.smartgrid(table, args=request.args[:2], formname=formname, **kwargs)
  564. return grid
  565. def hooks():
  566. import functools
  567. import inspect
  568. list_op=['_%s_%s' %(h,m) for h in ['before', 'after'] for m in ['insert','update','delete']]
  569. tables=[]
  570. with_build_it=False
  571. for db_str in sorted(databases):
  572. db = databases[db_str]
  573. for t in db.tables:
  574. method_hooks=[]
  575. for op in list_op:
  576. functions = []
  577. for f in getattr(db[t], op):
  578. if hasattr(f, '__call__'):
  579. try:
  580. if isinstance(f, (functools.partial)):
  581. f = f.func
  582. filename = inspect.getsourcefile(f)
  583. details = {'funcname':f.__name__,
  584. 'filename':filename[len(request.folder):] if request.folder in filename else None,
  585. 'lineno': inspect.getsourcelines(f)[1]}
  586. if details['filename']: # Built in functions as delete_uploaded_files are not editable
  587. details['url'] = URL(a='admin',c='default',f='edit', args=[request['application'], details['filename']],vars={'lineno':details['lineno']})
  588. if details['filename'] or with_build_it:
  589. functions.append(details)
  590. # compiled app and windows build don't support code inspection
  591. except:
  592. pass
  593. if len(functions):
  594. method_hooks.append({'name':op, 'functions':functions})
  595. if len(method_hooks):
  596. tables.append({'name':"%s.%s" % (db_str,t), 'slug': IS_SLUG()("%s.%s" % (db_str,t))[0], 'method_hooks':method_hooks})
  597. # Render
  598. ul_main = UL(_class='nav nav-list')
  599. for t in tables:
  600. ul_main.append(A(t['name'], _onclick="collapse('a_%s')" % t['slug']))
  601. ul_t = UL(_class='nav nav-list', _id="a_%s" % t['slug'], _style='display:none')
  602. for op in t['method_hooks']:
  603. ul_t.append(LI (op['name']))
  604. ul_t.append(UL([LI(A(f['funcname'], _class="editor_filelink", _href=f['url']if 'url' in f else None, **{'_data-lineno':f['lineno']-1})) for f in op['functions']]))
  605. ul_main.append(ul_t)
  606. return ul_main