From 764ccd5005d30fb041444246537b3369d615e83d Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sat, 7 Jul 2012 10:28:21 -0500 Subject: [PATCH 01/98] scheduler patch, issue 878, thanks Toomim --- VERSION | 2 +- applications/examples/views/default/who.html | 1 + gluon/contrib/markmin/markmin2html.py | 2 +- gluon/scheduler.py | 22 +++++++++++++------- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/VERSION b/VERSION index 2ac640e6..f1f44883 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-06 11:47:22) dev +Version 2.00.0 (2012-07-07 10:28:16) dev diff --git a/applications/examples/views/default/who.html b/applications/examples/views/default/who.html index 278ad5f5..827b68a4 100644 --- a/applications/examples/views/default/who.html +++ b/applications/examples/views/default/who.html @@ -96,6 +96,7 @@
  • Mateusz Banach (stickers, IS_EMAIL, IS_IMAGE, contenttype)
  • Michael Willis (shell)
  • Michele Comitini (faceboook) +
  • Michael Toomim (scheduler)
  • Nathan Freeze (admin design, IS_STRONG, DAL features, web2pyslices.com)
  • Niall Sweeny (MSSQL support)
  • Niccolo Polo (epydoc) diff --git a/gluon/contrib/markmin/markmin2html.py b/gluon/contrib/markmin/markmin2html.py index e97f2877..9b7d52a7 100755 --- a/gluon/contrib/markmin/markmin2html.py +++ b/gluon/contrib/markmin/markmin2html.py @@ -363,7 +363,7 @@ def render(text,extra={},allowed={},sep='p',URL=None,environment=None,latex='goo return str(environment[a]) text = re.compile(r'@\{(?P\w+?)\}').sub(u2,text) if not URL is None: - # this is experimental @{controller/index/args} + # this is experimental @{function/args} # turns into a digitally signed URL def u1(match,URL=URL): f,args = match.group('f'), match.group('args') diff --git a/gluon/scheduler.py b/gluon/scheduler.py index 401cff6c..25b162b7 100644 --- a/gluon/scheduler.py +++ b/gluon/scheduler.py @@ -169,16 +169,21 @@ def executor(queue,task): try: if task.app: os.chdir(os.environ['WEB2PY_PATH']) - from gluon.shell import env + from gluon.shell import env, parse_path_info from gluon.dal import BaseAdapter from gluon import current level = logging.getLogger().getEffectiveLevel() logging.getLogger().setLevel(logging.WARN) - _env = env(task.app,import_models=True) + # Get controller-specific subdirectory if task.app is of + # form 'app/controller' + (a,c,f) = parse_path_info(task.app) + _env = env(a=a,c=c,import_models=True) logging.getLogger().setLevel(level) scheduler = current._scheduler - scheduler_tasks = current._scheduler.tasks - _function = scheduler_tasks[task.function] + f = task.function + # First look for the func in tasks, else look in models + _function = current._scheduler.tasks.get(f) or _env.get(f) + assert _function, 'Function %s not found in scheduler\'s environmen globals().update(_env) args = loads(task.args) vars = loads(task.vars, object_hook=_decode_dict) @@ -353,6 +358,7 @@ class Scheduler(MetaScheduler): def define_tables(self,db,migrate): from gluon import current + from gluon.dal import DEFAULT logging.debug('defining tables (migrate=%s)' % migrate) now = datetime.datetime.now() db.define_table( @@ -364,7 +370,8 @@ class Scheduler(MetaScheduler): Field('status',requires=IS_IN_SET(TASK_STATUS), default=QUEUED,writable=False), Field('function_name', - requires=IS_IN_SET(sorted(self.tasks.keys()))), + requires=IS_IN_SET(sorted(self.tasks.keys())) \ + if self.tasks else DEFAULT), Field('args','text',default='[]',requires=TYPE(list)), Field('vars','text',default='{}',requires=TYPE(dict)), Field('enabled','boolean',default=True), @@ -379,8 +386,9 @@ class Scheduler(MetaScheduler): Field('assigned_worker_name',default='',writable=False), migrate=migrate,format='%(task_name)s') if hasattr(current,'request'): - db.scheduler_task.application_name.default=current.request.application - + db.scheduler_task.application_name.default = \ + '%s/%s' % (current.request.application, + current.request.controller) db.define_table( 'scheduler_run', Field('scheduler_task','reference scheduler_task'), From 782c6fb6a15e78d31a374fb834af760dd56159db Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 8 Jul 2012 09:45:20 -0500 Subject: [PATCH 02/98] improver navbar behavior, thanks Anthony --- VERSION | 2 +- gluon/tools.py | 39 ++++++++++++++------------------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/VERSION b/VERSION index f1f44883..0b8cb6da 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-07 10:28:16) dev +Version 2.00.0 (2012-07-08 09:45:15) dev diff --git a/gluon/tools.py b/gluon/tools.py index a0bfa75b..bf05c810 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1212,10 +1212,7 @@ class Auth(object): def navbar(self, prefix='Welcome', action=None, separators=(' [ ',' | ',' ] '), referrer_actions=DEFAULT): - if referrer_actions == DEFAULT: - referrer_actions = ['login','logout','profile', - 'register','change_password', - 'retrieve_username','request_reset_password'] + referrer_actions = [] if not referrer_actions else referrer_actions request = current.request T = current.T if isinstance(prefix,str): @@ -1228,23 +1225,17 @@ class Auth(object): if URL() == action: next = '' else: - next = '?_next='+urllib.quote(URL(args=request.args,vars=request.get_vars)) - - pr_next = next if 'profile' in referrer_actions else '' - pa_next = next if 'change_password' in referrer_actions else '' - re_next = next if 'register' in referrer_actions else '' - ru_next = next if 'retrieve_username' in referrer_actions else '' - rp_next = next if 'request_reset_password' in referrer_actions else '' - li_next = '?_next='+urllib.quote(self.settings.login_next) \ - if 'login' in referrer_actions else '' - lo_next = '?_next='+urllib.quote(self.settings.logout_next) \ - if 'logout' in referrer_actions else '' + next = '?_next=' + urllib.quote(URL(args=request.args, vars=request.get_vars)) + + href = lambda function: '%s/%s%s' % (action, function, + next if referrer_actions is DEFAULT or function in referrer_actions else '') if self.user_id: - logout=A(T('Logout'),_href=action+'/logout'+lo_next) - profile=A(T('Profile'),_href=action+'/profile'+pr_next) - password=A(T('Password'),_href=action+'/change_password'+pa_next) - bar = SPAN(prefix,self.user.first_name,s1, logout,s3,_class='auth_navbar') + logout = A(T('Logout'), _href='%s/logout?_next=%s' % + (action, urllib.quote(self.settings.logout_next))) + profile = A(T('Profile'), _href=href('profile')) + password = A(T('Password'), _href=href('change_password')) + bar = SPAN(prefix,self.user.first_name,s1, logout, s3, _class='auth_navbar') if not 'profile' in self.settings.actions_disabled: bar.insert(4, s2) bar.insert(5, profile) @@ -1252,12 +1243,10 @@ class Auth(object): bar.insert(-1, s2) bar.insert(-1, password) else: - login=A(T('Login'),_href=action+'/login'+li_next) - register=A(T('Register'),_href=action+'/register'+re_next) - retrieve_username=A(T('forgot username?'), - _href=action+'/retrieve_username'+ru_next) - lost_password=A(T('Lost password?'), - _href=action+'/request_reset_password'+ru_next) + login = A(T('Login'), _href=href('login')) + register = A(T('Register'), _href=href('register')) + retrieve_username = A(T('Forgot username?'), _href=href('retrieve_username')) + lost_password = A(T('Lost password?'), _href=href('request_reset_password')) bar = SPAN(s1, login, s3, _class='auth_navbar') if not 'register' in self.settings.actions_disabled: From f71bb239d8d865532c3ccc754d7127e56121ec52 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 8 Jul 2012 16:40:01 -0500 Subject: [PATCH 03/98] renamed welcome crontab crontab.example, thanks Jonathan --- VERSION | 2 +- applications/welcome/cron/{crontab => crontab.example} | 0 gluon/newcron.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename applications/welcome/cron/{crontab => crontab.example} (100%) diff --git a/VERSION b/VERSION index 0b8cb6da..0fc1e51b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-08 09:45:15) dev +Version 2.00.0 (2012-07-08 16:39:56) dev diff --git a/applications/welcome/cron/crontab b/applications/welcome/cron/crontab.example similarity index 100% rename from applications/welcome/cron/crontab rename to applications/welcome/cron/crontab.example diff --git a/gluon/newcron.py b/gluon/newcron.py index cb2f3c8f..1c082347 100644 --- a/gluon/newcron.py +++ b/gluon/newcron.py @@ -47,7 +47,7 @@ class extcron(threading.Thread): threading.Thread.__init__(self) self.setDaemon(False) self.path = applications_parent - crondance(self.path, 'external', startup=True) + # crondance(self.path, 'external', startup=True) def run(self): if not _cron_stopping: From 5b41df08596b3d88672fb11300939543f7895d4a Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 8 Jul 2012 17:11:24 -0500 Subject: [PATCH 04/98] scripts/setup-web2py-fedora-ami.sh, thanks Charles Law --- VERSION | 2 +- scripts/setup-web2py-fedora-ami.sh | 401 +++++++++++++++++++++++++++++ 2 files changed, 402 insertions(+), 1 deletion(-) create mode 100755 scripts/setup-web2py-fedora-ami.sh diff --git a/VERSION b/VERSION index 0fc1e51b..d4cd9af7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-08 16:39:56) dev +Version 2.00.0 (2012-07-08 17:11:19) dev diff --git a/scripts/setup-web2py-fedora-ami.sh b/scripts/setup-web2py-fedora-ami.sh new file mode 100755 index 00000000..a42d5edb --- /dev/null +++ b/scripts/setup-web2py-fedora-ami.sh @@ -0,0 +1,401 @@ +echo "This script will: +1) Install modules needed to run web2py on Fedora and CentOS/RHEL +2) Install Python 2.6 to /opt and recompile wsgi if not provided +2) Install web2py in /opt/web-apps/ +3) Configure SELinux and iptables +5) Create a self signed ssl certificate +6) Setup web2py with mod_wsgi +7) Create virtualhost entries so that web2py responds for '/' +8) Restart Apache. + +You should probably read this script before running it. + +Although SELinux permissions changes have been made, +further SELinux changes will be required for your personal +apps. (There may also be additional changes required for the +bundled apps.) As a last resort, SELinux can be disabled. + +A simple iptables configuration has been applied. You may +want to review it to verify that it meets your needs. + +Finally, if you require a proxy to access the Internet, please +set up your machine to do so before running this script. + +(author: Charles Law as berubejd) + +Press ENTER to continue...[ctrl+C to abort]" + +read CONFIRM + +#!/bin/bash + +### +### Phase 0 - This may get messy. Lets work from a temporary directory +### + +current_dir=`pwd` + +if [ -d /tmp/setup-web2py/ ]; then + mv /tmp/setup-web2py/ /tmp/setup-web2py.old/ +fi + +mkdir -p /tmp/setup-web2py +cd /tmp/setup-web2py + +### +### Phase 1 - Requirements installation +### + +echo +echo " - Installing packages" +echo + +# Verify packages are up to date +yum update + +# Install required packages +yum install httpd mod_ssl mod_wsgi wget python + +# Verify we have at least Python 2.5 +typeset -i version_major +typeset -i version_minor + +version=`rpm --qf %{Version} -q python` +version_major=`echo ${version} | awk '{split($0, parts, "."); print parts[1]}'` +version_minor=`echo ${version} | awk '{split($0, parts, "."); print parts[2]}'` + +if [ ! ${version_major} -ge 2 -o ! ${version_minor} -ge 5 ]; then + # Setup 2.6 in /opt - based upon + # http://markkoberlein.com/getting-python-26-with-django-11-together-on + + # Check for earlier Python 2.6 install + if [ -e /opt/python2.6 ]; then + # Is Python already installed? + RETV=`/opt/python2.6/bin/python -V > /dev/null 2>&1; echo $?` + if [ ${RETV} -eq 0 ]; then + python_installed='True' + else + mv /opt/python2.6 /opt/python2.6-old + fi + fi + + # Install Python 2.6 if it doesn't exist already + if [ ! "${python_installed}" == "True" ]; then + # Install requirements for the Python build + yum install sqlite-devel zlib-devel + + mkdir -p /opt/python2.6 + + # Download and install + wget http://www.python.org/ftp/python/2.6.4/Python-2.6.4.tgz + tar -xzf Python-2.6.4.tgz + cd Python-2.6.4 + ./configure --prefix=/opt/python2.6 --with-threads --enable-shared --with-zlib=/usr/include + make && make install + + cd /tmp/setup-web2py + fi + + # Create links for Python 2.6 + # even if it was previously installed just to be sure + ln -s /opt/python2.6/lib/libpython2.6.so /usr/lib + ln -s /opt/python2.6/lib/libpython2.6.so.1.0 /usr/lib + ln -s /opt/python2.6/bin/python /usr/local/bin/python + ln -s /opt/python2.6/bin/python /usr/bin/python2.6 + ln -s /opt/python2.6/lib/python2.6.so /opt/python2.6/lib/python2.6/config/ + + # Update linker for new libraries + /sbin/ldconfig + + # Rebuild wsgi to take advantage of Python 2.6 + yum install httpd-devel + + cd /tmp/setup-web2py + + wget http://modwsgi.googlecode.com/files/mod_wsgi-3.3.tar.gz + tar -xzf mod_wsgi-3.3.tar.gz + cd mod_wsgi-3.3 + ./configure --with-python=/usr/local/bin/python + make && make install + + echo "LoadModule wsgi_module modules/mod_wsgi.so" > /etc/httpd/conf.d/wsgi.conf + + cd /tmp/setup-web2py +fi + +### MySQL install untested! +# Install mysql packages (optional) +#yum install mysql mysql-server + +# Enable mysql to start at boot (optional) +#chkconfig --levels 235 mysqld on +#service mysqld start + +# Configure mysql security settings (not really optional if mysql installed) +#/usr/bin/mysql_secure_installation + +### +### Phase 2 - Install web2py +### + +echo +echo " - Downloading, installing, and starting web2py" +echo + +# Create web-apps directory, if required +if [ ! -d "/opt/web-apps" ]; then + mkdir -p /opt/web-apps + + chmod 755 /opt + chmod 755 /opt/web-apps +fi + +cd /opt/web-apps + +# Download web2py +if [ -e web2py_src.zip* ]; then + rm web2py_src.zip* +fi + +wget http://web2py.com/examples/static/web2py_src.zip +unzip web2py_src.zip +chown -R apache:apache web2py + +### +### Phase 3 - Setup SELinux context +### + +# Set context for Python libraries if Python 2.6 installed +if [ -d /opt/python2.6 ]; then + cd /opt/python2.6 + chcon -R -t lib_t lib/ +fi + +# Allow http_tmp_exec required for wsgi +RETV=`setsebool -P httpd_tmp_exec on > /dev/null 2>&1; echo $?` +if [ ! ${RETV} -eq 0 ]; then + # CentOS doesn't support httpd_tmp_exec + cd /tmp/setup-web2py + + # Create the SELinux policy +cat > httpd.te < iptables.rules < /etc/httpd/ssl/self_signed.key +openssl req -new -x509 -nodes -sha1 -days 365 -key /etc/httpd/ssl/self_signed.key > /etc/httpd/ssl/self_signed.cert +openssl x509 -noout -fingerprint -text < /etc/httpd/ssl/self_signed.cert > /etc/httpd/ssl/self_signed.info + +chmod 400 /etc/httpd/ssl/self_signed.* + +### +### Phase 6 - Configure Apache +### + +echo +echo " - Configure Apache to use mod_wsgi" +echo + +# Create config +if [ -e /etc/httpd/conf.d/welcome.conf ]; then + mv /etc/httpd/conf.d/welcome.conf /etc/httpd/conf.d/welcome.conf.disabled +fi + +cat > /etc/httpd/conf.d/default.conf < + WSGIDaemonProcess web2py user=apache group=apache + WSGIProcessGroup web2py + WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py + + + AllowOverride None + Order Allow,Deny + Deny from all + + Allow from all + + + + AliasMatch ^/([^/]+)/static/(.*) /opt/web-apps/web2py/applications/\$1/static/\$2 + + + Options -Indexes + Order Allow,Deny + Allow from all + + + + Deny from all + + + + Deny from all + + + CustomLog /var/log/httpd/access_log common + ErrorLog /var/log/httpd/error_log + + + + SSLEngine on + SSLCertificateFile /etc/httpd/ssl/self_signed.cert + SSLCertificateKeyFile /etc/httpd/ssl/self_signed.key + + WSGIProcessGroup web2py + + WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py + + + AllowOverride None + Order Allow,Deny + Deny from all + + Allow from all + + + + AliasMatch ^/([^/]+)/static/(.*) /opt/web-apps/web2py/applications/\$1/static/\$2 + + + Options -Indexes + ExpiresActive On + ExpiresDefault "access plus 1 hour" + Order Allow,Deny + Allow from all + + + CustomLog /var/log/httpd/access_log common + ErrorLog /var/log/httpd/error_log + + +EOF + +# Fix wsgi socket locations +echo "WSGISocketPrefix run/wsgi" >> /etc/httpd/conf.d/wsgi.conf + +# Restart Apache to pick up changes +service httpd restart + +### +### Phase 7 - Setup web2py admin password +### + +echo +echo " - Setup web2py admin password" +echo + +cd /opt/web-apps/web2py +sudo -u apache python -c "from gluon.main import save_password; save_password(raw_input('admin password: '),443)" + +### +### Phase 8 - Verify that required services start at boot +### + +/sbin/chkconfig iptables on +/sbin/chkconfig httpd on + +### +### Phase 999 - Done! +### + +# Change back to original directory +cd ${current_directory} + +echo " - Complete!" +echo + From 34a7afe2bbd33a1c8124f26613307cf23bb328fb Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 8 Jul 2012 17:26:44 -0500 Subject: [PATCH 05/98] multiple exports from grid, issue 423, thanks Hong-Khoan Quach --- VERSION | 2 +- gluon/sqlhtml.py | 113 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index d4cd9af7..6c759662 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-08 17:11:19) dev +Version 2.00.0 (2012-07-08 17:26:39) dev diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index f5ba2afb..1a40ddb0 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -29,6 +29,7 @@ import datetime import urllib import re import cStringIO +from gluon.html import INPUT table_field = re.compile('[\w_]+\.[\w_]+') widget_class = re.compile('^\w*') @@ -1486,6 +1487,7 @@ class SQLFORM(FORM): search_widget='default', ignore_rw = False, formstyle = 'table3cols', + exportclasses = None, formargs={}, createargs={}, editargs={}, @@ -1703,6 +1705,62 @@ class SQLFORM(FORM): raise HTTP(200,str(dbset.select()), **{'Content-Type':'text/csv', 'Content-Disposition':'attachment;filename=rows.csv;'}) + + #============================================================================== + + exportManager = dict(csv_with_hidden_cols=(ExporterCsv,'csv with hidden cols'), + csv=ExporterCsv, + html=ExporterHtml) + if not exportclasses is None: + exportManager.update(exportclasses) + + if len(request.args)>0 and request.args[-1]=='export': + export_type = request.vars.export_type + order = request.vars.order or '' + if sortable: + if order and not order=='None': + if order[:1]=='~': + sign, rorder = '~', order[1:] + else: + sign, rorder = '', order + tablename,fieldname = rorder.split('.',1) + if sign=='~': + orderby=~db[tablename][fieldname] + else: + orderby=db[tablename][fieldname] + + table_fields = [f for f in fields if f._tablename in tablenames] + if export_type =='csv_with_hidden_cols': + if request.vars.keywords: + try: + dbset=dbset(SQLFORM.build_query( + fields, + request.vars.get('keywords',''))) + except: + raise HTTP(400) + check_authorization() + rows = dbset.select() + else: + rows = dbset.select(left=left,orderby=orderby,*columns) + + if not export_type is None: + if exportManager.has_key(export_type): + value = exportManager[export_type] + if hasattr(value, '__getitem__'): + clazz = value[0] + else: + clazz = value + oExp = clazz(rows) + filename = '.'.join(('rows', oExp.file_ext)) + response.headers['Content-Type'] = oExp.content_type + response.headers['Content-Disposition'] = \ + 'attachment;filename='+filename+';' + + raise HTTP(200, oExp.export(), + **{'Content-Type':oExp.content_type, + 'Content-Disposition':'attachment;filename='+filename+';'}) + #================================================================================ + elif request.vars.records and not isinstance( request.vars.records,list): request.vars.records=[request.vars.records] @@ -1767,6 +1825,23 @@ class SQLFORM(FORM): trap = False, buttonurl=url(args=['csv'], vars=dict(keywords=request.vars.keywords or '')))) + + #================================================================ + options =[] + for k,v in exportManager.items(): + if hasattr(v, "__getitem__"): + label = v[1] + else: + label = k + options.append(OPTION(T(label),_value=k)) + f = FORM(SELECT(options, _name="export_type"), + INPUT(_type="submit", _value="export"), + INPUT(_type="hidden", _name="order", _value=request.vars.order), + INPUT(_type="hidden", _name="keywords", _value=request.vars.keywords or ''), + _method="GET", + _action=url(args=['export'])) + search_actions.append(f) + #================================================================ console.append(search_actions) @@ -2374,7 +2449,43 @@ class SQLTABLE(TABLE): form_factory = SQLFORM.factory # for backward compatibility, deprecated - +class ExportClass(object): + file_ext = None + content_type = None + + def __init__(self, rows): + self.rows = rows + + def export(self): + raise NotImplementedError + +class ExporterCsv(ExportClass): + file_ext = "csv" + content_type = "text/csv" + + def __init__(self, rows): + ExportClass.__init__(self, rows) + + def export(self): + return str(self.rows) + +class ExporterHtml(ExportClass): + file_ext = "html" + content_type = "text/html" + + def __init__(self, rows): + ExportClass.__init__(self, rows) + + def export(self): + out = cStringIO.StringIO() + out.write('\n\n\n') + for cols in self.rows: + out.write('\n') + for colname,value in cols.items(): + out.write('\n') + out.write('\n') + out.write('
    '+str(value)+'
    \n\n') + return str(out.getvalue()) From 6ffd15adf36ccc092264141e5d609653129dbe29 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 8 Jul 2012 17:39:37 -0500 Subject: [PATCH 06/98] fixed issue 701, thanks Marius van Niekerk --- VERSION | 2 +- gluon/dal.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 6c759662..53517239 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-08 17:26:39) dev +Version 2.00.0 (2012-07-08 17:39:33) dev diff --git a/gluon/dal.py b/gluon/dal.py index 996ac01b..5d5eb278 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -1281,7 +1281,7 @@ class BaseAdapter(ConnectionPool): for key in set(attributes.keys())-set(('orderby', 'groupby', 'limitby', 'required', 'cache', 'left', 'distinct', 'having', 'join', - 'for_update')): + 'for_update', 'processor')): raise SyntaxError, 'invalid select attribute: %s' % key tablenames = self.tables(query) @@ -4434,7 +4434,7 @@ class CouchDBAdapter(NoSQLAdapter): raise SyntaxError, "Not Supported" for key in set(attributes.keys())-set(('orderby','groupby','limitby', 'required','cache','left', - 'distinct','having')): + 'distinct', 'having', 'processor')): raise SyntaxError, 'invalid select attribute: %s' % key new_fields=[] for item in fields: From 91f49710564f60cd567eece7f433610f2820944e Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 8 Jul 2012 20:28:09 -0500 Subject: [PATCH 07/98] -K allows to specify which apps to cron, thanks Jonathan --- VERSION | 2 +- gluon/newcron.py | 16 +++++++++------- gluon/widget.py | 36 +++++++++++++++++++++--------------- logging.example.conf | 8 +++++++- 4 files changed, 38 insertions(+), 24 deletions(-) diff --git a/VERSION b/VERSION index 53517239..16682a72 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-08 17:39:33) dev +Version 2.00.0 (2012-07-08 20:28:04) dev diff --git a/gluon/newcron.py b/gluon/newcron.py index 1c082347..1b6fb689 100644 --- a/gluon/newcron.py +++ b/gluon/newcron.py @@ -43,16 +43,17 @@ def stopcron(): class extcron(threading.Thread): - def __init__(self, applications_parent): + def __init__(self, applications_parent, apps=None): threading.Thread.__init__(self) self.setDaemon(False) self.path = applications_parent - # crondance(self.path, 'external', startup=True) + self.apps = apps + # crondance(self.path, 'external', startup=True, apps=self.apps) def run(self): if not _cron_stopping: logger.debug('external cron invocation') - crondance(self.path, 'external', startup=False) + crondance(self.path, 'external', startup=False, apps=self.apps) class hardcron(threading.Thread): @@ -80,7 +81,7 @@ class softcron(threading.Thread): def __init__(self, applications_parent): threading.Thread.__init__(self) self.path = applications_parent - crondance(self.path, 'soft', startup=True) + # crondance(self.path, 'soft', startup=True) def run(self): if not _cron_stopping: @@ -242,7 +243,7 @@ class cronlauncher(threading.Thread): logger.debug('WEB2PY CRON Call returned success:\n%s' \ % stdoutdata) -def crondance(applications_parent, ctype='soft', startup=False): +def crondance(applications_parent, ctype='soft', startup=False, apps=None): apppath = os.path.join(applications_parent,'applications') cron_path = os.path.join(applications_parent) token = Token(cron_path) @@ -256,8 +257,9 @@ def crondance(applications_parent, ctype='soft', startup=False): ('dom',now_s.tm_mday), ('dow',(now_s.tm_wday+1)%7)) - apps = [x for x in os.listdir(apppath) - if os.path.isdir(os.path.join(apppath, x))] + if apps is None: + apps = [x for x in os.listdir(apppath) + if os.path.isdir(os.path.join(apppath, x))] full_apath_links = set() diff --git a/gluon/widget.py b/gluon/widget.py index dfa2cb99..6a7c823d 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -911,14 +911,6 @@ def start(cron=True): test(options.test, verbose=options.verbose) return - # ## if -K - if options.scheduler: - try: - start_schedulers(options) - except KeyboardInterrupt: - pass - return - # ## if -S start interactive shell (also no cron) if options.shell: if not options.args is None: @@ -928,21 +920,35 @@ def start(cron=True): return # ## if -C start cron run (extcron) and exit - # ## if -N or not cron disable cron in this *process* - # ## if --softcron use softcron - # ## use hardcron in all other cases + # ## -K specifies optional apps list (overloading scheduler) if options.extcron: - print 'Starting extcron...' + logger.debug('Starting extcron...') global_settings.web2py_crontype = 'external' - extcron = newcron.extcron(options.folder) + if options.scheduler: # -K + apps = [app.strip() for app in options.scheduler.split(',')] + else: + apps = None + extcron = newcron.extcron(options.folder, apps=apps) extcron.start() extcron.join() return - elif cron and not options.nocron and options.softcron: + + # ## if -K + if options.scheduler: + try: + start_schedulers(options) + except KeyboardInterrupt: + pass + return + + # ## if -N or not cron disable cron in this *process* + # ## if --softcron use softcron + # ## use hardcron in all other cases + if cron and not options.nocron and options.softcron: print 'Using softcron (but this is not very efficient)' global_settings.web2py_crontype = 'soft' elif cron and not options.nocron: - print 'Starting hardcron...' + logger.debug('Starting hardcron...') global_settings.web2py_crontype = 'hard' newcron.hardcron(options.folder).start() diff --git a/logging.example.conf b/logging.example.conf index 79b7272d..ac365aa1 100644 --- a/logging.example.conf +++ b/logging.example.conf @@ -29,7 +29,7 @@ # set to DEBUG. [loggers] -keys=root,rocket,markdown,web2py,rewrite,app,welcome +keys=root,rocket,markdown,web2py,rewrite,cron,app,welcome [handlers] keys=consoleHandler,messageBoxHandler @@ -55,6 +55,12 @@ qualname=web2py.rewrite handlers=consoleHandler propagate=0 +[logger_cron] +level=WARNING +qualname=web2py.cron +handlers=consoleHandler +propagate=0 + # generic app handler [logger_app] level=WARNING From d1cd0ac1e2a38e0d765793955fbaa0b5631150b4 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 9 Jul 2012 17:41:31 -0500 Subject: [PATCH 08/98] auto-add uploadfield --- VERSION | 2 +- gluon/dal.py | 27 +++++++++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/VERSION b/VERSION index 16682a72..5f0494d7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-08 20:28:04) dev +Version 2.00.0 (2012-07-09 17:41:27) dev diff --git a/gluon/dal.py b/gluon/dal.py index 5d5eb278..b0e5ceec 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -6982,8 +6982,8 @@ def index(): raise SyntaxError, 'invalid file format' else: tablename = line[6:] - self[tablename].import_from_csv_file(ifile, id_map, null, - unique, id_offset, *args, **kwargs) + self[tablename].import_from_csv_file( + ifile, id_map, null, unique, id_offset, *args, **kwargs) class SQLALL(object): """ @@ -7143,7 +7143,9 @@ class Table(dict): if isinstance(field, Field) and field.type == 'upload'\ and field.uploadfield is True: tmp = field.uploadfield = '%s_blob' % field.name - fields.append(self._db.Field(tmp, 'blob', default='')) + if isinstance(field.uploadfield,str) and \ + not [f for f in fields if f.name==field.uploadfield]: + fields.append(self._db.Field(field.uplaodfield,'blob',default='') lower_fieldnames = set() reserved = dir(Table) + ['fields'] @@ -7479,11 +7481,14 @@ class Table(dict): the 'table.' prefix is ignored. 'unique' argument is a field which must be unique (typically a uuid field) - 'restore' argument is default False. If set True will remove old values + 'restore' argument is default False. + If set True will remove old values in table first. - 'id_map' If set to None will not map id. The import will keep the id numbers - in the restored table. This assumes that there is an field of type id that - is integer and in incrementing order. Will keep the id numbers in restored table. + 'id_map' If set to None will not map id. + The import will keep the id numbers in the restored table. + This assumes that there is an field of type id that + is integer and in incrementing order. + Will keep the id numbers in restored table. """ delimiter = kwargs.get('delimiter', ',') @@ -7493,7 +7498,8 @@ class Table(dict): if restore: self._db[self].truncate() - reader = csv.reader(csvfile, delimiter=delimiter, quotechar=quotechar, quoting=quoting) + reader = csv.reader(csvfile, delimiter=delimiter, + quotechar=quotechar, quoting=quoting) colnames = None if isinstance(id_map, dict): if not self._tablename in id_map: @@ -7565,8 +7571,9 @@ class Table(dict): del_id = curr_id if first: first = False - #First curr_id is bigger than csv_id, then we are not restoring but - #extending db table with csv db table + # First curr_id is bigger than csv_id, + # then we are not restoring but + # extending db table with csv db table if curr_id>csv_id: id_offset[self._tablename] = curr_id-csv_id else: From 0c9723d1e09e2b8cf77444473bfc0f921d8565da Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 9 Jul 2012 21:08:57 -0500 Subject: [PATCH 09/98] better scheduler, thanks Niphlod --- VERSION | 2 +- gluon/scheduler.py | 120 +++++++++++++++++++++++++++++---------------- 2 files changed, 80 insertions(+), 42 deletions(-) diff --git a/VERSION b/VERSION index 5f0494d7..aa2f34dd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-09 17:41:27) dev +Version 2.00.0 (2012-07-09 21:08:54) dev diff --git a/gluon/scheduler.py b/gluon/scheduler.py index 25b162b7..80c8cda9 100644 --- a/gluon/scheduler.py +++ b/gluon/scheduler.py @@ -86,7 +86,7 @@ except: from simplejson import loads, dumps -from gluon import DAL, Field, IS_NOT_EMPTY, IS_IN_SET +from gluon import DAL, Field, IS_NOT_EMPTY, IS_IN_SET, IS_NOT_IN_DB from gluon.utils import web2py_uuid QUEUED = 'QUEUED' @@ -97,8 +97,9 @@ FAILED = 'FAILED' TIMEOUT = 'TIMEOUT' STOPPED = 'STOPPED' ACTIVE = 'ACTIVE' -INACTIVE = 'INACTIVE' +TERMINATE = 'TERMINATE' DISABLED = 'DISABLED' +KILL = 'KILL' EXPIRED = 'EXPIRED' SECONDS = 1 HEARTBEAT = 3*SECONDS @@ -183,7 +184,7 @@ def executor(queue,task): f = task.function # First look for the func in tasks, else look in models _function = current._scheduler.tasks.get(f) or _env.get(f) - assert _function, 'Function %s not found in scheduler\'s environmen + assert _function, "Function %s not found in scheduler's environment" globals().update(_env) args = loads(task.args) vars = loads(task.vars, object_hook=_decode_dict) @@ -226,7 +227,7 @@ class MetaScheduler(threading.Thread): p.terminate() p.join() self.have_heartbeat = False - logging.debug(' task stopped') + logging.debug(' task stopped by general exception') return TaskReport(STOPPED) if p.is_alive(): p.terminate() @@ -246,6 +247,10 @@ class MetaScheduler(threading.Thread): self.have_heartbeat = False self.terminate_process() + def give_up(self): + logging.info('Giving up as soon as possible!') + self.have_heartbeat = False + def terminate_process(self): try: self.process.terminate() @@ -293,7 +298,7 @@ class MetaScheduler(threading.Thread): else: self.empty_runs += 1 logging.debug('sleeping...') - if self.max_empty_runs <> 0: + if self.max_empty_runs != 0: logging.debug('empty runs %s/%s', self.empty_runs, self.max_empty_runs) if self.empty_runs >= self.max_empty_runs: logging.info('empty runs limit reached, killing myself') @@ -305,7 +310,7 @@ class MetaScheduler(threading.Thread): TASK_STATUS = (QUEUED, RUNNING, COMPLETED, FAILED, TIMEOUT, STOPPED, EXPIRED) RUN_STATUS = (RUNNING, COMPLETED, FAILED, TIMEOUT, STOPPED) -WORKER_STATUS = (ACTIVE,INACTIVE,DISABLED) +WORKER_STATUS = (ACTIVE, DISABLED, TERMINATE, KILL) class TYPE(object): """ @@ -333,7 +338,8 @@ class TYPE(object): class Scheduler(MetaScheduler): def __init__(self,db,tasks={},migrate=True, - worker_name=None,group_names=None,heartbeat=HEARTBEAT,max_empty_runs=0): + worker_name=None,group_names=None,heartbeat=HEARTBEAT, + max_empty_runs=0, discard_results=False): MetaScheduler.__init__(self) @@ -348,6 +354,7 @@ class Scheduler(MetaScheduler): #hibernation (i.e. when someone stop the #worker acting on the scheduler_worker table) self.max_empty_runs = max_empty_runs + self.discard_results = discard_results self.is_a_ticker = False self.do_assign_tasks = False @@ -370,25 +377,30 @@ class Scheduler(MetaScheduler): Field('status',requires=IS_IN_SET(TASK_STATUS), default=QUEUED,writable=False), Field('function_name', - requires=IS_IN_SET(sorted(self.tasks.keys())) \ - if self.tasks else DEFAULT), + requires=IS_IN_SET(sorted(self.tasks.keys())) + if self.tasks else DEFAULT), + Field('uuid', requires=IS_NOT_IN_DB(db, 'scheduler_task.uuid'), + unique=True, default=web2py_uuid), Field('args','text',default='[]',requires=TYPE(list)), Field('vars','text',default='{}',requires=TYPE(dict)), Field('enabled','boolean',default=True), Field('start_time','datetime',default=now), Field('next_run_time','datetime',default=now), Field('stop_time','datetime'), - Field('repeats','integer',default=1,comment="0=unlimted"), + Field('repeats','integer',default=1,comment="0=unlimited"), + Field('repeats_failed', 'integer', default=1, comment="0=unlimited"), Field('period','integer',default=60,comment='seconds'), Field('timeout','integer',default=60,comment='seconds'), Field('times_run','integer',default=0,writable=False), + Field('times_failed','integer',default=0,writable=False), Field('last_run_time','datetime',writable=False,readable=False), Field('assigned_worker_name',default='',writable=False), migrate=migrate,format='%(task_name)s') if hasattr(current,'request'): - db.scheduler_task.application_name.default = \ - '%s/%s' % (current.request.application, - current.request.controller) + db.scheduler_task.application_name.default= '%s/%s' % ( + current.request.application, current.request.controller + ) + db.define_table( 'scheduler_run', Field('scheduler_task','reference scheduler_task'), @@ -403,7 +415,7 @@ class Scheduler(MetaScheduler): db.define_table( 'scheduler_worker', - Field('worker_name'), + Field('worker_name', unique=True), Field('first_heartbeat','datetime'), Field('last_heartbeat','datetime'), Field('status',requires=IS_IN_SET(WORKER_STATUS)), @@ -428,13 +440,14 @@ class Scheduler(MetaScheduler): else: self.empty_runs += 1 logging.debug('sleeping...') - if self.max_empty_runs <> 0: + if self.max_empty_runs != 0: logging.debug('empty runs %s/%s', self.empty_runs, self.max_empty_runs) if self.empty_runs >= self.max_empty_runs: logging.info('empty runs limit reached, killing myself') self.die() self.sleep() - except KeyboardInterrupt: + except (KeyboardInterrupt, SystemExit): + logging.info('catched') self.die() def pop_task(self): @@ -471,8 +484,9 @@ class Scheduler(MetaScheduler): run_again = True else: run_again = False - logging.debug(' new scheduler_run record') - while True: + run_id = 0 + while True and not self.discard_results: + logging.debug(' new scheduler_run record') try: run_id = db.scheduler_run.insert( scheduler_task = task.id, @@ -495,35 +509,49 @@ class Scheduler(MetaScheduler): run_again = run_again, next_run_time=next_run_time, times_run = times_run, - stop_time = task.stop_time) + stop_time = task.stop_time, + repeats_failed = task.repeats_failed, + times_failed = task.times_failed) def report_task(self,task,task_report): - logging.debug(' recording task report in db (%s)' % task_report.status) db = self.db - db(db.scheduler_run.id==task.run_id).update( - status = task_report.status, - stop_time = datetime.datetime.now(), - result = task_report.result, - output = task_report.output, - traceback = task_report.tb) + if not self.discard_results: + if task_report.result != 'null' or task_report.tb: + #result is 'null' as a string if task completed + #if it's stopped it's None as NoneType, so we record + #the STOPPED "run" anyway + logging.debug(' recording task report in db (%s)' % task_report.status) + db(db.scheduler_run.id==task.run_id).update( + status = task_report.status, + stop_time = datetime.datetime.now(), + result = task_report.result, + output = task_report.output, + traceback = task_report.tb) + else: + logging.debug(' deleting task report in db because of no result') + db(db.scheduler_run.id==task.run_id).delete() is_expired = task.stop_time and task.next_run_time > task.stop_time and True or False status = (task.run_again and is_expired and EXPIRED or task.run_again and not is_expired and QUEUED or COMPLETED) if task_report.status == COMPLETED: d = dict(status = status, next_run_time = task.next_run_time, - times_run = task.times_run) - #I'd like to know who worked my task, reviewing some logs... - #,assigned_worker_name = '') + times_run = task.times_run, + times_failed = 0 #reset times_failed counter for the next run + ) + db(db.scheduler_task.id==task.task_id)\ + (db.scheduler_task.status==RUNNING).update(**d) else: - d = dict( - #same as before... - #assigned_worker_name = '', - status = {'FAILED':'FAILED', + st_mapping = {'FAILED':'FAILED', 'TIMEOUT':'TIMEOUT', - 'STOPPED':'QUEUED'}[task_report.status]) - db(db.scheduler_task.id==task.task_id)\ - (db.scheduler_task.status==RUNNING).update(**d) + 'STOPPED':'QUEUED'}[task_report.status] + status = (task.repeats_failed and task.times_failed + 1 < task.repeats_failed + and QUEUED or task.repeats_failed==0 and QUEUED or st_mapping) + db(db.scheduler_task.id==task.task_id)\ + (db.scheduler_task.status==RUNNING).update( + times_failed=db.scheduler_task.times_failed+1, + next_run_time = task.next_run_time, + status=status) db.commit() logging.info('task completed (%s)' % task_report.status) @@ -557,6 +585,16 @@ class Scheduler(MetaScheduler): logging.debug('........recording heartbeat') db(sw.worker_name==self.worker_name).update( last_heartbeat = now) + + elif mybackedstatus.status == TERMINATE: + self.worker_status = TERMINATE, self.worker_status[1] + logging.debug("Waiting to terminate the current task") + self.give_up() + return + elif mybackedstatus.status == KILL: + self.worker_status = KILL, self.worker_status[1] + self.die() + else: logging.debug('........recording heartbeat') db(sw.worker_name==self.worker_name).update( @@ -570,7 +608,7 @@ class Scheduler(MetaScheduler): logging.debug(' freeing workers that have not sent heartbeat') inactive_workers = db( ((sw.last_heartbeat DISABLED: + if self.worker_status[0] == ACTIVE: self.do_assign_tasks = True except: pass @@ -591,10 +629,10 @@ class Scheduler(MetaScheduler): def being_a_ticker(self): db = self.db_thread sw = db.scheduler_worker - ticker = db((sw.worker_name <> self.worker_name) & (sw.is_ticker == True) & (sw.status == ACTIVE)).select().first() + ticker = db((sw.worker_name != self.worker_name) & (sw.is_ticker == True) & (sw.status == ACTIVE)).select().first() if not ticker: db(sw.worker_name == self.worker_name).update(is_ticker = True) - db(sw.worker_name <> self.worker_name).update(is_ticker = False) + db(sw.worker_name != self.worker_name).update(is_ticker = False) logging.info("TICKER: I'm a ticker (%s)" % self.worker_name) return True else: @@ -605,7 +643,7 @@ class Scheduler(MetaScheduler): db = self.db sw, ts = db.scheduler_worker, db.scheduler_task now = datetime.datetime.now() - all_workers = db(sw.status <> DISABLED).select() + all_workers = db(sw.status == ACTIVE).select() #build workers as dict of groups wkgroups = {} for w in all_workers: From 2cca2c7ccaa7a4d29cee63d4881862aa90d3791c Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 9 Jul 2012 21:09:55 -0500 Subject: [PATCH 10/98] fixed typo in dal --- VERSION | 2 +- gluon/dal.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index aa2f34dd..e2be58be 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-09 21:08:54) dev +Version 2.00.0 (2012-07-09 21:09:52) dev diff --git a/gluon/dal.py b/gluon/dal.py index b0e5ceec..616b64d1 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -7145,7 +7145,7 @@ class Table(dict): tmp = field.uploadfield = '%s_blob' % field.name if isinstance(field.uploadfield,str) and \ not [f for f in fields if f.name==field.uploadfield]: - fields.append(self._db.Field(field.uplaodfield,'blob',default='') + fields.append(self._db.Field(field.uplaodfield,'blob',default='')) lower_fieldnames = set() reserved = dir(Table) + ['fields'] From 5f549e329b2a0f1eba0988961b2d55305c016d9b Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 9 Jul 2012 21:39:07 -0500 Subject: [PATCH 11/98] fixed oracle bigint, thanks faridgs --- VERSION | 2 +- gluon/dal.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/VERSION b/VERSION index e2be58be..482e490f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-09 21:09:52) dev +Version 2.00.0 (2012-07-09 21:39:03) dev diff --git a/gluon/dal.py b/gluon/dal.py index 616b64d1..0936f939 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -2487,9 +2487,9 @@ class OracleAdapter(BaseAdapter): 'blob': 'CLOB', 'upload': 'VARCHAR2(%(length)s)', 'integer': 'INT', - 'bigint': 'BIGINT', + 'bigint': 'NUMBER', 'float': 'FLOAT', - 'double': 'DOUBLE', + 'double': 'BINARY_DOUBLE', 'decimal': 'NUMERIC(%(precision)s,%(scale)s)', 'date': 'DATE', 'time': 'CHAR(8)', @@ -2499,8 +2499,8 @@ class OracleAdapter(BaseAdapter): 'list:integer': 'CLOB', 'list:string': 'CLOB', 'list:reference': 'CLOB', - 'big-id': 'BIGINT PRIMARY KEY', - 'big-reference': 'BIGINT, CONSTRAINT %(constraint_name)s FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s', + 'big-id': 'NUMBER PRIMARY KEY', + 'big-reference': 'NUMBER, CONSTRAINT %(constraint_name)s FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s', 'reference FK': ', CONSTRAINT FK_%(constraint_name)s FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s', 'reference TFK': ' CONSTRAINT FK_%(foreign_table)s_PK FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_table)s (%(foreign_key)s) ON DELETE %(on_delete_action)s', } From 77bf572a778af095183b1fee56e26851be23d734 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 9 Jul 2012 21:40:33 -0500 Subject: [PATCH 12/98] fixed typo in tools, issue 880, thanks Douglas Philips --- VERSION | 2 +- gluon/tools.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 482e490f..0605e85c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-09 21:39:03) dev +Version 2.00.0 (2012-07-09 21:40:31) dev diff --git a/gluon/tools.py b/gluon/tools.py index bf05c810..f858d0f9 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1875,7 +1875,7 @@ class Auth(object): user = self.get_or_create_user(form.vars) break if not user: - self.log_event(self.settings.login_failed_log, + self.log_event(self.messages.login_failed_log, request.post_vars) # invalid login session.flash = self.messages.invalid_login From 4da12d32610a7cf1ec6ebe03c7d304ab3314c572 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 9 Jul 2012 23:55:16 -0500 Subject: [PATCH 13/98] fixed typo in dal, thanks Jonathan --- VERSION | 2 +- gluon/dal.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 0605e85c..7bd36900 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-09 21:40:31) dev +Version 2.00.0 (2012-07-09 23:55:13) dev diff --git a/gluon/dal.py b/gluon/dal.py index 0936f939..d233beba 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -7145,7 +7145,7 @@ class Table(dict): tmp = field.uploadfield = '%s_blob' % field.name if isinstance(field.uploadfield,str) and \ not [f for f in fields if f.name==field.uploadfield]: - fields.append(self._db.Field(field.uplaodfield,'blob',default='')) + fields.append(self._db.Field(field.uploadfield,'blob',default='')) lower_fieldnames = set() reserved = dir(Table) + ['fields'] From 6313a83f83bcf33e45879f536693b82e769df33a Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 10 Jul 2012 00:22:35 -0500 Subject: [PATCH 14/98] better user.html, thanks Dave --- VERSION | 2 +- applications/welcome/views/default/user.html | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/VERSION b/VERSION index 7bd36900..9fa761cc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-09 23:55:13) dev +Version 2.00.0 (2012-07-10 00:22:32) dev diff --git a/applications/welcome/views/default/user.html b/applications/welcome/views/default/user.html index 61b72036..c6c28aca 100644 --- a/applications/welcome/views/default/user.html +++ b/applications/welcome/views/default/user.html @@ -1,13 +1,17 @@ {{extend 'layout.html'}}

    {{=T( request.args(0).replace('_',' ').capitalize() )}}

    - {{if request.args(0)=='login':}} - {{if not 'register' in auth.settings.actions_disabled:}} - {{form.add_button(T('Register'),URL(args='register'))}} - {{pass}} - {{form.add_button(T('Lost Password'),URL(args='request_reset_password'))}} - {{pass}} - {{=form}} +{{ +if request.args(0)=='login': + if not 'register' in auth.settings.actions_disabled: + form.add_button(T('Register'),URL(args='register')) + pass + if not 'request_reset_password' in auth.settings.actions_disabled: + form.add_button(T('Lost Password'),URL(args='request_reset_password')) + pass +pass +=form +}}
    """ -__all__ = ['render', 'markmin2html'] +__all__ = ['render', 'markmin2html', 'markmin_escape'] __doc__ = """ # Markmin markup language @@ -44,7 +45,7 @@ print markmin2pdf(m) ## Why? We wanted a markup language with the following requirements: -- less than 100 lines of functional code +- less than 200 lines of functional code - easy to read - secure - support table, ul, ol, code @@ -75,33 +76,38 @@ markmin2html.py and markmin2latex.py are single files and have no web2py depende ### Bold, italic, code and links --------------------------------------------------- -**SOURCE** | **OUTPUT** -``# title`` | **title** -``## section`` | **section** -``### subsection`` | **subsection** -``**bold**`` | **bold** -``''italic''`` | ''italic'' -``!`!`verbatim`!`!`` | ``verbatim`` -``http://google.com`` | http://google.com -``[[click me #myanchor]]`` | [[click me #myanchor]] ---------------------------------------------------- +------------------------------------------------------------------------------ +**SOURCE** | **OUTPUT** +``# title`` | **title** +``## section`` | **section** +``### subsection`` | **subsection** +``**bold**`` | **bold** +``''italic''`` | ''italic'' +``~~strikeout~~`` | ~~strikeout~~ +``!`!`verbatim`!`!`` | ``verbatim`` +``\`\`color with **bold**\`\`:red`` | ``color with **bold**``:red +``\`\`many colors\`\`:color[blue:#ffff00]`` | ``many colors``:color[blue:#ffff00] +``http://google.com`` | http://google.com +``[[**click** me #myanchor]]`` | [[**click** me #myanchor]] +``[[click me [extra info] #myanchor popup]]`` | [[click me [extra info] #myanchor popup]] +------------------------------------------------------------------------------- ### More on links -The format is always ``[[title link]]``. Notice you can nest bold, italic and code inside the link title. +The format is always ``[[title link]]`` or ``[[title [extra] link]]``. Notice you can nest bold, italic, strikeout and code inside the link ``title``. ### Anchors [[myanchor]] You can place an anchor anywhere in the text using the syntax ``[[name]]`` where ''name'' is the name of the anchor. -You can then link the anchor with [[link #myanchor]], i.e. ``[[link #myanchor]]``. +You can then link the anchor with [[link #myanchor]], i.e. ``[[link #myanchor]]`` or [[link with an extra info [extra info] #myanchor]], i.e. +``[[link with an extra info [extra info] #myanchor]]``. ### Images -[[some image http://www.web2py.com/examples/static/web2py_logo.png right 200px]] +[[alt-string for the image [the image title] http://www.web2py.com/examples/static/web2py_logo.png right 200px]] This paragraph has an image aligned to the right with a width of 200px. Its is placed using the code -``[[some image http://www.web2py.com/examples/static/web2py_logo.png right 200px]]``. +``[[alt-string for the image [the image title] http://www.web2py.com/examples/static/web2py_logo.png right 200px]]``. ### Unordered Lists @@ -169,6 +175,10 @@ def test(): ``:python Optionally a ` inside a ``!`!`...`!`!`` block can be inserted escaped with !`!. + +**NOTE:** You can escape markmin constructions (\\'\\',\`\`,\*\*,\~\~,\[,\{,\]\},\$,\@) with '\\\\' character: + so \\\\`\\\\` can replace !`!`! escape string + The ``:python`` after the markup is also optional. If present, by default, it is used to set the class of the block. The behavior can be overridden by passing an argument ``extra`` to the ``render`` function. For example: @@ -183,23 +193,26 @@ generates (the ``!`!`...`!`!:custom`` block is rendered by the ``custom=lambda`` function passed to ``render``). - ### Html5 support Markmin also supports the