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
- experimental MongoDB support in DAL
- FORM.confirm('Confirm?',{'Back':URL(...)})
- @auth.requires_login(otherwise=URL(...))
- request.args(0,default=0, cast=int, otherwise=URL(...)), thanks Anthony
- 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
### DAL Improvements
- MongoDB support in DAL (experimental)
- geodal and spatialite, thanks Denes and Fran (experimental)
- db.mytable._before_insert, _after_insert, _before_update, _after_update, _before_delete. _after_delete (list of callbacks)
- 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)
- IS_IN_DB distinct
- more secure SQLFORM.grid and SQLFORM.smartgrid
- more export options in SQLFORM.grid and SQLFORM.smartgrid
- more secure admin against CSRF
- improved spreadsheet, thanks Alan
- routes in can redirect outside with routes_in=[('/path','303->http://..')]
- 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
- IS_IN_DB(..., distinct=True)
- new syntax: db.mytable.insert(myuploadfield=open(....))
- db(...).select(db.mytable.myfield.count(distinct=True))
- db(db.a)._update(name=db(db.b.a==db.a.id).nested_select(db.b.id))
- db.mytable.myfield.filter_in, filter_out
- db.mytable._enable_record_versioning(db) adds verining to this table
- teradata adapter, thanks Andrew Willimott
- experimental Sybase Adapter
- added db.table.field.avg()
- db(db.a)._update(name=db(db.b.a==db.a.id).nested_select(db.b.id))
- new layout based on bootstrap and bootswatch
- Suport for Google App Engine projections
### 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
- Romanian translation for welcome, thanks ionel
- support for mercurial 2.6, thanks Vlad
- GAE projections
- SQLFORM.grid(groupby=...')
- 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)
### Scheduler Inprovements
- 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
@@ -125,7 +142,6 @@ This is a major revision in peparation for web2py 2.0
- experimental PyPy support
- experimental mongodb support, thanks Mark
- 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
git push
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
"""
def __init__(self,*args,**kwargs):
dict.__init__(self,*args,**kwargs)
self.__dict__ = self
def __getattr__(self, key):
return self[key]
def __setattr__(self, key, value):
self[key] = value
def __getitem__(self, 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
:attempts (defaults to 5). Number of times to attempt connecting
"""
self.__dict__ = self
if not decode_credentials:
credential_decoder = lambda cred: cred
else:
@@ -6983,8 +6984,7 @@ def index():
yield self[tablename]
def __getitem__(self, key):
key = str(key)
return dict.__getitem__(self,key)
return self.__getattr__(str(key))
def __getattr__(self, key):
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:
raise SyntaxError, \
'Object %s exists and cannot be redefined' % key
dict.__setattr__(self,key,value)
dict.__setitem__(self,key,value)
def __repr__(self):
return '<DAL ' + dict.__repr__(self) + '>'
@@ -7201,7 +7201,6 @@ class Table(dict):
:raises SyntaxError: when a supplied field is of incorrect type.
"""
self.__dict__ = self
self._actual = False # set to True by define_table()
self._tablename = tablename
self._sequence_name = args.get('sequence_name',None) or \
@@ -7463,6 +7462,9 @@ class Table(dict):
'value must be a dictionary: %s' % value
dict.__setitem__(self, str(key), value)
def __getattr__(self, key):
return self[key]
def __delitem__(self, key):
if isinstance(key, dict):
query = self._build_query(key)
@@ -7474,7 +7476,7 @@ class Table(dict):
def __setattr__(self, key, value):
if key[:1]!='_' and key in self:
raise SyntaxError, 'Object exists and cannot be redefined: %s' % key
dict.__setattr__(self,key,value)
self[key] = value
def __iter__(self):
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
osession = storage.load_storage(os.path.join(
up(request.folder), other_application, 'sessions', session_id))
except:
except Exception, e:
osession = storage.Storage()
return osession
+1 -1
View File
@@ -2044,7 +2044,7 @@ class FORM(DIV):
@staticmethod
def confim(text='OK',buttons=None,hidden=None):
def confirm(text='OK',buttons=None,hidden=None):
if not buttons: buttons = {}
if not hidden: hidden={}
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):
"filter incoming URI against a list of regexes"
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(':')
if i > 0:
host = host[:i]
@@ -823,21 +823,15 @@ class MapUrlIn(object):
self.remote_addr = self.env.get('REMOTE_ADDR','localhost')
self.scheme = self.env.get('wsgi.url_scheme', 'http').lower()
self.method = self.env.get('REQUEST_METHOD', 'get').lower()
self.host = self.env.get('HTTP_HOST')
self.port = None
(self.host, self.port) = (self.env.get('HTTP_HOST'), None)
if not self.host:
self.host = self.env.get('SERVER_NAME')
self.port = self.env.get('SERVER_PORT')
(self.host, self.port) = (self.env.get('SERVER_NAME'), self.env.get('SERVER_PORT'))
if not self.host:
self.host = 'localhost'
self.port = '80'
(self.host, self.port) = ('localhost', '80')
if ':' in self.host:
(self.host, self.port) = self.host.split(':')
if not self.port:
if self.scheme == 'https':
self.port = '443'
else:
self.port = '80'
self.port = '443' if self.scheme == 'https' else '80'
def map_prefix(self):
"strip path prefix, if present in its entirety"
+57 -70
View File
@@ -13,42 +13,11 @@ Provides:
"""
import cPickle
import portalocker
__all__ = ['List', 'Storage', 'Settings', 'Messages',
'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):
"""
A Storage object is like a dictionary except `obj.foo` can be used
@@ -66,31 +35,24 @@ class Storage(dict):
2
>>> del o.a
>>> print o.a
>>> print o.a
None
"""
def __init__(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
self.__dict__ = self
def __getattr__(self,key):
return getattr(self,key) if key in self else None
def __getitem__(self,key):
return dict.get(self,key,None)
def copy(self):
self.__dict__ = {}
s = Storage(self)
self.__dict__ = self
return s
def __getattr__(self, key):
return dict.get(self, key, None)
def __setattr__(self, key, value):
self[key] = value
def __delattr__(self, key):
del self[key]
def __getitem__(self, key):
return dict.get(self, key, None)
def __repr__(self):
return '<Storage %s>' % dict.__repr__(self)
return '<Storage %s>' + dict.__repr__(self)
def __getstate__(self):
return dict(self)
def __setstate__(self, sdict):
dict.__init__(self, sdict)
self.__dict__ = self
def update(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
self.__dict__ = self
def __setstate__(self,values):
self.update(values)
def getlist(self,key):
"""
Return a Storage value as a list.
@@ -111,9 +73,10 @@ class Storage(dict):
>>> request.vars.getlist('z')
[]
"""
value = getattr(self,key,[])
value = self.get(key,[])
return value if not value else \
value if isinstance(value,(list,tuple)) else [value]
def getfirst(self,key,default=None):
"""
Return the first or only value when given a request.vars-style key.
@@ -132,8 +95,9 @@ class Storage(dict):
'abc'
>>> request.vars.getfirst('z')
"""
values = self.getlist(default)
values = self.getlist(key)
return values[0] if values else default
def getlast(self,key,default=None):
"""
Returns the last or only single value when
@@ -153,8 +117,8 @@ class Storage(dict):
'def'
>>> request.vars.getlast('z')
"""
values = self.getlist(default)
return values[0] if values else default
values = self.getlist(key)
return values[-1] if values else default
PICKABLE = (str,int,long,float,bool,list,dict,tuple,set)
@@ -191,29 +155,52 @@ def save_storage(storage, filename):
class Settings(Storage):
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
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
Storage.__setattr__(self,key,value)
self[key] = value
class Messages(Storage):
class Messages(Settings):
def __init__(self, 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):
value = Storage.__getattr__(self,key)
value = self[key]
if isinstance(value, str):
return str(self.T(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__':
import doctest
+27 -25
View File
@@ -3138,7 +3138,8 @@ class Auth(object):
return id
def wiki(self,slug=None,env=None,manage_permissions=False,force_prefix=''):
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)
else:
self._wiki.env.update(env or {})
@@ -4466,13 +4467,12 @@ class Wiki(object):
items = text.split('/')
controller, function, args = items[0], items[1], items[2:]
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=''):
self.env = env or {}
self.env['component'] = Wiki.component
if render == 'markmin': render=self.markmin_render
self.auth = auth
self.automenu = automenu
if self.auth.user:
self.force_prefix = force_prefix % self.auth.user
else:
@@ -4487,7 +4487,6 @@ class Wiki(object):
readable=False,writable=False),
Field('title',unique=True),
Field('body','text',notnull=True),
Field('menu'),
Field('tags','list:string'),
Field('can_read','list:string',writable=perms,readable=perms,
default=[Wiki.everybody]),
@@ -4553,9 +4552,8 @@ class Wiki(object):
### END POLICY
def __call__(self):
request = current.request
if self.automenu:
current.response.menu = self.menu(request.controller,
request.function)
automenu = self.menu(request.controller,request.function)
current.response.menu += automenu
zero = request.args(0)
if zero and zero.isdigit():
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.title.default = title_guess
db.wiki_page.slug.default = slug
db.wiki_page.menu.default = slug
db.wiki_page.body.default = '## %s\n\npage content' % title_guess
if slug == 'wiki-menu':
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
if vars.body:
vars.body=vars.body.replace('://%s' % self.host,'://HOSTNAME')
@@ -4716,23 +4716,23 @@ class Wiki(object):
def menu(self,controller='default',function='index'):
db = self.auth.db
request = current.request
rows = db((db.wiki_page.menu!=None)|(db.wiki_page.menu!=''))\
.select(db.wiki_page.menu,db.wiki_page.title,db.wiki_page.slug,
orderby = db.wiki_page.menu)
menu_page = db.wiki_page(slug='wiki-menu')
menu = []
tree = {'.':menu}
regex = re.compile('\d\:')
for row in rows:
if row.menu:
key = './'+regex.sub('',row.menu)
base = key.rsplit('/',1)[0]
subtree = tree[key] = []
if base in tree:
tree[base].append((current.T(row.title),
request.args(0)==row.slug,
URL(controller,function,args=row.slug),
subtree))
#if self.auth.user:
if menu_page:
tree = {'':menu}
regex = re.compile('[\r\n\t]*(?P<base>(\s*\-\s*)+)(?P<title>\w.*?)\s+\>\s+(?P<link>\S+)')
for match in regex.finditer(self.fix_hostname(menu_page.body)):
base = match.group('base').replace(' ','')
title = match.group('title')
link = match.group('link')
if link.startswith('@'):
items = link[1:].split('/')
if len(items)>3:
link = URL(a=items[0] or None,c=items[1] or None,f=items[2] or None, args=items[3:])
parent = tree.get(base[1:],tree[''])
subtree = []
tree[base] = subtree
parent.append((current.T(title),False,link,subtree))
if True:
submenu = []
menu.append((current.T('[Wiki]'),None,None,submenu))
@@ -4763,6 +4763,8 @@ class Wiki(object):
# if self.can_manage():
submenu.append((current.T('Manage Pages'),None,
URL(controller,function,args=('_pages'))))
submenu.append((current.T('Edit Menu'),None,
URL(controller,function,args=('_edit','wiki-menu'))))
# if self.can_search():
submenu.append((current.T('Search Pages'),None,
URL(controller,function,args=('_search'))))