Compare commits

..
21 Commits
Author SHA1 Message Date
mdipierro 327b1cbfdd R-2.9.7 2014-09-04 22:37:12 -05:00
mdipierro 3bd44d4d84 R-2.9.7 2014-09-04 22:31:42 -05:00
mdipierro 7e50bd6050 R-2.9.7 2014-09-04 22:30:19 -05:00
mdipierro c1c3621bf3 using recfile for sessions for speed 2014-09-04 22:28:51 -05:00
mdipierro 2d9f0fafdc better cache-disk, thanks Leonel 2014-09-04 22:27:52 -05:00
mdipierro 9fd827c561 added recfile.py 2014-09-04 22:16:09 -05:00
mdipierro 6ba9f450b2 Merge pull request #489 from jonathannew/master
fix custom view delimiters
2014-09-04 22:06:29 -05:00
mdipierro d1d85e9614 Merge pull request #488 from niphlod/fix/scheduler
avoid multiple cascade paths
2014-09-04 22:05:14 -05:00
mdipierro 6649721a7d Merge pull request #485 from dokime7/patch-5
Fix LOAD on action @request.restful()
2014-09-04 22:04:35 -05:00
mdipierro a51007949f Merge pull request #487 from ilvalle/grid-fix
fix grid groupby with more than 2 Fields in the expression
2014-09-04 22:04:05 -05:00
Jonathan New 8c5422d2d6 fix custom view delimiters 2014-09-04 19:16:00 +08:00
mdipierro b8a29a67aa typo in try_create_web2py_filesystem 2014-09-03 17:14:00 -05:00
mdipierro 3902cb0b27 support for multiple db filesystems, thanks Luca 2014-09-03 16:37:19 -05:00
mdipierro d744a99e13 fixed partially problem with web2py_filesystem on GAE 2014-09-03 16:29:05 -05:00
niphlod 1456c0da1e references can be long too 2014-09-03 21:23:24 +02:00
niphlod fa5100cb2a avoid multiple cascade paths 2014-09-03 21:09:09 +02:00
ilvalle 9b9a5034ad fix grid groupby with more than 2 Fields in the expression 2014-09-03 19:30:22 +02:00
mdipierro d1e4ede9b3 fixed problem with delimiters, thanks Anthony 2014-09-03 10:52:31 -05:00
mdipierro f1ab50fb91 fixed a problem with reset_password 2014-09-02 12:17:26 -05:00
Jeremie Dokime 52fac63b9e Fix LOAD on action @request.restful()
LOAD didn't work on action decorated with @request.restful() when args and/or vars are passed because the restful method is called with the main "request" object (browser url action) instead of the "other_request" object used by LOAD.
So I have injected the restful method with the good object context.
It's a bit nasty, so if someone knows how to do it better, I'm happy.
2014-09-02 18:16:51 +02:00
mdipierro d73c668f2d Key.from_path -> self.keyfunc, thanks Quint 2014-09-02 10:10:14 -05:00
12 changed files with 772 additions and 669 deletions
+4 -1
View File
@@ -1,10 +1,13 @@
## 2.9.6 ## 2.9.6-2.9.7
- fixed support of GAE + SQL
- fixed a typo in the license of some login_methods code. It is now LGPL consistently with the rest of the web2py code. This change applied to all previous web2py versions. - fixed a typo in the license of some login_methods code. It is now LGPL consistently with the rest of the web2py code. This change applied to all previous web2py versions.
- support for SAML2 (with pysaml2) - support for SAML2 (with pysaml2)
- Sphinx documentation (thanks Niphlod) - Sphinx documentation (thanks Niphlod)
- improved scheduler (thanks Niphlod) - improved scheduler (thanks Niphlod)
- increased security - increased security
- better cache.dick (thanks Leonel)
- sessions are stored in subfolders for speed
- postgres support for "INSERT ... RETURING ..." - postgres support for "INSERT ... RETURING ..."
- ldap support for Certificate Authority (thanks Maggs and Shane) - ldap support for Certificate Authority (thanks Maggs and Shane)
- improved support for S/Mime X.509 (thanks Gyuris) - improved support for S/Mime X.509 (thanks Gyuris)
+10 -10
View File
@@ -30,20 +30,20 @@ update:
echo "remember that pymysql was tweaked" echo "remember that pymysql was tweaked"
src: src:
### Use semantic versioning ### Use semantic versioning
echo 'Version 2.9.6-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION echo 'Version 2.9.7-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
### rm -f all junk files ### rm -f all junk files
make clean make clean
### clean up baisc apps ### clean up baisc apps
rm -f routes.py rm -f routes.py
rm -f applications/*/sessions/* rm -rf applications/*/sessions/*
rm -f applications/*/errors/* | echo 'too many files' rm -rf applications/*/errors/* | echo 'too many files'
rm -f applications/*/cache/* rm -rf applications/*/cache/*
rm -f applications/admin/databases/* rm -rf applications/admin/databases/*
rm -f applications/welcome/databases/* rm -rf applications/welcome/databases/*
rm -f applications/examples/databases/* rm -rf applications/examples/databases/*
rm -f applications/admin/uploads/* rm -rf applications/admin/uploads/*
rm -f applications/welcome/uploads/* rm -rf applications/welcome/uploads/*
rm -f applications/examples/uploads/* rm -rf applications/examples/uploads/*
### NO MORE make epydoc ### NO MORE make epydoc
# make epydoc # make epydoc
### make welcome layout and appadmin the default ### make welcome layout and appadmin the default
+1 -1
View File
@@ -1 +1 @@
Version 2.9.6-stable+timestamp.2014.09.01.20.55.31 Version 2.9.7-stable+timestamp.2014.09.04.22.37.07
+119 -92
View File
@@ -20,20 +20,27 @@ caching will be provided by the GAE memcache
(see gluon.contrib.gae_memcache) (see gluon.contrib.gae_memcache)
""" """
import time import time
import portalocker import shutil
import shelve
import thread import thread
import os import os
import sys
import logging import logging
import re import re
import hashlib import hashlib
import datetime import datetime
import tempfile
from gluon import recfile
try: try:
from gluon import settings from gluon import settings
have_settings = True have_settings = True
except ImportError: except ImportError:
have_settings = False have_settings = False
try:
import cPickle as pickle
except:
import pickle
logger = logging.getLogger("web2py.cache") logger = logging.getLogger("web2py.cache")
__all__ = ['Cache', 'lazy_cache'] __all__ = ['Cache', 'lazy_cache']
@@ -122,7 +129,7 @@ class CacheAbstract(object):
Auxiliary function called by `clear` to search and clear cache entries Auxiliary function called by `clear` to search and clear cache entries
""" """
r = re.compile(regex) r = re.compile(regex)
for (key, value) in storage.items(): for key in storage:
if r.match(str(key)): if r.match(str(key)):
del storage[key] del storage[key]
break break
@@ -242,7 +249,6 @@ class CacheOnDisk(CacheAbstract):
This is implemented as a shelve object and it is shared by multiple web2py This is implemented as a shelve object and it is shared by multiple web2py
processes (and threads) as long as they share the same filesystem. processes (and threads) as long as they share the same filesystem.
The file is locked when accessed.
Disk cache provides persistance when web2py is started/stopped but it slower Disk cache provides persistance when web2py is started/stopped but it slower
than `CacheInRam` than `CacheInRam`
@@ -250,137 +256,158 @@ class CacheOnDisk(CacheAbstract):
Values stored in disk cache must be pickable. Values stored in disk cache must be pickable.
""" """
def _close_shelve_and_unlock(self): class PersistentStorage(object):
"""
Implements a key based storage in disk.
"""
def __init__(self, folder):
self.folder = folder
# Check the best way to do atomic file replacement.
if sys.version_info >= (3, 3):
self.replace = os.replace
elif sys.platform == "win32":
import ctypes
from ctypes import wintypes
ReplaceFile = ctypes.windll.kernel32.ReplaceFileW
ReplaceFile.restype = wintypes.BOOL
ReplaceFile.argtypes = [
wintypes.LPWSTR,
wintypes.LPWSTR,
wintypes.LPWSTR,
wintypes.DWORD,
wintypes.LPVOID,
wintypes.LPVOID,
]
def replace_windows(src, dst):
if not ReplaceFile(dst, src, None, 0, 0, 0):
os.rename(src, dst)
self.replace = replace_windows
else:
# POSIX rename() is always atomic
self.replace = os.rename
def __setitem__(self, key, value):
tmp_name, tmp_path = tempfile.mkstemp(dir=self.folder)
tmp = os.fdopen(tmp_name, 'wb')
try: try:
if self.storage: pickle.dump((time.time(), value), tmp, pickle.HIGHEST_PROTOCOL)
self.storage.close()
except ValueError:
pass
finally: finally:
self.storage = None tmp.close()
if self.locker and self.locked: fullfilename = os.path.join(self.folder, recfile.generate(key))
portalocker.unlock(self.locker) if not os.path.exists(os.path.dirname(fullfilename)):
self.locker.close() os.makedirs(os.path.dirname(fullfilename))
self.locked = False self.replace(tmp_path, fullfilename)
def _open_shelve_and_lock(self):
"""Open and return a shelf object, obtaining an exclusive lock
on self.locker first. Replaces the close method of the
returned shelf instance with one that releases the lock upon
closing."""
storage = None def __getitem__(self, key):
locker = None if recfile.exists(key, path=self.folder):
locked = False timestamp, value = pickle.load(recfile.open(key, 'rb', path=self.folder))
return value
else:
raise KeyError
def __contains__(self, key):
return recfile.exists(key, path=self.folder)
def __delitem__(self, key):
recfile.remove(key, path=self.folder)
def __iter__(self):
for dirpath, dirnames, filenames in os.walk(self.folder):
for filename in filenames:
yield filename
def get(self, key, default=None):
try: try:
locker = locker = open(self.locker_name, 'a') return self[key]
portalocker.lock(locker, portalocker.LOCK_EX) except KeyError:
locked = True return default
try:
storage = shelve.open(self.shelve_name)
except: def clear(self):
logger.error('corrupted cache file %s, will try rebuild it' for key in self:
% self.shelve_name) del self[key]
storage = None
if storage is None:
if os.path.exists(self.shelve_name):
os.unlink(self.shelve_name)
storage = shelve.open(self.shelve_name)
if not CacheAbstract.cache_stats_name in storage.keys():
storage[CacheAbstract.cache_stats_name] = {
'hit_total': 0, 'misses': 0}
storage.sync()
except Exception, e:
if storage:
storage.close()
storage = None
if locked:
portalocker.unlock(locker)
locker.close()
locked = False
raise RuntimeError(
'unable to create/re-create cache file %s' % self.shelve_name)
self.locker = locker
self.locked = locked
self.storage = storage
return storage
def __init__(self, request=None, folder=None): def __init__(self, request=None, folder=None):
self.initialized = False self.initialized = False
self.request = request self.request = request
self.folder = folder self.folder = folder
self.storage = {} self.storage = None
def initialize(self): def initialize(self):
if self.initialized: if self.initialized:
return return
else: else:
self.initialized = True self.initialized = True
folder = self.folder folder = self.folder
request = self.request request = self.request
# Lets test if the cache folder exists, if not # Lets test if the cache folder exists, if not
# we are going to create it # we are going to create it
folder = folder or os.path.join(request.folder, 'cache') folder = os.path.join(folder or request.folder, 'cache')
if not os.path.exists(folder): if not os.path.exists(folder):
os.mkdir(folder) os.mkdir(folder)
### we need this because of a possible bug in shelve that may self.storage = CacheOnDisk.PersistentStorage(folder)
### or may not lock
self.locker_name = os.path.join(folder, 'cache.lock') if not CacheAbstract.cache_stats_name in self.storage:
self.shelve_name = os.path.join(folder, 'cache.shelve') self.storage[CacheAbstract.cache_stats_name] = {'hit_total': 0, 'misses': 0}
def clear(self, regex=None):
self.initialize()
storage = self._open_shelve_and_lock()
try:
if regex is None:
storage.clear()
else:
self._clear(storage, regex)
storage.sync()
finally:
self._close_shelve_and_unlock()
def __call__(self, key, f, def __call__(self, key, f,
time_expire=DEFAULT_TIME_EXPIRE): time_expire=DEFAULT_TIME_EXPIRE):
self.initialize() self.initialize()
dt = time_expire dt = time_expire
storage = self._open_shelve_and_lock() item = self.storage.get(key)
try: self.storage[CacheAbstract.cache_stats_name]['hit_total'] += 1
item = storage.get(key, None)
storage[CacheAbstract.cache_stats_name]['hit_total'] += 1
if item and f is None: if item and f is None:
del storage[key] del self.storage[key]
storage.sync()
now = time.time()
if f is None: if f is None:
value = None return None
elif item and (dt is None or item[0] > now - dt):
now = time.time()
if item and ((dt is None) or (item[0] > now - dt)):
value = item[1] value = item[1]
else: else:
value = f() value = f()
storage[key] = (now, value) self.storage[key] = (now, value)
storage[CacheAbstract.cache_stats_name]['misses'] += 1 self.storage[CacheAbstract.cache_stats_name]['misses'] += 1
storage.sync()
finally:
self._close_shelve_and_unlock()
return value return value
def clear(self, regex=None):
self.initialize()
storage = self.storage
if regex is None:
storage.clear()
else:
self._clear(storage, regex)
if not CacheAbstract.cache_stats_name in storage:
storage[CacheAbstract.cache_stats_name] = {
'hit_total': 0, 'misses': 0}
def increment(self, key, value=1): def increment(self, key, value=1):
self.initialize() self.initialize()
storage = self._open_shelve_and_lock() self.storage[key] += value
try:
if key in storage:
value = storage[key][1] + value
storage[key] = (time.time(), value)
storage.sync()
finally:
self._close_shelve_and_unlock()
return value return value
class CacheAction(object): class CacheAction(object):
def __init__(self, func, key, time_expire, cache, cache_model): def __init__(self, func, key, time_expire, cache, cache_model):
self.__name__ = func.__name__ self.__name__ = func.__name__
+2 -1
View File
@@ -38,6 +38,7 @@ import marshal
import shutil import shutil
import imp import imp
import logging import logging
import types
logger = logging.getLogger("web2py") logger = logging.getLogger("web2py")
from gluon import rewrite from gluon import rewrite
from custom_import import custom_import_install from custom_import import custom_import_install
@@ -211,7 +212,7 @@ def LOAD(c=None, f='index', args=None, vars=None,
request.env.path_info request.env.path_info
other_request.cid = target other_request.cid = target
other_request.env.http_web2py_component_element = target other_request.env.http_web2py_component_element = target
other_request.restful = request.restful # Needed when you call LOAD() on a controller who has some actions decorates with @request.restful() other_request.restful = types.MethodType(request.restful.im_func, other_request) # A bit nasty but needed to use LOAD on action decorates with @request.restful()
other_response.view = '%s/%s.%s' % (c, f, other_request.extension) other_response.view = '%s/%s.%s' % (c, f, other_request.extension)
other_environment = copy.copy(current.globalenv) # NASTY other_environment = copy.copy(current.globalenv) # NASTY
+20 -14
View File
@@ -198,7 +198,6 @@ if PYTHON_VERSION[:2] < (2, 7):
else: else:
from collections import OrderedDict from collections import OrderedDict
CALLABLETYPES = (types.LambdaType, types.FunctionType, CALLABLETYPES = (types.LambdaType, types.FunctionType,
types.BuiltinFunctionType, types.BuiltinFunctionType,
types.MethodType, types.BuiltinMethodType) types.MethodType, types.BuiltinMethodType)
@@ -4587,24 +4586,28 @@ class CubridAdapter(MySQLAdapter):
######## GAE MySQL ########## ######## GAE MySQL ##########
class DatabaseStoredFile: class DatabaseStoredFile:
web2py_filesystem = False web2py_filesystems = set()
def escape(self, obj): def escape(self, obj):
return self.db._adapter.escape(obj) return self.db._adapter.escape(obj)
@staticmethod
def try_create_web2py_filesystem(db):
if not db._uri in DatabaseStoredFile.web2py_filesystems:
if db._adapter.dbengine == 'mysql':
sql = "CREATE TABLE IF NOT EXISTS web2py_filesystem (path VARCHAR(255), content LONGTEXT, PRIMARY KEY(path) ) ENGINE=InnoDB;"
elif db._adapter.dbengine in ('postgres', 'sqlite'):
sql = "CREATE TABLE IF NOT EXISTS web2py_filesystem (path VARCHAR(255), content TEXT, PRIMARY KEY(path));"
db.executesql(sql)
DatabaseStoredFile.web2py_filesystems.add(db._uri)
def __init__(self, db, filename, mode): def __init__(self, db, filename, mode):
if not db._adapter.dbengine in ('mysql', 'postgres', 'sqlite'): if not db._adapter.dbengine in ('mysql', 'postgres', 'sqlite'):
raise RuntimeError("only MySQL/Postgres/SQLite can store metadata .table files in database for now") raise RuntimeError("only MySQL/Postgres/SQLite can store metadata .table files in database for now")
self.db = db self.db = db
self.filename = filename self.filename = filename
self.mode = mode self.mode = mode
if not self.web2py_filesystem: DatabaseStoredFile.try_create_web2py_filesystem(db)
if db._adapter.dbengine == 'mysql':
sql = "CREATE TABLE IF NOT EXISTS web2py_filesystem (path VARCHAR(255), content LONGTEXT, PRIMARY KEY(path) ) ENGINE=InnoDB;"
elif db._adapter.dbengine in ('postgres', 'sqlite'):
sql = "CREATE TABLE IF NOT EXISTS web2py_filesystem (path VARCHAR(255), content TEXT, PRIMARY KEY(path));"
self.db.executesql(sql)
DatabaseStoredFile.web2py_filesystem = True
self.p = 0 self.p = 0
self.data = '' self.data = ''
if mode in ('r', 'rw', 'a'): if mode in ('r', 'rw', 'a'):
@@ -4655,6 +4658,9 @@ class DatabaseStoredFile:
def exists(db, filename): def exists(db, filename):
if exists(filename): if exists(filename):
return True return True
DatabaseStoredFile.try_create_web2py_filesystem(db)
query = "SELECT path FROM web2py_filesystem WHERE path='%s'" % filename query = "SELECT path FROM web2py_filesystem WHERE path='%s'" % filename
try: try:
if db.executesql(query): if db.executesql(query):
@@ -5137,35 +5143,35 @@ class GoogleDatastoreAdapter(NoSQLAdapter):
return [GAEF(first.name, '!=', self.represent(second, first.type), lambda a, b:a!=b)] return [GAEF(first.name, '!=', self.represent(second, first.type), lambda a, b:a!=b)]
else: else:
if not second is None: if not second is None:
second = Key.from_path(first._tablename, long(second)) second = self.keyfunc(first._tablename, long(second))
return [GAEF(first.name, '!=', second, lambda a, b:a!=b)] return [GAEF(first.name, '!=', second, lambda a, b:a!=b)]
def LT(self, first, second=None): def LT(self, first, second=None):
if first.type != 'id': if first.type != 'id':
return [GAEF(first.name, '<', self.represent(second, first.type), lambda a, b:a<b)] return [GAEF(first.name, '<', self.represent(second, first.type), lambda a, b:a<b)]
else: else:
second = Key.from_path(first._tablename, long(second)) second = self.keyfunc(first._tablename, long(second))
return [GAEF(first.name, '<', second, lambda a, b:a<b)] return [GAEF(first.name, '<', second, lambda a, b:a<b)]
def LE(self, first, second=None): def LE(self, first, second=None):
if first.type != 'id': if first.type != 'id':
return [GAEF(first.name, '<=', self.represent(second, first.type), lambda a, b:a<=b)] return [GAEF(first.name, '<=', self.represent(second, first.type), lambda a, b:a<=b)]
else: else:
second = Key.from_path(first._tablename, long(second)) second = self.keyfunc(first._tablename, long(second))
return [GAEF(first.name, '<=', second, lambda a, b:a<=b)] return [GAEF(first.name, '<=', second, lambda a, b:a<=b)]
def GT(self, first, second=None): def GT(self, first, second=None):
if first.type != 'id' or second==0 or second == '0': if first.type != 'id' or second==0 or second == '0':
return [GAEF(first.name, '>', self.represent(second, first.type), lambda a, b:a>b)] return [GAEF(first.name, '>', self.represent(second, first.type), lambda a, b:a>b)]
else: else:
second = Key.from_path(first._tablename, long(second)) second = self.keyfunc(first._tablename, long(second))
return [GAEF(first.name, '>', second, lambda a, b:a>b)] return [GAEF(first.name, '>', second, lambda a, b:a>b)]
def GE(self, first, second=None): def GE(self, first, second=None):
if first.type != 'id': if first.type != 'id':
return [GAEF(first.name, '>=', self.represent(second, first.type), lambda a, b:a>=b)] return [GAEF(first.name, '>=', self.represent(second, first.type), lambda a, b:a>=b)]
else: else:
second = Key.from_path(first._tablename, long(second)) second = self.keyfunc(first._tablename, long(second))
return [GAEF(first.name, '>=', second, lambda a, b:a>=b)] return [GAEF(first.name, '>=', second, lambda a, b:a>=b)]
def INVERT(self, first): def INVERT(self, first):
+3 -3
View File
@@ -25,6 +25,7 @@ from gluon.serializers import json, custom_json
import gluon.settings as settings import gluon.settings as settings
from gluon.utils import web2py_uuid, secure_dumps, secure_loads from gluon.utils import web2py_uuid, secure_dumps, secure_loads
from gluon.settings import global_settings from gluon.settings import global_settings
from gluon import recfile
import hashlib import hashlib
import portalocker import portalocker
import cPickle import cPickle
@@ -165,7 +166,6 @@ class Request(Storage):
- is_local - is_local
- is_https - is_https
- restful() - restful()
- settings
""" """
def __init__(self, env): def __init__(self, env):
@@ -825,7 +825,7 @@ class Session(Storage):
'sessions', response.session_id) 'sessions', response.session_id)
try: try:
response.session_file = \ response.session_file = \
open(response.session_filename, 'rb+') recfile.open(response.session_filename, 'rb+')
portalocker.lock(response.session_file, portalocker.lock(response.session_file,
portalocker.LOCK_EX) portalocker.LOCK_EX)
response.session_locked = True response.session_locked = True
@@ -1147,7 +1147,7 @@ class Session(Storage):
session_folder = os.path.dirname(response.session_filename) session_folder = os.path.dirname(response.session_filename)
if not os.path.exists(session_folder): if not os.path.exists(session_folder):
os.mkdir(session_folder) os.mkdir(session_folder)
response.session_file = open(response.session_filename, 'wb') response.session_file = recfile.open(response.session_filename, 'wb')
portalocker.lock(response.session_file, portalocker.LOCK_EX) portalocker.lock(response.session_file, portalocker.LOCK_EX)
response.session_locked = True response.session_locked = True
if response.session_file: if response.session_file:
+57
View File
@@ -0,0 +1,57 @@
import os, uuid
def generate(filename, depth=2, base=512):
dummyhash = sum(ord(c)*256**(i % 4) for i,c in enumerate(filename)) % base**depth
folders = []
for level in range(depth-1,-1,-1):
code, dummyhash = divmod(dummyhash, base**level)
folders.append("%03x" % code)
folders.append(filename)
return os.path.join(*folders)
def exists(filename, path=None):
if os.path.exists(filename):
return True
if path is None:
path, filename = os.path.split(filename)
fullfilename = os.path.join(path, generate(filename))
if os.path.exists(fullfilename):
return True
return False
def remove(filename, path=None):
if os.path.exists(filename):
return os.unlink(filename)
if path is None:
path, filename = os.path.split(filename)
fullfilename = os.path.join(path, generate(filename))
if os.path.exists(fullfilename):
return os.unlink(fullfilename)
raise IOError
def open(filename, mode="r", path=None):
if not path:
path, filename = os.path.split(filename)
fullfilename = None
if not mode.startswith('w'):
fullfilename = os.path.join(path, filename)
if not os.path.exists(fullfilename):
fullfilename = None
if not fullfilename:
fullfilename = os.path.join(path, generate(filename))
if mode.startswith('w') and not os.path.exists(os.path.dirname(fullfilename)):
os.makedirs(os.path.dirname(fullfilename))
return file(fullfilename, mode)
def test():
if not os.path.exists('tests'):
os.mkdir('tests')
for k in range(20):
filename = os.path.join('tests',str(uuid.uuid4())+'.test')
open(filename, "w").write('test')
assert open(filename, "r").read()=='test'
if exists(filename):
remove(filename)
if __name__ == '__main__':
test()
+7 -4
View File
@@ -96,7 +96,7 @@ IDENTIFIER = "%s#%s" % (socket.gethostname(),os.getpid())
logger = logging.getLogger('web2py.scheduler.%s' % IDENTIFIER) logger = logging.getLogger('web2py.scheduler.%s' % IDENTIFIER)
from gluon import DAL, Field, IS_NOT_EMPTY, IS_IN_SET, IS_NOT_IN_DB from gluon import DAL, Field, IS_NOT_EMPTY, IS_IN_SET, IS_NOT_IN_DB
from gluon import IS_INT_IN_RANGE, IS_DATETIME from gluon import IS_INT_IN_RANGE, IS_DATETIME, IS_IN_DB
from gluon.utils import web2py_uuid from gluon.utils import web2py_uuid
from gluon.storage import Storage from gluon.storage import Storage
@@ -671,7 +671,10 @@ class Scheduler(MetaScheduler):
db.define_table( db.define_table(
'scheduler_task_deps', 'scheduler_task_deps',
Field('job_name', default='job_0'), Field('job_name', default='job_0'),
Field('task_parent', 'reference scheduler_task'), Field('task_parent', 'integer',
requires=IS_IN_DB(db, 'scheduler_task.id',
'%(task_name)s')
),
Field('task_child', 'reference scheduler_task'), Field('task_child', 'reference scheduler_task'),
Field('can_visit', 'boolean', default=False), Field('can_visit', 'boolean', default=False),
migrate=self.__get_migrate('scheduler_task_deps', migrate) migrate=self.__get_migrate('scheduler_task_deps', migrate)
@@ -1311,7 +1314,7 @@ class Scheduler(MetaScheduler):
""" """
from gluon.dal import Query from gluon.dal import Query
sr, st = self.db.scheduler_run, self.db.scheduler_task sr, st = self.db.scheduler_run, self.db.scheduler_task
if isinstance(ref, int): if isinstance(ref, (int, long)):
q = st.id == ref q = st.id == ref
elif isinstance(ref, str): elif isinstance(ref, str):
q = st.uuid == ref q = st.uuid == ref
@@ -1362,7 +1365,7 @@ class Scheduler(MetaScheduler):
Experimental Experimental
""" """
st, sw = self.db.scheduler_task, self.db.scheduler_worker st, sw = self.db.scheduler_task, self.db.scheduler_worker
if isinstance(ref, int): if isinstance(ref, (int, long)):
q = st.id == ref q = st.id == ref
elif isinstance(ref, str): elif isinstance(ref, str):
q = st.uuid == ref q = st.uuid == ref
+2
View File
@@ -2111,6 +2111,8 @@ class SQLFORM(FORM):
field_id = groupby #take the field passed as groupby field_id = groupby #take the field passed as groupby
elif groupby and isinstance(groupby, Expression): elif groupby and isinstance(groupby, Expression):
field_id = groupby.first #take the first groupby field field_id = groupby.first #take the first groupby field
while not(isinstance(field_id, Field)): # Navigate to the first Field of the expression
field_id = field_id.first
table = field_id.table table = field_id.table
tablename = table._tablename tablename = table._tablename
if not any(str(f) == str(field_id) for f in fields): if not any(str(f) == str(field_id) for f in fields):
+10 -6
View File
@@ -279,15 +279,19 @@ class TemplateParser(object):
self.context = context self.context = context
# allow optional alternative delimiters # allow optional alternative delimiters
if delimiters is None:
delimiters = context.get('response', {})\
.get('app_settings',{}).get('template_delimiters')
if delimiters != self.default_delimiters: if delimiters != self.default_delimiters:
escaped_delimiters = (escape(elimiters[0]), escaped_delimiters = (escape(delimiters[0]),
escape(delimiters[1])) escape(delimiters[1]))
self.r_tag = compile(r'(%s.*?%s)' % escaped_delimiters, DOTALL) self.r_tag = compile(r'(%s.*?%s)' % escaped_delimiters, DOTALL)
else: elif hasattr(context.get('response', None), 'delimiters'):
delimiters = self.default_delimiters if context['response'].delimiters != self.default_delimiters:
delimiters = context['response'].delimiters
escaped_delimiters = (
escape(delimiters[0]),
escape(delimiters[1]))
self.r_tag = compile(r'(%s.*?%s)' % escaped_delimiters,
DOTALL)
self.delimiters = delimiters self.delimiters = delimiters
# Create a root level Content that everything will go into. # Create a root level Content that everything will go into.
+1 -1
View File
@@ -3007,7 +3007,7 @@ class Auth(object):
if self.settings.prevent_password_reset_attacks: if self.settings.prevent_password_reset_attacks:
key = request.vars.key key = request.vars.key
if not key and len(request.args)>1: if not key and len(request.args)>0:
key = request.args[-1] key = request.args[-1]
if key: if key:
session._reset_password_key = key session._reset_password_key = key