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