diff --git a/VERSION b/VERSION
index 7ba0fb63..fa8769ff 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-Version 2.0.9 (2012-09-18 13:39:12) stable
+Version 2.0.9 (2012-09-20 13:25:48) stable
diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py
index 0cb2e8de..a4d527a6 100644
--- a/applications/admin/controllers/default.py
+++ b/applications/admin/controllers/default.py
@@ -1652,7 +1652,6 @@ def git_pull():
session.flash = T("Application updated via git pull")
redirect(URL('site'))
except CheckoutError, message:
- logging.error(message)
session.flash = T("Pull failed, certain files could not be checked out. Check logs for details.")
redirect(URL('site'))
except UnmergedEntriesError:
@@ -1662,11 +1661,9 @@ def git_pull():
session.flash = T("Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.")
redirect(URL('site'))
except GitCommandError, status:
- logging.error(str(status))
session.flash = T("Pull failed, git exited abnormally. See logs for details.")
redirect(URL('site'))
except Exception,e:
- logging.error("Unexpected error:", sys.exc_info()[0])
session.flash = T("Pull failed, git exited abnormally. See logs for details.")
redirect(URL('site'))
elif 'cancel' in request.vars:
@@ -1698,7 +1695,6 @@ def git_push():
session.flash = T("Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.")
redirect(URL('site'))
except Exception, e:
- logging.error("Unexpected error:", sys.exc_info()[0])
session.flash = T("Push failed, git exited abnormally. See logs for details.")
redirect(URL('site'))
return dict(app=app,form=form)
diff --git a/applications/admin/views/default/edit.html b/applications/admin/views/default/edit.html
index 755dd0b2..6555ab50 100644
--- a/applications/admin/views/default/edit.html
+++ b/applications/admin/views/default/edit.html
@@ -19,6 +19,10 @@
+
+
+
+
{{elif TEXT_EDITOR == 'ace':}}
@@ -246,6 +250,11 @@ window.onload = function() {
{{=shortcut('Ctrl+S', T('Save via Ajax'))}}
{{=shortcut('Ctrl+F11', T('Toggle Fullscreen'))}}
+ {{=shortcut('Ctrl-F / Cmd-F', T('Start searching'))}}
+ {{=shortcut('Ctrl-G / Cmd-G', T('Find Next'))}}
+ {{=shortcut('Shift-Ctrl-G / Shift-Cmd-G', T('Find Previous'))}}
+ {{=shortcut('Shift-Ctrl-F / Cmd-Option-F', T('Replace'))}}
+ {{=shortcut('Shift-Ctrl-R / Shift-Cmd-Option-F', T('Replace All'))}}
{{=shortcut('Tab', T('Expand Abbreviation'))}}
{{elif TEXT_EDITOR == 'codemirror':}}
@@ -253,6 +262,11 @@ window.onload = function() {
{{=shortcut('Ctrl+S', T('Save via Ajax'))}}
{{=shortcut('Ctrl+F11', T('Toggle Fullscreen'))}}
+ {{=shortcut('Ctrl-F / Cmd-F', T('Start searching'))}}
+ {{=shortcut('Ctrl-G / Cmd-G', T('Find Next'))}}
+ {{=shortcut('Shift-Ctrl-G / Shift-Cmd-G', T('Find Previous'))}}
+ {{=shortcut('Shift-Ctrl-F / Cmd-Option-F', T('Replace'))}}
+ {{=shortcut('Shift-Ctrl-R / Shift-Cmd-Option-F', T('Replace All'))}}
{{else:}}
{{=T("Key bindings")}}
diff --git a/applications/welcome/models/menu.py b/applications/welcome/models/menu.py
index 784f4a4a..dcf1c275 100644
--- a/applications/welcome/models/menu.py
+++ b/applications/welcome/models/menu.py
@@ -33,9 +33,9 @@ def _():
# shortcuts
app = request.application
ctr = request.controller
- # useful links to internal and external resources
+ # useful links to internal and external resources
response.menu+=[
- (SPAN('web2py',_style='color:yellow'),False, 'http://web2py.com', [
+ (SPAN('web2py',_class='highlighted'),False, 'http://web2py.com', [
(T('My Sites'),False,URL('admin','default','site')),
(T('This App'),False,URL('admin','default','design/%s' % app), [
(T('Controller'),False,
diff --git a/applications/welcome/static/css/web2py_bootstrap.css b/applications/welcome/static/css/web2py_bootstrap.css
index 0c2cf74c..74534cb3 100644
--- a/applications/welcome/static/css/web2py_bootstrap.css
+++ b/applications/welcome/static/css/web2py_bootstrap.css
@@ -70,7 +70,15 @@ div.controls .error{
//display:inline; /* uncommenting this, the animation effect is lost */
}
div.controls .inline-help{color:#3A87AD;}
-div.controls .error_wrapper+.inline-help{margin-left:-99999px}
+div.controls .error_wrapper+.inline-help{margin-left:-99999px;}
+/* beautify brand */
+.navbar-inverse .brand{color:#c6cecc;}
+.navbar-inverse .brand b{display:inline-block;margin-top:-1px;}
+.navbar-inverse .brand b>span{font-size:22px;color:white}
+.navbar-inverse .brand:hover b>span{color:white}
+/* beautify web2py link in navbar */
+span.highlighted{color:#d8d800;}
+.open span.highlighted{color:#ffff00;}
/*=============================================================
OVERRIDING WEB2PY.CSS RULES
@@ -185,6 +193,8 @@ td.w2p_fw ul{margin-left:0px;}
margin:3px 0 0 2px;
}
.web2py_grid form table{width:auto;}
+/* auth_user_remember checkbox extrapadding in IE fix */
+.ie-lte9 input#auth_user_remember.checkbox {padding-left:0;}
/*=============================================================
MEDIA QUERIES
diff --git a/applications/welcome/static/css/web2py_bootstrap_nojs.css b/applications/welcome/static/css/web2py_bootstrap_nojs.css
index 2b75915e..0ec7312f 100644
--- a/applications/welcome/static/css/web2py_bootstrap_nojs.css
+++ b/applications/welcome/static/css/web2py_bootstrap_nojs.css
@@ -1,3 +1,23 @@
+/*=============================================================
+ BOOTSTRAP DROPDOWN MENU
+==============================================================*/
+
+.dropdown-menu ul{
+ left:100%;
+ position:absolute;
+ top:0;
+ visibility:hidden;
+ margin-top:-1px;
+}
+.dropdown-menu li:hover ul{visibility:visible;}
+.navbar .dropdown-menu ul:before{
+ border-bottom:7px solid transparent;
+ border-left:none;
+ border-right:7px solid rgba(0, 0, 0, 0.2);
+ border-top:7px solid transparent;
+ left:-7px;
+ top:5px;
+}
.nav > li.dropdown > a:after {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
@@ -15,8 +35,7 @@
border-bottom-color: #FFFFFF;
border-top-color: #FFFFFF;
}
-
-
+.dropdown-menu span{display:inline-block;}
ul.dropdown-menu li.dropdown > a:after {
border-left: 4px solid #000;
border-right: 4px solid transparent;
@@ -35,4 +54,69 @@ ul.dropdown-menu li.dropdown > a:after {
ul.nav li.dropdown:hover ul.dropdown-menu {
display: block;
-}
\ No newline at end of file
+}
+
+.open >.dropdown-menu ul{display:block;} /* fix menu issue when BS2.0.4 is applied */
+
+/*=============================================================
+ BOOTSTRAP SUBMIT BUTTON
+==============================================================*/
+
+input[type='submit']:not(.btn) {
+display: inline-block;
+padding: 4px 14px;
+margin-bottom: 0;
+font-size: 14px;
+line-height: 20px;
+color: #333;
+text-align: center;
+text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
+vertical-align: middle;
+cursor: pointer;
+background-color: whiteSmoke;
+background-image: -webkit-gradient(linear,0 0,0 100%,from(white),to(#E6E6E6));
+background-image: -webkit-linear-gradient(top,white,#E6E6E6);
+background-image: -o-linear-gradient(top,white,#E6E6E6);
+background-image: linear-gradient(to bottom,white,#E6E6E6);
+background-image: -moz-linear-gradient(top,white,#E6E6E6);
+background-repeat: repeat-x;
+border: 1px solid #BBB;
+border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+border-bottom-color: #A2A2A2;
+-webkit-border-radius: 4px;
+-moz-border-radius: 4px;
+border-radius: 4px;
+filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);
+filter: progid:dximagetransform.microsoft.gradient(enabled=false);
+-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);
+-moz-box-shadow: inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);
+box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+input[type='submit']:not(.btn):hover {
+color: #333;
+text-decoration: none;
+background-color: #E6E6E6;
+background-position: 0 -15px;
+-webkit-transition: background-position .1s linear;
+-moz-transition: background-position .1s linear;
+-o-transition: background-position .1s linear;
+transition: background-position .1s linear;
+}
+
+input[type='submit']:not(.btn).active, input[type='submit']:not(.btn):active {
+background-color: #E6E6E6;
+background-color: #D9D9D9 9;
+background-image: none;
+outline: 0;
+-webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);
+-moz-box-shadow: inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);
+box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+/*=============================================================
+ OTHER
+==============================================================*/
+
+.ie-lte8 .navbar-fixed-top {position:static;}
+
diff --git a/applications/welcome/views/layout.html b/applications/welcome/views/layout.html
index d0a2fd2f..19225730 100644
--- a/applications/welcome/views/layout.html
+++ b/applications/welcome/views/layout.html
@@ -76,7 +76,7 @@
- web2py™
+ web2py™
{{='auth' in globals() and auth.navbar(mode="dropdown") or ''}}
{{is_mobile=request.user_agent().is_mobile}}
@@ -159,10 +159,10 @@
});
if(jQuery(document).width()>=980) {
jQuery('ul.nav a.dropdown-toggle').parent().hover(function() {
- mi = jQuery(this);
- mi.children('.dropdown-menu').stop(true, true).delay(200).fadeIn(400,function(){mi.addClass('open')});
+ mi = jQuery(this).addClass('open');
+ mi.children('.dropdown-menu').stop(true, true).delay(200).fadeIn(400);
}, function() {
- mi = jQuery(this);
+ mi = jQuery(this);
mi.children('.dropdown-menu').stop(true, true).delay(200).fadeOut(function(){mi.removeClass('open')});
});
}
diff --git a/gluon/dal.py b/gluon/dal.py
index e4fe3b1d..857bcf84 100644
--- a/gluon/dal.py
+++ b/gluon/dal.py
@@ -4323,7 +4323,8 @@ class GoogleDatastoreAdapter(NoSQLAdapter):
if first.type != 'id':
return [GAEF(first.name,'!=',self.represent(second,first.type),lambda a,b:a!=b)]
else:
- second = Key.from_path(first._tablename, long(second))
+ if not second is None:
+ second = Key.from_path(first._tablename, long(second))
return [GAEF(first.name,'!=',second,lambda a,b:a!=b)]
def LT(self,first,second=None):
diff --git a/gluon/languages.py b/gluon/languages.py
index d09e55bb..0b669d6b 100644
--- a/gluon/languages.py
+++ b/gluon/languages.py
@@ -155,9 +155,9 @@ def read_possible_plural_rules():
create list of all possible plural rules files
result is cached in PLURAL_RULES dictionary to increase speed
"""
+ plurals = {}
try:
- import gluon.contrib.plural_rules as package
- plurals = {}
+ import contrib.plural_rules as package
for importer, modname, ispkg in pkgutil.iter_modules(package.__path__):
if len(modname)==2:
module = __import__(package.__name__+'.'+modname,
diff --git a/gluon/rewrite.py b/gluon/rewrite.py
index bd7cd408..79ac23d0 100644
--- a/gluon/rewrite.py
+++ b/gluon/rewrite.py
@@ -192,6 +192,7 @@ def url_out(request, env, application, controller, function,
port = ''
else:
port = ':%s' % port
+ host = host.split(':')[0]
url = '%s://%s%s%s' % (scheme, host, port, url)
return url
diff --git a/gluon/tools.py b/gluon/tools.py
index 6e449085..6130a226 100644
--- a/gluon/tools.py
+++ b/gluon/tools.py
@@ -264,6 +264,7 @@ class Mail(object):
cc=None,
bcc=None,
reply_to=None,
+ sender='%(sender)s',
encoding='utf-8',
raw=False,
headers={}
@@ -606,7 +607,8 @@ class Mail(object):
# no cryptography process as usual
payload=payload_in
- payload['From'] = encoded_or_raw(self.settings.sender.decode(encoding))
+ sender = sender % dict(sender=self.settings.sender)
+ payload['From'] = encoded_or_raw(sender.decode(encoding))
origTo = to[:]
if to:
payload['To'] = encoded_or_raw(', '.join(to).decode(encoding))
@@ -623,10 +625,10 @@ class Mail(object):
for k,v in headers.iteritems():
payload[k] = encoded_or_raw(v.decode(encoding))
result = {}
- try:
+ try:
if self.settings.server == 'logging':
logger.warn('email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' % \
- ('-'*40,self.settings.sender,
+ ('-'*40,sender,
', '.join(to),subject,
text or html,'-'*40))
elif self.settings.server == 'gae':
@@ -1195,7 +1197,7 @@ class Auth(object):
'reset_password','request_reset_password',
'change_password','profile','groups',
'impersonate','not_authorized'):
- if len(request.args) >= 2:
+ if len(request.args) >= 2 and args[0]=='impersonate':
return getattr(self,args[0])(request.args[1])
else:
return getattr(self,args[0])()
@@ -2443,7 +2445,7 @@ class Auth(object):
session = current.session
if next is DEFAULT:
- next = self.settings.reset_password_next
+ next = self.next or self.settings.reset_password_next
try:
key = request.vars.key or getarg(-1)
t0 = int(key.split('-')[0])
diff --git a/gluon/validators.py b/gluon/validators.py
index 90c5b81b..457720a1 100644
--- a/gluon/validators.py
+++ b/gluon/validators.py
@@ -2725,6 +2725,43 @@ class CRYPT(object):
return ('', translate(self.error_message))
return (LazyCrypt(self,value),None)
+# entropy calculator for IS_STRONG
+#
+lowerset = frozenset(unicode('abcdefghijklmnopqrstuvwxyz'))
+upperset = frozenset(unicode('ABCDEFGHIJKLMNOPQRSTUVWXYZ'))
+numberset = frozenset(unicode('0123456789'))
+sym1set = frozenset(unicode('!@#$%^&*()'))
+sym2set = frozenset(unicode('~`-_=+[]{}\\|;:\'",.<>?/'))
+otherset = frozenset(unicode('0123456789abcdefghijklmnopqrstuvwxyz')) # anything else
+
+def calc_entropy(string):
+ " calculate a simple entropy for a given string "
+ import math
+ alphabet = 0 # alphabet size
+ other = set()
+ seen = set()
+ lastset = None
+ if isinstance(string, str):
+ string = unicode(string, encoding='utf8')
+ for c in string:
+ # classify this character
+ inset = otherset
+ for cset in (lowerset, upperset, numberset, sym1set, sym2set):
+ if c in cset:
+ inset = cset
+ break
+ # calculate effect of character on alphabet size
+ if inset not in seen:
+ seen.add(inset)
+ alphabet += len(inset) # credit for a new character set
+ elif c not in other:
+ alphabet += 1 # credit for unique characters
+ other.add(c)
+ if inset is not lastset:
+ alphabet += 1 # credit for set transitions
+ lastset = cset
+ entropy = len(string) * math.log(alphabet) / 0.6931471805599453 # math.log(2)
+ return round(entropy, 2)
class IS_STRONG(object):
"""
@@ -2734,23 +2771,61 @@ class IS_STRONG(object):
requires=IS_STRONG(min=10, special=2, upper=2))
enforces complexity requirements on a field
+
+ >>> IS_STRONG(es=True)('Abcd1234')
+ ('Abcd1234', 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|')
+ >>> IS_STRONG(es=True)('Abcd1234!')
+ ('Abcd1234!', None)
+ >>> IS_STRONG(es=True, entropy=1)('a')
+ ('a', None)
+ >>> IS_STRONG(es=True, entropy=1, min=2)('a')
+ ('a', 'Minimum length is 2')
+ >>> IS_STRONG(es=True, entropy=100)('abc123')
+ ('abc123', 'Entropy (32.35) less than required (100)')
+ >>> IS_STRONG(es=True, entropy=100)('and')
+ ('and', 'Entropy (14.57) less than required (100)')
+ >>> IS_STRONG(es=True, entropy=100)('aaa')
+ ('aaa', 'Entropy (14.42) less than required (100)')
+ >>> IS_STRONG(es=True, entropy=100)('a1d')
+ ('a1d', 'Entropy (15.97) less than required (100)')
+ >>> IS_STRONG(es=True, entropy=100)('añd')
+ ('a\\xc3\\xb1d', 'Entropy (18.13) less than required (100)')
+
"""
- def __init__(self, min=8, max=20, upper=1, lower=1, number=1,
- special=1, specials=r'~!@#$%^&*()_+-=?<>,.:;{}[]|',
- invalid=' "', error_message=None):
- self.min = min
- self.max = max
- self.upper = upper
- self.lower = lower
- self.number = number
- self.special = special
+ def __init__(self, min=None, max=None, upper=None, lower=None, number=None,
+ entropy=None,
+ special=None, specials=r'~!@#$%^&*()_+-=?<>,.:;{}[]|',
+ invalid=' "', error_message=None, es=False):
+ self.entropy = entropy
+ if entropy is None:
+ # enforce default requirements
+ self.min = 8 if min is None else min
+ self.max = max # was 20, but that doesn't make sense
+ self.upper = 1 if upper is None else upper
+ self.lower = 1 if lower is None else lower
+ self.number = 1 if number is None else number
+ self.special = 1 if special is None else special
+ else:
+ # by default, an entropy spec is exclusive
+ self.min = min
+ self.max = max
+ self.upper = upper
+ self.lower = lower
+ self.number = number
+ self.special = special
self.specials = specials
self.invalid = invalid
self.error_message = error_message
+ self.estring = es # return error message as string (for doctest)
def __call__(self, value):
failures = []
+ if self.entropy is not None:
+ entropy = calc_entropy(value)
+ if entropy < self.entropy:
+ failures.append(translate("Entropy (%(have)s) less than required (%(need)s)") \
+ % dict(have=entropy, need=self.entropy))
if type(self.min) == int and self.min > 0:
if not len(value) >= self.min:
failures.append(translate("Minimum length is %s") % self.min)
@@ -2761,7 +2836,7 @@ class IS_STRONG(object):
all_special = [ch in value for ch in self.specials]
if self.special > 0:
if not all_special.count(True) >= self.special:
- failures.append(translate("Must include at least %s of the following : %s") \
+ failures.append(translate("Must include at least %s of the following: %s") \
% (self.special, self.specials))
if self.invalid:
all_invalid = [ch in value for ch in self.invalid]
@@ -2801,6 +2876,8 @@ class IS_STRONG(object):
if len(failures) == 0:
return (value, None)
if not self.error_message:
+ if self.estring:
+ return (value, '|'.join(failures))
from html import XML
return (value, XML('
'.join(failures)))
else:
diff --git a/scripts/zip_static_files.py b/scripts/zip_static_files.py
index 10497b7a..67d342f0 100644
--- a/scripts/zip_static_files.py
+++ b/scripts/zip_static_files.py
@@ -3,32 +3,45 @@
## launch with python web2py.py -S myapp -R scripts/zip_static_files.py
-ALLOWED_EXTS = ['.css', '.js']
+
import os
import gzip
-static_path = os.path.abspath(os.path.join(request.folder, 'static'))
-filelist = []
-for root, dir, files in os.walk(static_path):
- for file in files:
- filelist.append(os.path.join(root, file))
-tsave = 0
-for fi in filelist:
- extension = os.path.splitext(fi)
- extension = len(extension) > 1 and extension[1] or None
- if not extension or extension not in ALLOWED_EXTS:
- print 'skipping %s' % os.path.basename(fi)
- continue
- fstats = os.stat(fi)
- atime, mtime = fstats.st_atime, fstats.st_mtime
- gfi = fi + '.gz'
- print 'gzipping %s to %s' % (os.path.basename(fi), os.path.basename(gfi))
- f_in = open(fi, 'rb')
- f_out = gzip.open(gfi, 'wb')
- f_out.writelines(f_in)
- f_out.close()
- f_in.close()
- os.utime(gfi, (atime,mtime))
- saved = fstats.st_size - os.stat(gfi).st_size
- tsave+= saved
-print 'saved %s KB' % (int(tsave)/1000.0)
+def zip_static(filelist=[]):
+ tsave = 0
+ for fi in filelist:
+ extension = os.path.splitext(fi)
+ extension = len(extension) > 1 and extension[1] or None
+ if not extension or extension not in ALLOWED_EXTS:
+ print 'skipping %s' % os.path.basename(fi)
+ continue
+ fstats = os.stat(fi)
+ atime, mtime = fstats.st_atime, fstats.st_mtime
+ gfi = fi + '.gz'
+ if os.path.isfile(gfi):
+ zstats = os.stat(gfi)
+ zatime, zmtime = zstats.st_atime, zstats.st_mtime
+ if zatime == atime and zmtime == mtime:
+ print 'skipping %s, already gzipped to the latest version' % os.path.basename(fi)
+ continue
+ print 'gzipping %s to %s' % (os.path.basename(fi), os.path.basename(gfi))
+ f_in = open(fi, 'rb')
+ f_out = gzip.open(gfi, 'wb')
+ f_out.writelines(f_in)
+ f_out.close()
+ f_in.close()
+ os.utime(gfi, (atime,mtime))
+ saved = fstats.st_size - os.stat(gfi).st_size
+ tsave+= saved
+
+ print 'saved %s KB' % (int(tsave)/1000.0)
+
+if __name__ == '__main__':
+ ALLOWED_EXTS = ['.css', '.js']
+ static_path = os.path.abspath(os.path.join(request.folder, 'static'))
+ filelist = []
+ for root, dir, files in os.walk(static_path):
+ for file in files:
+ filelist.append(os.path.join(root, file))
+
+ zip_static(filelist)