Merge github.com:web2py/web2py

This commit is contained in:
Michele Comitini
2012-08-23 14:10:50 +02:00
12 changed files with 185 additions and 186 deletions
+71 -55
View File
@@ -1,67 +1,84 @@
## 2.00.0 ## 2.00.1
- redirect(...,type='auto') will be handled properly in ajax responses ### DAL Improvements
- experimental MongoDB support in DAL
- FORM.confirm('Confirm?',{'Back':URL(...)}) - MongoDB support in DAL (experimental)
- @auth.requires_login(otherwise=URL(...)) - geodal and spatialite, thanks Denes and Fran (experimental)
- request.args(0,default=0, cast=int, otherwise=URL(...)), thanks Anthony - db.mytable._before_insert, _after_insert, _before_update, _after_update, _before_delete. _after_delete (list of callbacks)
- better memcache support
- generic.ics
- generic.map
- geodal and spatialite, thanks Denes and Fran
- auth.enable_record_versioning(...db=...)
- db.table._before_insert, _after_insert, _before_update, _after_update, _before_delete. _after_delete
- db(...).update_naive(...) same as update but ignores table._before_update and table._after_update - db(...).update_naive(...) same as update but ignores table._before_update and table._after_update
- SQLFORM.smartdictform
- form.add_button(value,link)
- db.table.insert(uploadfield=open(....))
- DAL BIGINT support and DAL(...,bigint_id=True) - DAL BIGINT support and DAL(...,bigint_id=True)
- IS_IN_DB distinct - IS_IN_DB(..., distinct=True)
- more secure SQLFORM.grid and SQLFORM.smartgrid - new syntax: db.mytable.insert(myuploadfield=open(....))
- more export options in SQLFORM.grid and SQLFORM.smartgrid - db(...).select(db.mytable.myfield.count(distinct=True))
- more secure admin against CSRF - db(db.a)._update(name=db(db.b.a==db.a.id).nested_select(db.b.id))
- improved spreadsheet, thanks Alan - db.mytable.myfield.filter_in, filter_out
- routes in can redirect outside with routes_in=[('/path','303->http://..')] - db.mytable._enable_record_versioning(db) adds verining to this table
- auth supports salt and compatible with third party data, thanks Dave Stoll
- CRYPT now defaults to pbkdf2(1000,20,sha1)
- experimental GIT integration
- experimental OpenShift deployment
- new makrmin with supports for nested lists, <i>, <em>, autolinks
- new interantionalization engine
- multi-language pluralization engine
- db.table.field.count(True)
- auth.wiki()
- new syntax BR()*5
- gluon.cache.lazy_cache decorator
- auth.settings.everybody_group_id
- ace text web editor in admin
- -E -b -L options for scheduler
- coffee and less support in response.fields, thanks Sam Sheftel
- ldap certificate support
- pg8000 postgresql driver support
- portable html.py
- new admin pages: manage_students, bulk_regsiter, and progress reports
- @cache('%(name)s%(args)s%(vars)s',5) and cache.autokey
- added tox.ini, thanks Marc
- better mongodb support (still experimental) thanks Mark
- web2py.py --run_system_tests, thanks Marc Abramowitz
- teradata adapter, thanks Andrew Willimott - teradata adapter, thanks Andrew Willimott
- experimental Sybase Adapter - experimental Sybase Adapter
- added db.table.field.avg() - added db.table.field.avg()
- db(db.a)._update(name=db(db.b.a==db.a.id).nested_select(db.b.id)) - Suport for Google App Engine projections
- new layout based on bootstrap and bootswatch
### Auth improvements
- auth.enable_record_versioning(db) adds full versining to all tables
- @auth.requires_login(otherwise=URL(...))
- auth supports salt and compatible with third party data, thanks Dave Stoll
- CRYPT now defaults to pbkdf2(1000,20,sha1)
- Built-in wiki with menu, tags, search, media, permissions. def index: return auth.wiki()
- auth.settings.everybody_group_id
- allow storage of uploads on any PyFileSystem (including amazon)
### Form improvements
- FORM.confirm('Are you sure?',{'Back':URL(...)})
- SQLFORM.smartdictform(dict)
- form.add_button(value,link)
- SQLFORM.grid(groupby=...')
- fixed security issue with SQLFORM.grid and SQLFORM.smartgrid
- more export options in SQLFORM.grid and SQLFORM.smartgrid (html, xml, csv, ...)
### Admin improvements
- new admin pages: manage_students, bulk_regsiter, and progress reports
- increased secure admin against CSRF
- experimental Git integration
- experimental OpenShift deployment
- multi-language pluralization engine
- ace text web editor in admin
- Ukrainian translations, thanks Vladyslav Kozlovskyy - Ukrainian translations, thanks Vladyslav Kozlovskyy
- Romanian translation for welcome, thanks ionel - Romanian translation for welcome, thanks ionel
- support for mercurial 2.6, thanks Vlad - support for mercurial 2.6, thanks Vlad
- GAE projections
- SQLFORM.grid(groupby=...') ### Scheduler Inprovements
- db.table.field.filter_in, filter_out
- allowed storage of uploads on any PyFileSystem (including amazon)
- new scheduler can now handle 10k tasks with 20 concurrent workers and no known issues (thanks to niphlod, ykessler, dhx, toomim)
- shceduler: tasks can be found in the environment (no need to define the tasks parameter)
- scheduler: max_empty_runs kills the workers automatically if no new tasks are found in queue (nice for "spikes" of processing power), discard_results to completely discard the results (if you don't need the output of the task), utc_time enables datetime calculations with UTC time, task_name is no longer required (filled automatically with function_name if found empty), uuid makes easy to coordinate scheduler_task maintenance (filled automatically if not provided), stop_time has no default (previously was today+1), retry_failed to requeue automatically failed tasks, sync_output refreshes automatically the output (nice to report percentages)
- scheduler tasks can be DISABLED (put to sleep and do nothing if not sending the heartbeat every 30 seconds), TERMINATE (complete the current task and then die), KILL (kill ASAP), EXPIRED (if stop_time passed)
- web2py.py -K myapp -X starts the myapp scheduler alongside the webserver - web2py.py -K myapp -X starts the myapp scheduler alongside the webserver
- more options: web2py.py -E -b -L
- scheduler can now handle 10k tasks with 20 concurrent workers and no known issues (thanks to niphlod, ykessler, dhx, toomim)
- tasks can be found in the environment (no need to define the tasks parameter)
- max_empty_runs kills the workers automatically if no new tasks are found in queue (nice for "spikes" of processing power), discard_results to completely discard the results (if you don't need the output of the task), utc_time enables datetime calculations with UTC time, task_name is no longer required (filled automatically with function_name if found empty), uuid makes easy to coordinate scheduler_task maintenance (filled automatically if not provided), stop_time has no default (previously was today+1), retry_failed to requeue automatically failed tasks, sync_output refreshes automatically the output (nice to report percentages)
- tasks can be DISABLED (put to sleep and do nothing if not sending the heartbeat every 30 seconds), TERMINATE (complete the current task and then die), KILL (kill ASAP), EXPIRED (if stop_time passed)
### Other Improvements
- new layout based on Twitter Bootstrap
- New generic views: generic.ics (Mac Mail Calendar) and generic.map (Google Maps)
- request.args(0,default=0, cast=int, otherwise=URL(...)), thanks Anthony
- redirect(...,type='auto') will be handled properly in ajax responses
- routes in can redirect outside with routes_in=[('/path','303->http://..')]
- better memcache support
- improved spreadsheet, thanks Alan
- new interantionalization engine, thanks Vladyslav
- pluralization engine, thanks Vladyslav
- new makrmin with supports for nested lists, <i>, <em>, autolinks, thanks Vladyslav
- new syntax: {{=BR()*5}}
- gluon.cache.lazy_cache decorator allows caching functions in modules
- .coffee and .less support in response.fields, thanks Sam Sheftel
- ldap certificate support
- pg8000 postgresql driver support (experimental)
- @cache('%(name)s%(args)s%(vars)s',5) and cache.autokey
- added tox.ini, thanks Marc
- web2py.py --run_system_tests, thanks Marc Abramowitz
- html.py (and web2py helpers) can be used without web2py dependencies
## 1.99.5-1.99.7 ## 1.99.5-1.99.7
@@ -125,7 +142,6 @@ This is a major revision in peparation for web2py 2.0
- experimental PyPy support - experimental PyPy support
- experimental mongodb support, thanks Mark - experimental mongodb support, thanks Mark
- tickets in db now accessible from admin, thanks Niphlod - tickets in db now accessible from admin, thanks Niphlod
- many grid improvements and bug fixes
+5 -1
View File
@@ -124,4 +124,8 @@ push:
hg push hg push
git push git push
git push mdipierro git push mdipierro
#bzr push bzr+ssh://mdipierro@bazaar.launchpad.net/~mdipierro/web2py/devel --use-existing-dir tag:
git tag -l '$(S)'
hg tag -l '$(S)'
make commit
make push
+1 -1
View File
@@ -1 +1 @@
Version 2.00.0 (2012-08-21 18:20:02) dev Version 2.00.0 (2012-08-22 19:47:08) dev
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+11 -9
View File
@@ -6281,9 +6281,11 @@ class Row(dict):
this is only used to store a Row this is only used to store a Row
""" """
def __init__(self,*args,**kwargs): def __getattr__(self, key):
dict.__init__(self,*args,**kwargs) return self[key]
self.__dict__ = self
def __setattr__(self, key, value):
self[key] = value
def __getitem__(self, key): def __getitem__(self, key):
key=str(key) key=str(key)
@@ -6572,7 +6574,6 @@ class DAL(dict):
:fake_migrate_all (defaults to False). If sets to True fake migrates ALL tables :fake_migrate_all (defaults to False). If sets to True fake migrates ALL tables
:attempts (defaults to 5). Number of times to attempt connecting :attempts (defaults to 5). Number of times to attempt connecting
""" """
self.__dict__ = self
if not decode_credentials: if not decode_credentials:
credential_decoder = lambda cred: cred credential_decoder = lambda cred: cred
else: else:
@@ -6983,8 +6984,7 @@ def index():
yield self[tablename] yield self[tablename]
def __getitem__(self, key): def __getitem__(self, key):
key = str(key) return self.__getattr__(str(key))
return dict.__getitem__(self,key)
def __getattr__(self, key): def __getattr__(self, key):
if not key is '_LAZY_TABLES' and key in self._LAZY_TABLES: if not key is '_LAZY_TABLES' and key in self._LAZY_TABLES:
@@ -6999,7 +6999,7 @@ def index():
if key[:1]!='_' and key in self: if key[:1]!='_' and key in self:
raise SyntaxError, \ raise SyntaxError, \
'Object %s exists and cannot be redefined' % key 'Object %s exists and cannot be redefined' % key
dict.__setattr__(self,key,value) dict.__setitem__(self,key,value)
def __repr__(self): def __repr__(self):
return '<DAL ' + dict.__repr__(self) + '>' return '<DAL ' + dict.__repr__(self) + '>'
@@ -7201,7 +7201,6 @@ class Table(dict):
:raises SyntaxError: when a supplied field is of incorrect type. :raises SyntaxError: when a supplied field is of incorrect type.
""" """
self.__dict__ = self
self._actual = False # set to True by define_table() self._actual = False # set to True by define_table()
self._tablename = tablename self._tablename = tablename
self._sequence_name = args.get('sequence_name',None) or \ self._sequence_name = args.get('sequence_name',None) or \
@@ -7463,6 +7462,9 @@ class Table(dict):
'value must be a dictionary: %s' % value 'value must be a dictionary: %s' % value
dict.__setitem__(self, str(key), value) dict.__setitem__(self, str(key), value)
def __getattr__(self, key):
return self[key]
def __delitem__(self, key): def __delitem__(self, key):
if isinstance(key, dict): if isinstance(key, dict):
query = self._build_query(key) query = self._build_query(key)
@@ -7474,7 +7476,7 @@ class Table(dict):
def __setattr__(self, key, value): def __setattr__(self, key, value):
if key[:1]!='_' and key in self: if key[:1]!='_' and key in self:
raise SyntaxError, 'Object exists and cannot be redefined: %s' % key raise SyntaxError, 'Object exists and cannot be redefined: %s' % key
dict.__setattr__(self,key,value) self[key] = value
def __iter__(self): def __iter__(self):
for fieldname in self.fields: for fieldname in self.fields:
+1 -1
View File
@@ -318,7 +318,7 @@ def get_session(request, other_application='admin'):
session_id = request.cookies['session_id_' + other_application].value session_id = request.cookies['session_id_' + other_application].value
osession = storage.load_storage(os.path.join( osession = storage.load_storage(os.path.join(
up(request.folder), other_application, 'sessions', session_id)) up(request.folder), other_application, 'sessions', session_id))
except: except Exception, e:
osession = storage.Storage() osession = storage.Storage()
return osession return osession
+1 -1
View File
@@ -2044,7 +2044,7 @@ class FORM(DIV):
@staticmethod @staticmethod
def confim(text='OK',buttons=None,hidden=None): def confirm(text='OK',buttons=None,hidden=None):
if not buttons: buttons = {} if not buttons: buttons = {}
if not hidden: hidden={} if not hidden: hidden={}
inputs = [INPUT(_type='button', inputs = [INPUT(_type='button',
+5 -11
View File
@@ -476,7 +476,7 @@ def load_routers(all_apps):
def regex_uri(e, regexes, tag, default=None): def regex_uri(e, regexes, tag, default=None):
"filter incoming URI against a list of regexes" "filter incoming URI against a list of regexes"
path = e['PATH_INFO'] path = e['PATH_INFO']
host = e.get('HTTP_HOST', 'localhost').lower() host = e.get('http_host', e.get('SERVER_NAME','localhost')).lower()
i = host.find(':') i = host.find(':')
if i > 0: if i > 0:
host = host[:i] host = host[:i]
@@ -823,21 +823,15 @@ class MapUrlIn(object):
self.remote_addr = self.env.get('REMOTE_ADDR','localhost') self.remote_addr = self.env.get('REMOTE_ADDR','localhost')
self.scheme = self.env.get('wsgi.url_scheme', 'http').lower() self.scheme = self.env.get('wsgi.url_scheme', 'http').lower()
self.method = self.env.get('REQUEST_METHOD', 'get').lower() self.method = self.env.get('REQUEST_METHOD', 'get').lower()
self.host = self.env.get('HTTP_HOST') (self.host, self.port) = (self.env.get('HTTP_HOST'), None)
self.port = None
if not self.host: if not self.host:
self.host = self.env.get('SERVER_NAME') (self.host, self.port) = (self.env.get('SERVER_NAME'), self.env.get('SERVER_PORT'))
self.port = self.env.get('SERVER_PORT')
if not self.host: if not self.host:
self.host = 'localhost' (self.host, self.port) = ('localhost', '80')
self.port = '80'
if ':' in self.host: if ':' in self.host:
(self.host, self.port) = self.host.split(':') (self.host, self.port) = self.host.split(':')
if not self.port: if not self.port:
if self.scheme == 'https': self.port = '443' if self.scheme == 'https' else '80'
self.port = '443'
else:
self.port = '80'
def map_prefix(self): def map_prefix(self):
"strip path prefix, if present in its entirety" "strip path prefix, if present in its entirety"
+57 -70
View File
@@ -13,42 +13,11 @@ Provides:
""" """
import cPickle import cPickle
import portalocker
__all__ = ['List', 'Storage', 'Settings', 'Messages', __all__ = ['List', 'Storage', 'Settings', 'Messages',
'StorageList', 'load_storage', 'save_storage'] 'StorageList', 'load_storage', 'save_storage']
class List(list):
"""
Like a regular python list but a[i] if i is out of bounds return None
instead of IndexOutOfBounds
"""
def __call__(self, i, default=None, cast=None, otherwise=None):
"""
request.args(0,default=0,cast=int,otherwise='http://error_url')
request.args(0,default=0,cast=int,otherwise=lambda:...)
"""
n = len(self)
if 0<=i<n or -n<=i<0:
value = self[i]
else:
value = default
if cast:
try:
value = cast(value)
except (ValueError, TypeError):
from http import HTTP, redirect
if otherwise is None:
raise HTTP(404)
elif isinstance(otherwise,str):
redirect(otherwise)
elif callable(otherwise):
return otherwise()
else:
raise RuntimeError, "invalid otherwise"
return value
class Storage(dict): class Storage(dict):
""" """
A Storage object is like a dictionary except `obj.foo` can be used A Storage object is like a dictionary except `obj.foo` can be used
@@ -66,31 +35,24 @@ class Storage(dict):
2 2
>>> del o.a >>> del o.a
>>> print o.a >>> print o.a
None
""" """
def __init__(self, *args, **kwargs): def __getattr__(self, key):
dict.__init__(self, *args, **kwargs) return dict.get(self, key, None)
self.__dict__ = self def __setattr__(self, key, value):
def __getattr__(self,key): self[key] = value
return getattr(self,key) if key in self else None def __delattr__(self, key):
def __getitem__(self,key): del self[key]
return dict.get(self,key,None) def __getitem__(self, key):
def copy(self): return dict.get(self, key, None)
self.__dict__ = {}
s = Storage(self)
self.__dict__ = self
return s
def __repr__(self): def __repr__(self):
return '<Storage %s>' % dict.__repr__(self) return '<Storage %s>' + dict.__repr__(self)
def __getstate__(self): def __getstate__(self):
return dict(self) return dict(self)
def __setstate__(self, sdict): def __setstate__(self,values):
dict.__init__(self, sdict) self.update(values)
self.__dict__ = self
def update(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
self.__dict__ = self
def getlist(self,key): def getlist(self,key):
""" """
Return a Storage value as a list. Return a Storage value as a list.
@@ -111,9 +73,10 @@ class Storage(dict):
>>> request.vars.getlist('z') >>> request.vars.getlist('z')
[] []
""" """
value = getattr(self,key,[]) value = self.get(key,[])
return value if not value else \ return value if not value else \
value if isinstance(value,(list,tuple)) else [value] value if isinstance(value,(list,tuple)) else [value]
def getfirst(self,key,default=None): def getfirst(self,key,default=None):
""" """
Return the first or only value when given a request.vars-style key. Return the first or only value when given a request.vars-style key.
@@ -132,8 +95,9 @@ class Storage(dict):
'abc' 'abc'
>>> request.vars.getfirst('z') >>> request.vars.getfirst('z')
""" """
values = self.getlist(default) values = self.getlist(key)
return values[0] if values else default return values[0] if values else default
def getlast(self,key,default=None): def getlast(self,key,default=None):
""" """
Returns the last or only single value when Returns the last or only single value when
@@ -153,8 +117,8 @@ class Storage(dict):
'def' 'def'
>>> request.vars.getlast('z') >>> request.vars.getlast('z')
""" """
values = self.getlist(default) values = self.getlist(key)
return values[0] if values else default return values[-1] if values else default
PICKABLE = (str,int,long,float,bool,list,dict,tuple,set) PICKABLE = (str,int,long,float,bool,list,dict,tuple,set)
@@ -191,29 +155,52 @@ def save_storage(storage, filename):
class Settings(Storage): class Settings(Storage):
def __setattr__(self, key, value): def __setattr__(self, key, value):
if key != 'lock_keys' and self.lock_keys and not key in self: if key != 'lock_keys' and 'lock_keys' in self and not key in self:
raise SyntaxError, 'setting key \'%s\' does not exist' % key raise SyntaxError, 'setting key \'%s\' does not exist' % key
if key != 'lock_values' and self.lock_values: if key != 'lock_values' and 'lock_values' in self:
raise SyntaxError, 'setting value cannot be changed: %s' % key raise SyntaxError, 'setting value cannot be changed: %s' % key
Storage.__setattr__(self,key,value) self[key] = value
class Messages(Storage): class Messages(Settings):
def __init__(self, T): def __init__(self, T):
Storage.__init__(self,T=T) Storage.__init__(self,T=T)
def __setattr__(self, key, value):
if key != 'lock_keys' and self.lock_keys and not key in self:
raise SyntaxError, 'setting key \'%s\' does not exist' % key
if key != 'lock_values' and self.lock_values:
raise SyntaxError, 'setting value cannot be changed: %s' % key
Storage.__setattr__(self,key,value)
def __getattr__(self, key): def __getattr__(self, key):
value = Storage.__getattr__(self,key) value = self[key]
if isinstance(value, str): if isinstance(value, str):
return str(self.T(value)) return str(self.T(value))
return value return value
class List(list):
"""
Like a regular python list but a[i] if i is out of bounds return None
instead of IndexOutOfBounds
"""
def __call__(self, i, default=None, cast=None, otherwise=None):
"""
request.args(0,default=0,cast=int,otherwise='http://error_url')
request.args(0,default=0,cast=int,otherwise=lambda:...)
"""
n = len(self)
if 0<=i<n or -n<=i<0:
value = self[i]
else:
value = default
if cast:
try:
value = cast(value)
except (ValueError, TypeError):
from http import HTTP, redirect
if otherwise is None:
raise HTTP(404)
elif isinstance(otherwise,str):
redirect(otherwise)
elif callable(otherwise):
return otherwise()
else:
raise RuntimeError, "invalid otherwise"
return value
if __name__ == '__main__': if __name__ == '__main__':
import doctest import doctest
+27 -25
View File
@@ -3138,7 +3138,8 @@ class Auth(object):
return id return id
def wiki(self,slug=None,env=None,manage_permissions=False,force_prefix=''): def wiki(self,slug=None,env=None,manage_permissions=False,force_prefix=''):
if not hasattr(self,'_wiki'): if not hasattr(self,'_wiki'):
self._wiki = Wiki(self,manage_permissions=manage_permissions, self._wiki = Wiki(self,
manage_permissions=manage_permissions,
force_prefix=force_prefix,env=env) force_prefix=force_prefix,env=env)
else: else:
self._wiki.env.update(env or {}) self._wiki.env.update(env or {})
@@ -4466,13 +4467,12 @@ class Wiki(object):
items = text.split('/') items = text.split('/')
controller, function, args = items[0], items[1], items[2:] controller, function, args = items[0], items[1], items[2:]
return LOAD(controller, function, args=args, ajax=True).xml() return LOAD(controller, function, args=args, ajax=True).xml()
def __init__(self,auth,env=None,automenu=True,render='markmin', def __init__(self,auth,env=None,render='markmin',
manage_permissions=False,force_prefix=''): manage_permissions=False,force_prefix=''):
self.env = env or {} self.env = env or {}
self.env['component'] = Wiki.component self.env['component'] = Wiki.component
if render == 'markmin': render=self.markmin_render if render == 'markmin': render=self.markmin_render
self.auth = auth self.auth = auth
self.automenu = automenu
if self.auth.user: if self.auth.user:
self.force_prefix = force_prefix % self.auth.user self.force_prefix = force_prefix % self.auth.user
else: else:
@@ -4487,7 +4487,6 @@ class Wiki(object):
readable=False,writable=False), readable=False,writable=False),
Field('title',unique=True), Field('title',unique=True),
Field('body','text',notnull=True), Field('body','text',notnull=True),
Field('menu'),
Field('tags','list:string'), Field('tags','list:string'),
Field('can_read','list:string',writable=perms,readable=perms, Field('can_read','list:string',writable=perms,readable=perms,
default=[Wiki.everybody]), default=[Wiki.everybody]),
@@ -4553,9 +4552,8 @@ class Wiki(object):
### END POLICY ### END POLICY
def __call__(self): def __call__(self):
request = current.request request = current.request
if self.automenu: automenu = self.menu(request.controller,request.function)
current.response.menu = self.menu(request.controller, current.response.menu += automenu
request.function)
zero = request.args(0) zero = request.args(0)
if zero and zero.isdigit(): if zero and zero.isdigit():
return self.media(int(zero)) return self.media(int(zero))
@@ -4644,8 +4642,10 @@ class Wiki(object):
db.wiki_page.can_edit.default = [auth.user_group_role()] db.wiki_page.can_edit.default = [auth.user_group_role()]
db.wiki_page.title.default = title_guess db.wiki_page.title.default = title_guess
db.wiki_page.slug.default = slug db.wiki_page.slug.default = slug
db.wiki_page.menu.default = slug if slug == 'wiki-menu':
db.wiki_page.body.default = '## %s\n\npage content' % title_guess db.wiki_page.body.default = '- Menu Item > @////index\n- - Submenu > http://web2py.com'
else:
db.wiki_page.body.default = '## %s\n\npage content' % title_guess
vars = current.request.post_vars vars = current.request.post_vars
if vars.body: if vars.body:
vars.body=vars.body.replace('://%s' % self.host,'://HOSTNAME') vars.body=vars.body.replace('://%s' % self.host,'://HOSTNAME')
@@ -4716,23 +4716,23 @@ class Wiki(object):
def menu(self,controller='default',function='index'): def menu(self,controller='default',function='index'):
db = self.auth.db db = self.auth.db
request = current.request request = current.request
rows = db((db.wiki_page.menu!=None)|(db.wiki_page.menu!=''))\ menu_page = db.wiki_page(slug='wiki-menu')
.select(db.wiki_page.menu,db.wiki_page.title,db.wiki_page.slug,
orderby = db.wiki_page.menu)
menu = [] menu = []
tree = {'.':menu} if menu_page:
regex = re.compile('\d\:') tree = {'':menu}
for row in rows: regex = re.compile('[\r\n\t]*(?P<base>(\s*\-\s*)+)(?P<title>\w.*?)\s+\>\s+(?P<link>\S+)')
if row.menu: for match in regex.finditer(self.fix_hostname(menu_page.body)):
key = './'+regex.sub('',row.menu) base = match.group('base').replace(' ','')
base = key.rsplit('/',1)[0] title = match.group('title')
subtree = tree[key] = [] link = match.group('link')
if base in tree: if link.startswith('@'):
tree[base].append((current.T(row.title), items = link[1:].split('/')
request.args(0)==row.slug, if len(items)>3:
URL(controller,function,args=row.slug), link = URL(a=items[0] or None,c=items[1] or None,f=items[2] or None, args=items[3:])
subtree)) parent = tree.get(base[1:],tree[''])
#if self.auth.user: subtree = []
tree[base] = subtree
parent.append((current.T(title),False,link,subtree))
if True: if True:
submenu = [] submenu = []
menu.append((current.T('[Wiki]'),None,None,submenu)) menu.append((current.T('[Wiki]'),None,None,submenu))
@@ -4763,6 +4763,8 @@ class Wiki(object):
# if self.can_manage(): # if self.can_manage():
submenu.append((current.T('Manage Pages'),None, submenu.append((current.T('Manage Pages'),None,
URL(controller,function,args=('_pages')))) URL(controller,function,args=('_pages'))))
submenu.append((current.T('Edit Menu'),None,
URL(controller,function,args=('_edit','wiki-menu'))))
# if self.can_search(): # if self.can_search():
submenu.append((current.T('Search Pages'),None, submenu.append((current.T('Search Pages'),None,
URL(controller,function,args=('_search')))) URL(controller,function,args=('_search'))))