diff --git a/Makefile b/Makefile index ccb5f5c7..f38e1bc3 100644 --- a/Makefile +++ b/Makefile @@ -121,7 +121,6 @@ commit: push: hg push git push - git push mdipierro tag: git tag -l '$(S)' hg tag -l '$(S)' diff --git a/VERSION b/VERSION index c163047c..73372cfc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.4.1-alpha.2+timestamp.2012.12.28.01.21.20 +Version 2.4.1-alpha.2+timestamp.2013.01.09.16.56.34 diff --git a/anyserver.py b/anyserver.py index 473cceaa..ebf556c8 100644 --- a/anyserver.py +++ b/anyserver.py @@ -163,6 +163,22 @@ class Servers: "tcp://127.0.0.1:9996") mongrel2_handler(app, conn, debug=False) + @staticmethod + def motor(app, address, **options): + #https://github.com/rpedroso/motor + import motor + app = motor.WSGIContainer(app) + http_server = motor.HTTPServer(app) + http_server.listen(address=address[0], port=address[1]) + #http_server.start(2) + motor.IOLoop.instance().start() + + @staticmethod + def pulsar(app, address, **options): + from pulsar.apps import wsgi + sys.argv = ['anyserver.py'] + s = wsgi.WSGIServer(callable=app, bind="%s:%d" % address) + s.start() def run(servername, ip, port, softcron=True, logging=False, profiler=None): if logging: diff --git a/applications/admin/controllers/debug.py b/applications/admin/controllers/debug.py index f02b2866..97ef49f8 100644 --- a/applications/admin/controllers/debug.py +++ b/applications/admin/controllers/debug.py @@ -212,3 +212,26 @@ def toggle_breakpoint(): except Exception, e: session.flash = str(e) return response.json({'ok': ok, 'lineno': lineno}) + +def list_breakpoints(): + "Return a list of linenumbers for current breakpoints" + + breakpoints = [] + ok = None + try: + filename = os.path.join(request.env['applications_parent'], + 'applications', request.vars.filename) + # normalize path name: replace slashes, references, etc... + filename = os.path.normpath(os.path.normcase(filename)) + for bp in qdb_debugger.do_list_breakpoint(): + no, bp_filename, bp_lineno, temporary, enabled, hits, cond = bp + # normalize path name: replace slashes, references, etc... + bp_filename = os.path.normpath(os.path.normcase(bp_filename)) + if filename == bp_filename: + breakpoints.append(bp_lineno) + ok = True + except Exception, e: + session.flash = str(e) + ok = False + return response.json({'ok': ok, 'breakpoints': breakpoints}) + diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index 4004402a..ba67a9f5 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -1493,7 +1493,9 @@ def errors(): else: for item in request.vars: - if item[:7] == 'delete_': + # delete_all} rows doesn't contain any ticket + # Remove anything else as requested + if item[:7] == 'delete_' and (not item == "delete_all}"): os.unlink(apath('%s/errors/%s' % (app, item[7:]), r=request)) func = lambda p: os.stat(apath('%s/errors/%s' % (app, p), r=request)).st_mtime diff --git a/applications/admin/static/js/ajax_editor.js b/applications/admin/static/js/ajax_editor.js index b24b2fd0..44d5de7d 100644 --- a/applications/admin/static/js/ajax_editor.js +++ b/applications/admin/static/js/ajax_editor.js @@ -148,8 +148,12 @@ function getSelectionRange() { return sel; } -function doToggleBreakpoint(filename, url) { - var sel = getSelectionRange(); +function doToggleBreakpoint(filename, url, sel) { + if (sel==null) { + // use cursor position to determine the breakpoint line + // (gutter already tell us the selected line) + sel = getSelectionRange(); + } var dataForPost = prepareMultiPartPOST(new Array( prepareDataForSave('filename', filename), prepareDataForSave('sel_start', sel["start"]), @@ -199,6 +203,43 @@ function doToggleBreakpoint(filename, url) { return false; } +// on load, update all breakpoints markers: +function doListBreakpoints(filename, url) { + var dataForPost = prepareMultiPartPOST(new Array( + prepareDataForSave('filename', filename) + )); + jQuery.ajax({ + type: "POST", + contentType: 'multipart/form-data;boundary="'+dataForPost[1]+'"', + url: url, + dataType: "json", + data: dataForPost[0], + timeout: 5000, + beforeSend: function(xhr) { + xhr.setRequestHeader('web2py-component-location', + document.location); + xhr.setRequestHeader('web2py-component-element', + 'doListBreakpoints');}, + success: function(json,text,xhr){ + try { + if (json.error) { + window.location.href=json.redirect; + } else { + if (window.mirror) { + for (i in json.breakpoints) { + lineno = json.breakpoints[i]; + // mark the breakpoint if ok=True + editor.setMarker(lineno-1, + " %N%"); + } + } + } + } catch(e) { on_error(); } + }, + error: function(json) { on_error(); } + }); + return false; +} function keepalive(url) { jQuery.ajax({ diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index 2c58fbf0..e3076795 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -44,12 +44,25 @@ function web2py_event_handlers() { doc.on('keyup', 'input.double, input.decimal', function(){this.value=this.value.reverse().replace(/[^0-9\-\.,]|[\-](?=.)|[\.,](?=[0-9]*[\.,])/g,'').reverse();}); var confirm_message = (typeof w2p_ajax_confirm_message != 'undefined') ? w2p_ajax_confirm_message : "Are you sure you want to delete this object?"; doc.on('click', "input[type='checkbox'].delete", function(){if(this.checked) if(!confirm(confirm_message)) this.checked=false;}); + doc.ajaxSuccess(function(e, xhr) { var redirect=xhr.getResponseHeader('web2py-redirect-location'); - if (redirect != null) { + var command=xhr.getResponseHeader('web2py-component-command'); + var flash=xhr.getResponseHeader('web2py-component-flash'); + if (redirect !== null) { window.location = redirect; }; + if(command !== null){ + eval(decodeURIComponent(command)); + } + if(flash) { + jQuery('.flash') + .html(decodeURIComponent(flash)) + .append('×') + .slideDown(); + } }); + doc.ajaxError(function(e, xhr, settings, exception) { doc.off('click', '.flash') switch(xhr.status){ @@ -98,8 +111,6 @@ function web2py_ajax_page(method, action, data, target) { 'complete':function(xhr,text){ var html=xhr.responseText; var content=xhr.getResponseHeader('web2py-component-content'); - var command=xhr.getResponseHeader('web2py-component-command'); - var flash=xhr.getResponseHeader('web2py-component-flash'); var t = jQuery('#'+target); if(content=='prepend') t.prepend(html); else if(content=='append') t.append(html); @@ -107,14 +118,6 @@ function web2py_ajax_page(method, action, data, target) { web2py_trap_form(action,target); web2py_trap_link(target); web2py_ajax_init('#'+target); - if(command) - eval(decodeURIComponent(command)); - if(flash) { - jQuery('.flash') - .html(decodeURIComponent(flash)) - .append('×') - .slideDown(); - } } }); } diff --git a/applications/admin/views/default/edit.html b/applications/admin/views/default/edit.html index 19c416e4..cb1b6035 100644 --- a/applications/admin/views/default/edit.html +++ b/applications/admin/views/default/edit.html @@ -25,6 +25,13 @@ + {{elif TEXT_EDITOR == 'ace':}} @@ -103,7 +110,7 @@ jQuery(document).ready(function(){ {{if filetype=='python':}} {{=A(SPAN(T('toggle breakpoint')), _value="breakpoint", _name="breakpoint", - _onclick="return doToggleBreakpoint('%s','%s://%s%s');" % (filename, + _onclick="return doToggleBreakpoint('%s','%s://%s%s',null);" % (filename, request.env['wsgi_url_scheme'], request.env['http_host'], URL(c='debug', f='toggle_breakpoint')), _class="button special btn btn-inverse")}} @@ -202,7 +209,13 @@ jQuery(document).ready(function(){ autofocus: true, onCursorActivity: function() { editor.setLineClass(hlLine, null, null); - hlLine = editor.setLineClass(editor.getCursor().line, null, "activeline");} + hlLine = editor.setLineClass(editor.getCursor().line, null, "activeline");}, + onGutterClick: function(cm, n) { + sel = {start: n, end: n, data: ''}; + doToggleBreakpoint({{=XML("'%s','%s://%s%s',sel" % (filename, + request.env['wsgi_url_scheme'], request.env['http_host'], + URL(c='debug', f='toggle_breakpoint')))}}); + } }; var editor = CodeMirror.fromTextArea( document.getElementById("body"),cm_opts); @@ -293,4 +306,4 @@ window.onload = function() { {{pass}} - \ No newline at end of file + diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js index 2c58fbf0..e3076795 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -44,12 +44,25 @@ function web2py_event_handlers() { doc.on('keyup', 'input.double, input.decimal', function(){this.value=this.value.reverse().replace(/[^0-9\-\.,]|[\-](?=.)|[\.,](?=[0-9]*[\.,])/g,'').reverse();}); var confirm_message = (typeof w2p_ajax_confirm_message != 'undefined') ? w2p_ajax_confirm_message : "Are you sure you want to delete this object?"; doc.on('click', "input[type='checkbox'].delete", function(){if(this.checked) if(!confirm(confirm_message)) this.checked=false;}); + doc.ajaxSuccess(function(e, xhr) { var redirect=xhr.getResponseHeader('web2py-redirect-location'); - if (redirect != null) { + var command=xhr.getResponseHeader('web2py-component-command'); + var flash=xhr.getResponseHeader('web2py-component-flash'); + if (redirect !== null) { window.location = redirect; }; + if(command !== null){ + eval(decodeURIComponent(command)); + } + if(flash) { + jQuery('.flash') + .html(decodeURIComponent(flash)) + .append('×') + .slideDown(); + } }); + doc.ajaxError(function(e, xhr, settings, exception) { doc.off('click', '.flash') switch(xhr.status){ @@ -98,8 +111,6 @@ function web2py_ajax_page(method, action, data, target) { 'complete':function(xhr,text){ var html=xhr.responseText; var content=xhr.getResponseHeader('web2py-component-content'); - var command=xhr.getResponseHeader('web2py-component-command'); - var flash=xhr.getResponseHeader('web2py-component-flash'); var t = jQuery('#'+target); if(content=='prepend') t.prepend(html); else if(content=='append') t.append(html); @@ -107,14 +118,6 @@ function web2py_ajax_page(method, action, data, target) { web2py_trap_form(action,target); web2py_trap_link(target); web2py_ajax_init('#'+target); - if(command) - eval(decodeURIComponent(command)); - if(flash) { - jQuery('.flash') - .html(decodeURIComponent(flash)) - .append('×') - .slideDown(); - } } }); } diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index 2c58fbf0..e3076795 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -44,12 +44,25 @@ function web2py_event_handlers() { doc.on('keyup', 'input.double, input.decimal', function(){this.value=this.value.reverse().replace(/[^0-9\-\.,]|[\-](?=.)|[\.,](?=[0-9]*[\.,])/g,'').reverse();}); var confirm_message = (typeof w2p_ajax_confirm_message != 'undefined') ? w2p_ajax_confirm_message : "Are you sure you want to delete this object?"; doc.on('click', "input[type='checkbox'].delete", function(){if(this.checked) if(!confirm(confirm_message)) this.checked=false;}); + doc.ajaxSuccess(function(e, xhr) { var redirect=xhr.getResponseHeader('web2py-redirect-location'); - if (redirect != null) { + var command=xhr.getResponseHeader('web2py-component-command'); + var flash=xhr.getResponseHeader('web2py-component-flash'); + if (redirect !== null) { window.location = redirect; }; + if(command !== null){ + eval(decodeURIComponent(command)); + } + if(flash) { + jQuery('.flash') + .html(decodeURIComponent(flash)) + .append('×') + .slideDown(); + } }); + doc.ajaxError(function(e, xhr, settings, exception) { doc.off('click', '.flash') switch(xhr.status){ @@ -98,8 +111,6 @@ function web2py_ajax_page(method, action, data, target) { 'complete':function(xhr,text){ var html=xhr.responseText; var content=xhr.getResponseHeader('web2py-component-content'); - var command=xhr.getResponseHeader('web2py-component-command'); - var flash=xhr.getResponseHeader('web2py-component-flash'); var t = jQuery('#'+target); if(content=='prepend') t.prepend(html); else if(content=='append') t.append(html); @@ -107,14 +118,6 @@ function web2py_ajax_page(method, action, data, target) { web2py_trap_form(action,target); web2py_trap_link(target); web2py_ajax_init('#'+target); - if(command) - eval(decodeURIComponent(command)); - if(flash) { - jQuery('.flash') - .html(decodeURIComponent(flash)) - .append('×') - .slideDown(); - } } }); } diff --git a/applications/welcome/views/default/user.html b/applications/welcome/views/default/user.html index 475be5b3..b4bd0a15 100644 --- a/applications/welcome/views/default/user.html +++ b/applications/welcome/views/default/user.html @@ -4,7 +4,7 @@ {{ if request.args(0)=='login': if not 'register' in auth.settings.actions_disabled: - form.add_button(T('Register'),URL(args='register'),_class='btn') + form.add_button(T('Register'),URL(args='register', vars={'_next': request.vars._next} if request.vars._next else None),_class='btn') pass if not 'request_reset_password' in auth.settings.actions_disabled: form.add_button(T('Lost Password'),URL(args='request_reset_password'),_class='btn') diff --git a/gluon/__init__.py b/gluon/__init__.py index 3e9801f7..da8d5587 100644 --- a/gluon/__init__.py +++ b/gluon/__init__.py @@ -10,7 +10,7 @@ Web2Py framework modules ======================== """ -__all__ = ['A', 'B', 'BEAUTIFY', 'BODY', 'BR', 'CAT', 'CENTER', 'CLEANUP', 'CODE', 'CRYPT', 'DAL', 'DIV', 'EM', 'EMBED', 'FIELDSET', 'FORM', 'Field', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEAD', 'HR', 'HTML', 'HTTP', 'I', 'IFRAME', 'IMG', 'INPUT', 'IS_ALPHANUMERIC', 'IS_DATE', 'IS_DATETIME', 'IS_DATETIME_IN_RANGE', 'IS_DATE_IN_RANGE', 'IS_DECIMAL_IN_RANGE', 'IS_EMAIL', 'IS_EMPTY_OR', 'IS_EQUAL_TO', 'IS_EXPR', 'IS_FLOAT_IN_RANGE', 'IS_IMAGE', 'IS_INT_IN_RANGE', 'IS_IN_DB', 'IS_IN_SET', 'IS_IPV4', 'IS_LENGTH', 'IS_LIST_OF', 'IS_LOWER', 'IS_MATCH', 'IS_NOT_EMPTY', 'IS_NOT_IN_DB', 'IS_NULL_OR', 'IS_SLUG', 'IS_STRONG', 'IS_TIME', 'IS_UPLOAD_FILENAME', 'IS_UPPER', 'IS_URL', 'LABEL', 'LEGEND', 'LI', 'LINK', 'LOAD', 'MARKMIN', 'MENU', 'META', 'OBJECT', 'OL', 'ON', 'OPTGROUP', 'OPTION', 'P', 'PRE', 'SCRIPT', 'SELECT', 'SPAN', 'SQLFORM', 'SQLTABLE', 'STRONG', 'STYLE', 'TABLE', 'TAG', 'TBODY', 'TD', 'TEXTAREA', 'TFOOT', 'TH', 'THEAD', 'TITLE', 'TR', 'TT', 'UL', 'URL', 'XHTML', 'XML', 'redirect', 'current', 'embed64'] +__all__ = ['A', 'B', 'BEAUTIFY', 'BODY', 'BR', 'CAT', 'CENTER', 'CLEANUP', 'CODE', 'CRYPT', 'DAL', 'DIV', 'EM', 'EMBED', 'FIELDSET', 'FORM', 'Field', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEAD', 'HR', 'HTML', 'HTTP', 'I', 'IFRAME', 'IMG', 'INPUT', 'IS_ALPHANUMERIC', 'IS_DATE', 'IS_DATETIME', 'IS_DATETIME_IN_RANGE', 'IS_DATE_IN_RANGE', 'IS_DECIMAL_IN_RANGE', 'IS_EMAIL', 'IS_EMPTY_OR', 'IS_EQUAL_TO', 'IS_EXPR', 'IS_FLOAT_IN_RANGE', 'IS_IMAGE', 'IS_JSON', 'IS_INT_IN_RANGE', 'IS_IN_DB', 'IS_IN_SET', 'IS_IPV4', 'IS_LENGTH', 'IS_LIST_OF', 'IS_LOWER', 'IS_MATCH', 'IS_NOT_EMPTY', 'IS_NOT_IN_DB', 'IS_NULL_OR', 'IS_SLUG', 'IS_STRONG', 'IS_TIME', 'IS_UPLOAD_FILENAME', 'IS_UPPER', 'IS_URL', 'LABEL', 'LEGEND', 'LI', 'LINK', 'LOAD', 'MARKMIN', 'MENU', 'META', 'OBJECT', 'OL', 'ON', 'OPTGROUP', 'OPTION', 'P', 'PRE', 'SCRIPT', 'SELECT', 'SPAN', 'SQLFORM', 'SQLTABLE', 'STRONG', 'STYLE', 'TABLE', 'TAG', 'TBODY', 'TD', 'TEXTAREA', 'TFOOT', 'TH', 'THEAD', 'TITLE', 'TR', 'TT', 'UL', 'URL', 'XHTML', 'XML', 'redirect', 'current', 'embed64'] from globals import current from html import * diff --git a/gluon/contrib/login_methods/oauth20_account.py b/gluon/contrib/login_methods/oauth20_account.py index 0ed3adb2..e5c51c76 100644 --- a/gluon/contrib/login_methods/oauth20_account.py +++ b/gluon/contrib/login_methods/oauth20_account.py @@ -121,13 +121,13 @@ server for requests. It can be used for the optional"scope" parameters for Face """ # Create an OpenerDirector with support # for Basic HTTP Authentication... - - auth_handler = urllib2.HTTPBasicAuthHandler() - auth_handler.add_password(None, - uri, - self.client_id, - self.client_secret) - opener = urllib2.build_opener(auth_handler) + password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() + password_mgr.add_password(realm=None, + uri=uri, + user=self.client_id, + passwd=self.client_secret) + handler = urllib2.HTTPBasicAuthHandler(password_mgr) + opener = urllib2.build_opener(handler) return opener def accessToken(self): diff --git a/gluon/contrib/login_methods/openid_auth.py b/gluon/contrib/login_methods/openid_auth.py index 4e977030..0c763e81 100644 --- a/gluon/contrib/login_methods/openid_auth.py +++ b/gluon/contrib/login_methods/openid_auth.py @@ -129,18 +129,21 @@ class OpenIDAuth(object): def _define_alt_login_table(self): """ Define the OpenID login table. - Note: type is what I used for our project. We're going to support 'fackbook' and - 'plurk' alternate login methods. Otherwise it's always 'openid' and you + Note: oidtype is what I used for our project. + We're going to support 'fackbook' and + 'plurk' alternate login methods. + Otherwise it's always 'openid' and you may not need it. This should be easy to changed. (Just remove the field of "type" and remove the - "and db.alt_logins.type == type_" in _find_matched_openid function) + "and db.alt_logins.oidtype == type_" + in _find_matched_openid function) """ db = self.db table = db.define_table( self.table_alt_logins_name, Field('username', length=512, default=''), - Field('type', length=128, default='openid', readable=False), - Field('user', self.table_user, readable=False), + Field('oidtype', length=128, default='openid', readable=False), + Field('oiduser', self.table_user, readable=False), ) table.username.requires = IS_NOT_IN_DB(db, table.username) self.table_alt_logins = table @@ -213,7 +216,7 @@ class OpenIDAuth(object): # Get existed OpenID user user = db( - self.table_user.id == alt_login.user).select().first() + self.table_user.id == alt_login.oiduser).select().first() if user: if current.session.w2popenid: del(current.session.w2popenid) @@ -230,7 +233,7 @@ class OpenIDAuth(object): Get the matched OpenID for given """ query = ( - (db.alt_logins.username == oid) & (db.alt_logins.type == type_)) + (db.alt_logins.username == oid) & (db.alt_logins.oidtype == type_)) alt_login = db(query).select().first() # Get the OpenID record return alt_login @@ -239,7 +242,7 @@ class OpenIDAuth(object): Associate the user logged in with given OpenID """ # print "[DB] %s authenticated" % oid - self.db.alt_logins.insert(username=oid, user=user.id) + self.db.alt_logins.insert(username=oid, oiduser=user.id) def _form_with_notification(self): """ @@ -400,7 +403,7 @@ width: 400px; if 'delete_openid' in request.vars: self.remove_openid(request.vars.delete_openid) - query = self.db.alt_logins.user == self.auth.user.id + query = self.db.alt_logins.oiduser == self.auth.user.id alt_logins = self.db(query).select() l = [] for alt_login in alt_logins: @@ -529,7 +532,7 @@ class Web2pyStore(OpenIDStore): self.database.define_table(self.table_oid_nonces_name, Field('server_url', 'string', length=2047, required=True), - Field('timestamp', + Field('itimestamp', 'integer', required=True), Field('salt', 'string', length=40, required=True) @@ -591,12 +594,12 @@ class Web2pyStore(OpenIDStore): db = self.database if abs(timestamp - time.time()) > nonce.SKEW: return False - query = (db.oid_nonces.server_url == server_url) & (db.oid_nonces.timestamp == timestamp) & (db.oid_nonces.salt == salt) + query = (db.oid_nonces.server_url == server_url) & (db.oid_nonces.itimestamp == timestamp) & (db.oid_nonces.salt == salt) if db(query).count() > 0: return False else: db.oid_nonces.insert(server_url=server_url, - timestamp=timestamp, + itimestamp=timestamp, salt=salt) return True @@ -628,7 +631,7 @@ class Web2pyStore(OpenIDStore): """ db = self.database - query = (db.oid_nonces.timestamp < time.time() - nonce.SKEW) + query = (db.oid_nonces.itimestamp < time.time() - nonce.SKEW) return db(query).delete() def cleanupAssociations(self): diff --git a/gluon/dal.py b/gluon/dal.py index b4df67e9..2e7a1569 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -621,6 +621,7 @@ class BaseAdapter(ConnectionPool): 'boolean': 'CHAR(1)', 'string': 'CHAR(%(length)s)', 'text': 'TEXT', + 'json': 'TEXT', 'password': 'CHAR(%(length)s)', 'blob': 'BLOB', 'upload': 'CHAR(%(length)s)', @@ -1160,8 +1161,8 @@ class BaseAdapter(ConnectionPool): logfile.write('success!\n') def _insert(self, table, fields): - keys = ','.join(f.name for f,v in fields) - values = ','.join(self.expand(v,f.type) for f,v in fields) + keys = ','.join(f.name for f, v in fields) + values = ','.join(self.expand(v, f.type) for f, v in fields) return 'INSERT INTO %s(%s) VALUES (%s);' % (table, keys, values) def insert(self, table, fields): @@ -1225,7 +1226,7 @@ class BaseAdapter(ConnectionPool): self.expand('%'+second, 'string')) def CONTAINS(self, first, second): - if first.type in ('string', 'text'): + if first.type in ('string', 'text', 'json'): key = '%'+str(second).replace('%','%%')+'%' elif first.type.startswith('list:'): key = '%|'+str(second).replace('|','||').replace('%','%%')+'|%' @@ -1291,6 +1292,8 @@ class BaseAdapter(ConnectionPool): return '%s AS %s' % (self.expand(first), second) def ON(self, first, second): + if use_common_filters(second): + second = self.common_filter(second,[first._tablename]) return '%s ON %s' % (self.expand(first), self.expand(second)) def INVERT(self, first): @@ -1466,9 +1469,6 @@ class BaseAdapter(ConnectionPool): if not tablename in tablenames: tablenames.append(tablename) - if use_common_filters(query): - query = self.common_filter(query,tablenames) - if len(tablenames) < 1: raise SyntaxError('Set: no tables selected') self._colnames = map(self.expand, fields) @@ -1477,10 +1477,6 @@ class BaseAdapter(ConnectionPool): field = field.st_astext() return self.expand(field) sql_f = ', '.join(map(geoexpand, fields)) - if query: - sql_w = ' WHERE ' + self.expand(query) - else: - sql_w = '' sql_o = '' sql_s = '' left = args_get('left', False) @@ -1530,6 +1526,13 @@ class BaseAdapter(ConnectionPool): important_tablenames = joint + joinont + tables_to_merge.keys() excluded = [t for t in tablenames if not t in important_tablenames ] + else: + excluded = tablenames + + if use_common_filters(query): + query = self.common_filter(query,excluded) + sql_w = ' WHERE ' + self.expand(query) if query else '' + def alias(t): return str(self.db[t]) if inner_join and not left: @@ -1717,7 +1720,7 @@ class BaseAdapter(ConnectionPool): obj = obj() if isinstance(fieldtype, SQLCustomType): value = fieldtype.encoder(obj) - if fieldtype.type in ('string','text'): + if fieldtype.type in ('string','text', 'json'): return self.adapt(value) return value if isinstance(obj, (Expression, Field)): @@ -1731,11 +1734,12 @@ class BaseAdapter(ConnectionPool): obj = map(str,obj) else: obj = map(int,obj) - if isinstance(obj, (list, tuple)): + # we don't want to bar_encode json objects + if isinstance(obj, (list, tuple)) and (not fieldtype == "json"): obj = bar_encode(obj) if obj is None: return 'NULL' - if obj == '' and not fieldtype[:2] in ['st', 'te', 'pa', 'up']: + if obj == '' and not fieldtype[:2] in ['st', 'te', 'js', 'pa', 'up']: return 'NULL' r = self.represent_exceptions(obj, fieldtype) if not r is None: @@ -1778,6 +1782,16 @@ class BaseAdapter(ConnectionPool): obj = obj.isoformat()[:10] else: obj = str(obj) + elif (fieldtype == 'json'): + if not isinstance(obj, basestring): + if have_serializers: + obj = serializers.json(obj) + else: + try: + import json as simplejson + except ImportError: + import gluon.contrib.simplejson as simplejson + obj = simplejson.dumps(items) if not isinstance(obj,bytes): obj = bytes(obj) try: @@ -1909,6 +1923,20 @@ class BaseAdapter(ConnectionPool): def parse_double(self, value, field_type): return float(value) + def parse_json(self, value, field_type): + if isinstance(value, basestring): + if isinstance(value, unicode): + value = value.encode('utf-8') + if have_serializers: + value = serializers.loads_json(value) + else: + try: + import json as simplejson + except ImportError: + import gluon.contrib.simplejson as simplejson + value = simplejson.loads(value) + return value + def build_parsemap(self): self.parsemap = { 'id':self.parse_id, @@ -1923,6 +1951,7 @@ class BaseAdapter(ConnectionPool): 'datetime':self.parse_datetime, 'blob':self.parse_blob, 'decimal':self.parse_decimal, + 'json':self.parse_json, 'list:integer':self.parse_list_integers, 'list:reference':self.parse_list_references, 'list:string':self.parse_list_strings, @@ -2305,6 +2334,7 @@ class MySQLAdapter(BaseAdapter): 'boolean': 'CHAR(1)', 'string': 'VARCHAR(%(length)s)', 'text': 'LONGTEXT', + 'json': 'LONGTEXT', 'password': 'VARCHAR(%(length)s)', 'blob': 'LONGBLOB', 'upload': 'VARCHAR(%(length)s)', @@ -2417,6 +2447,7 @@ class PostgreSQLAdapter(BaseAdapter): 'boolean': 'CHAR(1)', 'string': 'VARCHAR(%(length)s)', 'text': 'TEXT', + 'json': 'TEXT', 'password': 'VARCHAR(%(length)s)', 'blob': 'BYTEA', 'upload': 'VARCHAR(%(length)s)', @@ -2458,7 +2489,7 @@ class PostgreSQLAdapter(BaseAdapter): def ADD(self, first, second): t = first.type - if t in ('text','string','password','upload','blob'): + if t in ('text','string','password', 'json', 'upload','blob'): return '(%s || %s)' % (self.expand(first), self.expand(second, t)) else: return '(%s + %s)' % (self.expand(first), self.expand(second, t)) @@ -2559,7 +2590,7 @@ class PostgreSQLAdapter(BaseAdapter): self.expand('%'+second,'string')) def CONTAINS(self,first,second): - if first.type in ('string','text'): + if first.type in ('string','text', 'json'): key = '%'+str(second).replace('%','%%')+'%' elif first.type.startswith('list:'): key = '%|'+str(second).replace('|','||').replace('%','%%')+'|%' @@ -2580,12 +2611,17 @@ class PostgreSQLAdapter(BaseAdapter): """ return 'ST_AsText(%s)' %(self.expand(first)) -# def ST_CONTAINED(self, first, second): -# """ -# non-standard function based on ST_Contains with parameters reversed -# http://postgis.org/docs/ST_Contains.html -# """ -# return 'ST_Contains(%s,%s)' % (self.expand(second, first.type), self.expand(first)) + def ST_X(self, first): + """ + http://postgis.org/docs/ST_X.html + """ + return 'ST_X(%s)' %(self.expand(first)) + + def ST_Y(self, first): + """ + http://postgis.org/docs/ST_Y.html + """ + return 'ST_Y(%s)' %(self.expand(first)) def ST_CONTAINS(self, first, second): """ @@ -2659,6 +2695,7 @@ class NewPostgreSQLAdapter(PostgreSQLAdapter): 'boolean': 'CHAR(1)', 'string': 'VARCHAR(%(length)s)', 'text': 'TEXT', + 'json': 'TEXT', 'password': 'VARCHAR(%(length)s)', 'blob': 'BYTEA', 'upload': 'VARCHAR(%(length)s)', @@ -2758,6 +2795,7 @@ class OracleAdapter(BaseAdapter): 'boolean': 'CHAR(1)', 'string': 'VARCHAR2(%(length)s)', 'text': 'CLOB', + 'json': 'CLOB', 'password': 'VARCHAR2(%(length)s)', 'blob': 'CLOB', 'upload': 'VARCHAR2(%(length)s)', @@ -2928,6 +2966,7 @@ class MSSQLAdapter(BaseAdapter): 'boolean': 'BIT', 'string': 'VARCHAR(%(length)s)', 'text': 'TEXT', + 'json': 'TEXT', 'password': 'VARCHAR(%(length)s)', 'blob': 'IMAGE', 'upload': 'VARCHAR(%(length)s)', @@ -3141,6 +3180,7 @@ class MSSQL2Adapter(MSSQLAdapter): 'boolean': 'CHAR(1)', 'string': 'NVARCHAR(%(length)s)', 'text': 'NTEXT', + 'json': 'NTEXT', 'password': 'NVARCHAR(%(length)s)', 'blob': 'IMAGE', 'upload': 'NVARCHAR(%(length)s)', @@ -3165,7 +3205,7 @@ class MSSQL2Adapter(MSSQLAdapter): def represent(self, obj, fieldtype): value = BaseAdapter.represent(self, obj, fieldtype) - if fieldtype in ('string','text') and value[:1]=="'": + if fieldtype in ('string','text', 'json') and value[:1]=="'": value = 'N'+value return value @@ -3179,6 +3219,7 @@ class SybaseAdapter(MSSQLAdapter): 'boolean': 'BIT', 'string': 'CHAR VARYING(%(length)s)', 'text': 'TEXT', + 'json': 'TEXT', 'password': 'CHAR VARYING(%(length)s)', 'blob': 'IMAGE', 'upload': 'CHAR VARYING(%(length)s)', @@ -3273,6 +3314,7 @@ class FireBirdAdapter(BaseAdapter): 'boolean': 'CHAR(1)', 'string': 'VARCHAR(%(length)s)', 'text': 'BLOB SUB_TYPE 1', + 'json': 'BLOB SUB_TYPE 1', 'password': 'VARCHAR(%(length)s)', 'blob': 'BLOB SUB_TYPE 0', 'upload': 'VARCHAR(%(length)s)', @@ -3441,6 +3483,7 @@ class InformixAdapter(BaseAdapter): 'boolean': 'CHAR(1)', 'string': 'VARCHAR(%(length)s)', 'text': 'BLOB SUB_TYPE 1', + 'json': 'BLOB SUB_TYPE 1', 'password': 'VARCHAR(%(length)s)', 'blob': 'BLOB SUB_TYPE 0', 'upload': 'VARCHAR(%(length)s)', @@ -3568,6 +3611,7 @@ class DB2Adapter(BaseAdapter): 'boolean': 'CHAR(1)', 'string': 'VARCHAR(%(length)s)', 'text': 'CLOB', + 'json': 'CLOB', 'password': 'VARCHAR(%(length)s)', 'blob': 'BLOB', 'upload': 'VARCHAR(%(length)s)', @@ -3653,6 +3697,7 @@ class TeradataAdapter(BaseAdapter): 'boolean': 'CHAR(1)', 'string': 'VARCHAR(%(length)s)', 'text': 'CLOB', + 'json': 'CLOB', 'password': 'VARCHAR(%(length)s)', 'blob': 'BLOB', 'upload': 'VARCHAR(%(length)s)', @@ -3719,6 +3764,7 @@ class IngresAdapter(BaseAdapter): 'boolean': 'CHAR(1)', 'string': 'VARCHAR(%(length)s)', 'text': 'CLOB', + 'json': 'CLOB', 'password': 'VARCHAR(%(length)s)', ## Not sure what this contains utf8 or nvarchar. Or even bytes? 'blob': 'BLOB', 'upload': 'VARCHAR(%(length)s)', ## FIXME utf8 or nvarchar... or blob? what is this type? @@ -3821,6 +3867,7 @@ class IngresUnicodeAdapter(IngresAdapter): 'boolean': 'CHAR(1)', 'string': 'NVARCHAR(%(length)s)', 'text': 'NCLOB', + 'json': 'NCLOB', 'password': 'NVARCHAR(%(length)s)', ## Not sure what this contains utf8 or nvarchar. Or even bytes? 'blob': 'BLOB', 'upload': 'VARCHAR(%(length)s)', ## FIXME utf8 or nvarchar... or blob? what is this type? @@ -3851,6 +3898,7 @@ class SAPDBAdapter(BaseAdapter): 'boolean': 'CHAR(1)', 'string': 'VARCHAR(%(length)s)', 'text': 'LONG', + 'json': 'LONG', 'password': 'VARCHAR(%(length)s)', 'blob': 'LONG', 'upload': 'VARCHAR(%(length)s)', @@ -4152,7 +4200,7 @@ class NoSQLAdapter(BaseAdapter): if not isinstance(obj, (list, tuple)): obj = [obj] if obj == '' and not \ - (is_string and fieldtype[:2] in ['st','te','pa','up']): + (is_string and fieldtype[:2] in ['st','te', 'pa','up']): return None if not obj is None: if isinstance(obj, list) and not is_list: @@ -4195,6 +4243,17 @@ class NoSQLAdapter(BaseAdapter): obj = datetime.datetime(y, m, d, h, mi, s) elif fieldtype == 'blob': pass + elif fieldtype == 'json': + if isinstance(obj, basestring): + obj = self.to_unicode(obj) + if have_serializers: + obj = serializers.loads_json(obj) + else: + try: + import json as simplejson + except ImportError: + import gluon.contrib.simplejson as simplejson + obj = simplejson.loads(obj) elif is_string and field_is_type('list:string'): return map(self.to_unicode,obj) elif is_list: @@ -4302,6 +4361,7 @@ class GoogleDatastoreAdapter(NoSQLAdapter): 'boolean': gae.BooleanProperty, 'string': (lambda: gae.StringProperty(multiline=True)), 'text': gae.TextProperty, + 'json': gae.TextProperty, 'password': gae.StringProperty, 'blob': gae.BlobProperty, 'upload': gae.StringProperty, @@ -4381,7 +4441,7 @@ class GoogleDatastoreAdapter(NoSQLAdapter): def expand(self,expression,field_type=None): if isinstance(expression,Field): - if expression.type in ('text','blob'): + if expression.type in ('text', 'blob', 'json'): raise SyntaxError('AppEngine does not index by: %s' % expression.type) return expression.name elif isinstance(expression, (Expression, Query)): @@ -4520,7 +4580,7 @@ class GoogleDatastoreAdapter(NoSQLAdapter): elif args_get('projection') == True: projection = [] for f in fields: - if f.type in ['text', 'blob']: + if f.type in ['text', 'blob', 'json']: raise SyntaxError( "text and blob field types not allowed in projection queries") else: @@ -4701,6 +4761,7 @@ class CouchDBAdapter(NoSQLAdapter): 'boolean': bool, 'string': str, 'text': str, + 'json': str, 'password': str, 'blob': str, 'upload': str, @@ -4900,6 +4961,7 @@ class MongoDBAdapter(NoSQLAdapter): 'boolean': bool, 'string': str, 'text': str, + 'json': str, 'password': str, 'blob': str, 'upload': str, @@ -4917,170 +4979,205 @@ class MongoDBAdapter(NoSQLAdapter): 'list:reference': list, } + error_messages = {"javascript_needed": "This must yet be replaced" + + " with javascript in order to work."} + def __init__(self,db,uri='mongodb://127.0.0.1:5984/db', - pool_size=0,folder=None,db_codec ='UTF-8', + pool_size=0, folder=None, db_codec ='UTF-8', credential_decoder=IDENTITY, driver_args={}, adapter_args={}, do_connect=True): + self.db = db self.uri = uri if do_connect: self.find_driver(adapter_args) + import random + from bson.objectid import ObjectId + from bson.son import SON + import pymongo.uri_parser + + m = pymongo.uri_parser.parse_uri(uri) + + self.SON = SON + self.ObjectId = ObjectId + self.random = random - m=None - try: - #Since version 2 - import pymongo.uri_parser - m = pymongo.uri_parser.parse_uri(uri) - except ImportError: - try: - #before version 2 of pymongo - import pymongo.connection - m = pymongo.connection._parse_uri(uri) - except ImportError: - raise ImportError("Uriparser for mongodb is not available") - except: - raise SyntaxError("This type of uri is not supported by the mongodb uri parser") self.dbengine = 'mongodb' self.folder = folder db['_lastsql'] = '' self.db_codec = 'UTF-8' self.pool_size = pool_size - #this is the minimum amount of replicates that it should wait for on insert/update + #this is the minimum amount of replicates that it should wait + # for on insert/update self.minimumreplication = adapter_args.get('minimumreplication',0) - #by default alle insert and selects are performand asynchronous, but now the default is - #synchronous, except when overruled by either this default or function parameter + # by default all inserts and selects are performand asynchronous, + # but now the default is + # synchronous, except when overruled by either this default or + # function parameter self.safe = adapter_args.get('safe',True) - if isinstance(m,tuple): m = {"database" : m[1]} if m.get('database')==None: raise SyntaxError("Database is required!") def connector(uri=self.uri,m=m): try: - return self.driver.Connection(uri)[m.get('database')] + # Connection() is deprecated + if hasattr(self.driver, "MongoClient"): + Connection = self.driver.MongoClient + else: + Connection = self.driver.Connection + return Connection(uri)[m.get('database')] except self.driver.errors.ConnectionFailure: inst = sys.exc_info()[1] - raise SyntaxError("The connection to " + uri + " could not be made") - except Exception: - inst = sys.exc_info()[1] - if inst == "cannot specify database without a username and password": - raise SyntaxError("You are probebly running version 1.1 of pymongo which contains a bug which requires authentication. Update your pymongo.") - else: - raise SyntaxError("This is not an official Mongodb uri (http://www.mongodb.org/display/DOCS/Connections) Error : %s" % inst) + raise SyntaxError("The connection to " + + uri + " could not be made") + self.reconnect(connector,cursor=False) + def object_id(self, arg=None): + """ Convert input to a valid Mongodb ObjectId instance + + self.object_id("") -> ObjectId (not unique) instance """ + if not arg: + arg = 0 + if isinstance(arg, basestring): + # we assume an integer as default input + rawhex = len(arg.replace("0x", "").replace("L", "")) == 24 + if arg.isdigit() and (not rawhex): + arg = int(arg) + elif arg == "": + arg = int("0x%sL" % \ + "".join([self.random.choice("0123456789abcdef") \ + for x in range(24)]), 0) + elif arg.isalnum(): + if not arg.startswith("0x"): + arg = "0x%s" % arg + try: + arg = int(arg, 0) + except ValueError, e: + raise ValueError( + "invalid objectid argument string: %s" % e) + else: + raise ValueError("Invalid objectid argument string. " + + "Requires an integer or base 16 value") + elif isinstance(arg, self.ObjectId): + return arg + if not isinstance(arg, (int, long)): + raise TypeError("object_id argument must be of type " + + "ObjectId or an objectid representable integer") + if arg == 0: + hexvalue = "".zfill(24) + else: + hexvalue = hex(arg)[2:].replace("L", "") + return self.ObjectId(hexvalue) + def represent(self, obj, fieldtype): value = NoSQLAdapter.represent(self, obj, fieldtype) if fieldtype =='date': if value == None: return value - t = datetime.time(0, 0, 0)#this piece of data can be stripped of based on the fieldtype - return datetime.datetime.combine(value, t) #mongodb doesn't has a date object and so it must datetime, string or integer + # this piece of data can be stripped off based on the fieldtype + t = datetime.time(0, 0, 0) + # mongodb doesn't has a date object and so it must datetime, + # string or integer + return datetime.datetime.combine(value, t) elif fieldtype == 'time': if value == None: return value - d = datetime.date(2000, 1, 1) #this piece of data can be stripped of based on the fieldtype - return datetime.datetime.combine(d, value) #mongodb doesn't has a time object and so it must datetime, string or integer - elif fieldtype == 'list:string' or fieldtype == 'list:integer' or fieldtype == 'list:reference': - return value #raise SyntaxError("Not Supported") + # this piece of data can be stripped of based on the fieldtype + d = datetime.date(2000, 1, 1) + # mongodb doesn't has a time object and so it must datetime, + # string or integer + return datetime.datetime.combine(d, value) + elif fieldtype == 'list:string' or \ + fieldtype == 'list:integer' or \ + fieldtype == 'list:reference': + return value return value - #Safe determines whether a asynchronious request is done or a synchronious action is done - #For safety, we use by default synchronious requests - def insert(self,table,fields,safe=None): + # Safe determines whether a asynchronious request is done or a + # synchronious action is done + # For safety, we use by default synchronious requests + def insert(self, table, fields, safe=None): if safe==None: - safe=self.safe + safe = self.safe ctable = self.connection[table._tablename] - values = dict((k.name,self.represent(v,table[k.name].type)) for k,v in fields) - ctable.insert(values,safe=safe) + values = dict() + for k, v in fields: + if not k.name in ["id", "safe"]: + fieldname = k.name + fieldtype = table[k.name].type + if ("reference" in fieldtype) or (fieldtype=="id"): + values[fieldname] = self.object_id(v) + else: + values[fieldname] = self.represent(v, fieldtype) + ctable.insert(values, safe=safe) return int(str(values['_id']), 16) - def create_table(self, table, migrate=True, fake_migrate=False, polymodel=None, isCapped=False): + def create_table(self, table, migrate=True, fake_migrate=False, + polymodel=None, isCapped=False): if isCapped: raise RuntimeError("Not implemented") - else: - pass - def count(self,query,distinct=None,snapshot=True): + def count(self, query, distinct=None, snapshot=True): if distinct: raise RuntimeError("COUNT DISTINCT not supported") if not isinstance(query,Query): raise SyntaxError("Not Supported") tablename = self.get_table(query) - return int(self.select(query,[self.db[tablename]._id],{},count=True,snapshot=snapshot)['count']) - #Maybe it would be faster if we just implemented the pymongo .count() function which is probably quicker? - # therefor call __select() connection[table].find(query).count() Since this will probably reduce the return set? + return int(self.select(query,[self.db[tablename]._id], {}, + count=True,snapshot=snapshot)['count']) + # Maybe it would be faster if we just implemented the pymongo + # .count() function which is probably quicker? + # therefor call __select() connection[table].find(query).count() + # Since this will probably reduce the return set? def expand(self, expression, field_type=None): - try: - from pymongo.objectid import ObjectId - except ImportError: - from bson.objectid import ObjectId - #if isinstance(expression,Field): - # if expression.type=='id': - # return {_id}" if isinstance(expression, Query): # any query using 'id':= - # set name as _id (as per pymongo/mongodb primary key) + # set name as _id (as per pymongo/mongodb primary key) # convert second arg to an objectid field # (if its not already) # if second arg is 0 convert to objectid if isinstance(expression.first,Field) and \ - expression.first.type == 'id': - expression.first.name = '_id' - if expression.second != 0 and \ - not isinstance(expression.second,ObjectId): - if isinstance(expression.second,int): - try: - # Because the reference field is by default - # an integer and therefore this must be an - # integer to be able to work with other - # databases - expression.second = ObjectId(("%X" % expression.second)) - except: - raise SyntaxError('The second argument must by an integer that can represent an objectid.') - else: - try: - #But a direct id is also possible - expression.second = ObjectId(expression.second) - except: - raise SyntaxError('second argument must be of type ObjectId or an objectid representable integer') - elif expression.second == 0: - expression.second = ObjectId('000000000000000000000000') - return expression.op(expression.first, expression.second) + ((expression.first.type == 'id') or \ + ("reference" in expression.first.type)): + if expression.first.type == 'id': + expression.first.name = '_id' + # cast to Mongo ObjectId + expression.second = self.object_id(expression.second) + result = expression.op(expression.first, expression.second) if isinstance(expression, Field): if expression.type=='id': - return "_id" + result = "_id" else: - return expression.name - #return expression + result = expression.name + elif isinstance(expression, (Expression, Query)): if not expression.second is None: - return expression.op(expression.first, expression.second) + result = expression.op(expression.first, expression.second) elif not expression.first is None: - return expression.op(expression.first) + result = expression.op(expression.first) elif not isinstance(expression.op, str): - return expression.op() + result = expression.op() else: - return expression.op + result = expression.op elif field_type: - return str(self.represent(expression,field_type)) + result = str(self.represent(expression,field_type)) elif isinstance(expression,(list,tuple)): - return ','.join(self.represent(item,field_type) for item in expression) + result = ','.join(self.represent(item,field_type) for + item in expression) else: - return expression - - def _select(self,query,fields,attributes): - try: - from bson.son import SON - except ImportError: - from pymongo.son import SON + result = expression + return result + def _select(self, query, fields, attributes): if 'for_update' in attributes: logging.warn('mongodb does not support for_update') - for key in set(attributes.keys())-set(('limitby','orderby','for_update')): + for key in set(attributes.keys())-set(('limitby', + 'orderby','for_update')): if attributes[key]!=None: - raise SyntaxError('invalid select attribute: %s' % key) + logging.warn('select attribute not implemented: %s' % key) new_fields=[] mongosort_list = [] @@ -5088,28 +5185,27 @@ class MongoDBAdapter(NoSQLAdapter): # try an orderby attribute orderby = attributes.get('orderby', False) limitby = attributes.get('limitby', False) - #distinct = attributes.get('distinct', False) + # distinct = attributes.get('distinct', False) if orderby: - #print "in if orderby %s" % orderby if isinstance(orderby, (list, tuple)): orderby = xorify(orderby) # !!!! need to add 'random' for f in self.expand(orderby).split(','): if f.startswith('-'): - mongosort_list.append((f[1:],-1)) + mongosort_list.append((f[1:], -1)) else: - mongosort_list.append((f,1)) + mongosort_list.append((f, 1)) if limitby: limitby_skip, limitby_limit = limitby else: limitby_skip = limitby_limit = 0 - mongofields_dict = SON() + mongofields_dict = self.SON() mongoqry_dict = {} for item in fields: - if isinstance(item,SQLALL): + if isinstance(item, SQLALL): new_fields += item._table else: new_fields.append(item) @@ -5119,60 +5215,73 @@ class MongoDBAdapter(NoSQLAdapter): elif len(fields) != 0: tablename = fields[0].tablename else: - raise SyntaxError("The table name could not be found in the query nor from the select statement.") + raise SyntaxError("The table name could not be found in " + + "the query nor from the select statement.") + mongoqry_dict = self.expand(query) fields = fields or self.db[tablename] for field in fields: mongofields_dict[field.name] = 1 - return tablename, mongoqry_dict, mongofields_dict, \ - mongosort_list, limitby_limit, limitby_skip - # need to define all the 'sql' methods gt,lt etc.... + return tablename, mongoqry_dict, mongofields_dict, mongosort_list, \ + limitby_limit, limitby_skip - def select(self,query,fields,attributes,count=False,snapshot=False): - try: - from pymongo.objectid import ObjectId - except ImportError: - from bson.objectid import ObjectId - tablename, mongoqry_dict, mongofields_dict, \ - mongosort_list, limitby_limit, limitby_skip = \ - self._select(query,fields,attributes) + + def select(self, query, fields, attributes, count=False, + snapshot=False): + # TODO: support joins + tablename, mongoqry_dict, mongofields_dict, mongosort_list, \ + limitby_limit, limitby_skip = self._select(query, fields, attributes) ctable = self.connection[tablename] + if count: return {'count' : ctable.find( mongoqry_dict, mongofields_dict, skip=limitby_skip, limit=limitby_limit, sort=mongosort_list, snapshot=snapshot).count()} else: - mongo_list_dicts = ctable.find( - mongoqry_dict, mongofields_dict, - skip=limitby_skip, limit=limitby_limit, - sort=mongosort_list, snapshot=snapshot) # pymongo cursor object - # DEBUG: print "mongo_list_dicts=%s" % mongo_list_dicts + # pymongo cursor object + mongo_list_dicts = ctable.find(mongoqry_dict, + mongofields_dict, skip=limitby_skip, + limit=limitby_limit, sort=mongosort_list, + snapshot=snapshot) rows = [] - ### populate row in proper order - colnames = [str(field) for field in fields] - for k,record in enumerate(mongo_list_dicts): + # populate row in proper order + # Here we replace ._id with .id to follow the standard naming + colnames = [] + newnames = [] + for field in fields: + colname = str(field) + colnames.append(colname) + tablename, fieldname = colname.split(".") + if fieldname == "_id": + # Mongodb reserved uuid key + field.name = "id" + newnames.append(".".join((tablename, field.name))) + + for record in mongo_list_dicts: row=[] - for fullcolname in colnames: - colname = fullcolname.split('.')[1] - column = '_id' if colname=='id' else colname - if column in record: - if column == '_id' and isinstance( - record[column],ObjectId): - value = int(str(record[column]),16) - elif column != '_id': - value = record[column] + for colname in colnames: + tablename, fieldname = colname.split(".") + # switch to Mongo _id uuids for retrieving + # record id's + if fieldname == "id": fieldname = "_id" + if fieldname in record: + if isinstance(record[fieldname], + self.ObjectId): + value = int(str(record[fieldname]), 16) else: - value = None + value = record[fieldname] else: value = None row.append(value) rows.append(row) - processor = attributes.get('processor',self.parse) - return processor(rows,fields,colnames,False) + processor = attributes.get('processor', self.parse) + result = processor(rows, fields, newnames, False) + return result - def INVERT(self,first): + + def INVERT(self, first): #print "in invert first=%s" % first return '-%s' % self.expand(first) @@ -5181,62 +5290,69 @@ class MongoDBAdapter(NoSQLAdapter): ctable.drop() - def truncate(self,table,mode,safe=None): - if safe==None: + def truncate(self, table, mode, safe=None): + if safe == None: safe=self.safe ctable = self.connection[table._tablename] ctable.remove(None, safe=True) - #the update function should return a string - def oupdate(self,tablename,query,fields): - if not isinstance(query,Query): + def oupdate(self, tablename, query, fields): + if not isinstance(query, Query): raise SyntaxError("Not Supported") filter = None if query: filter = self.expand(query) - f_v = [] + modify = {'$set': dict((k.name, self.represent(v, k.type)) for + k, v in fields)} + return modify, filter - - modify = { '$set' : dict(((k.name,self.represent(v,k.type)) for k,v in fields)) } - return modify,filter - - #TODO implement update - #TODO implement set operator - #TODO implement find and modify - #todo implement complex update - def update(self,tablename,query,fields,safe=None): - if safe==None: - safe=self.safe - #return amount of adjusted rows or zero, but no exceptions related not finding the result - if not isinstance(query,Query): + def update(self, tablename, query, fields, safe=None): + if safe == None: + safe = self.safe + # return amount of adjusted rows or zero, but no exceptions + # @ related not finding the result + if not isinstance(query, Query): raise RuntimeError("Not implemented") - amount = self.count(query,False) - modify,filter = self.oupdate(tablename,query,fields) + amount = self.count(query, False) + modify, filter = self.oupdate(tablename, query, fields) try: + result = self.connection[tablename].update(filter, + modify, multi=True, safe=safe) if safe: - return self.connection[tablename].update(filter,modify,multi=True,safe=safe).n + try: + # if result count is available fetch it + return result["n"] + except (KeyError, AttributeError, TypeError): + return amount else: - amount =self.count(query) - self.connection[tablename].update(filter,modify,multi=True,safe=safe) return amount - except: - #TODO Reverse update query to verifiy that the query succeded - return 0 - """ - An special update operator that enables the update of specific field - return a dict - """ - - + except Exception, e: + # TODO Reverse update query to verifiy that the query succeded + raise RuntimeError("uncaught exception when updating rows: %s" % e) #this function returns a dict with the where clause and update fields def _update(self,tablename,query,fields): - return str(self.oupdate(tablename,query,fields)) + return str(self.oupdate(tablename, query, fields)) + + def delete(self, tablename, query, safe=None): + if safe is None: + safe = self.safe + amount = 0 + amount = self.count(query, False) + if not isinstance(query, Query): + raise RuntimeError("query type %s is not supported" % \ + type(query)) + filter = self.expand(query) + self._delete(tablename, filter, safe=safe) + return amount + + def _delete(self, tablename, filter, safe=None): + return self.connection[tablename].remove(filter, safe=safe) def bulk_insert(self, table, items): return [self.insert(table,item) for item in items] - #TODO This will probably not work:( + # TODO This will probably not work:( def NOT(self, first): result = {} result["$not"] = self.expand(first) @@ -5249,7 +5365,7 @@ class MongoDBAdapter(NoSQLAdapter): return f def OR(self,first,second): - # pymongo expects: .find( {'$or' : [{'name':'1'}, {'name':'2'}] } ) + # pymongo expects: .find({'$or': [{'name':'1'}, {'name':'2'}]}) result = {} f = self.expand(first) s = self.expand(second) @@ -5266,9 +5382,6 @@ class MongoDBAdapter(NoSQLAdapter): def EQ(self,first,second): result = {} - #if second is None: - #return '(%s == null)' % self.expand(first) - #return '(%s == %s)' % (self.expand(first),self.expand(second,first.type)) result[self.expand(first)] = self.expand(second) return result @@ -5304,84 +5417,101 @@ class MongoDBAdapter(NoSQLAdapter): return result def ADD(self, first, second): - raise NotImplementedError("This must yet be replaced with javascript in order to accomplish this. Sorry") - return '%s + %s' % (self.expand(first), self.expand(second, first.type)) + raise NotImplementedError(self.error_messages["javascript_needed"]) + return '%s + %s' % (self.expand(first), + self.expand(second, first.type)) def SUB(self, first, second): - raise NotImplementedError("This must yet be replaced with javascript in order to accomplish this. Sorry") - return '(%s - %s)' % (self.expand(first), self.expand(second, first.type)) + raise NotImplementedError(self.error_messages["javascript_needed"]) + return '(%s - %s)' % (self.expand(first), + self.expand(second, first.type)) def MUL(self, first, second): - raise NotImplementedError("This must yet be replaced with javascript in order to accomplish this. Sorry") - return '(%s * %s)' % (self.expand(first), self.expand(second, first.type)) + raise NotImplementedError(self.error_messages["javascript_needed"]) + return '(%s * %s)' % (self.expand(first), + self.expand(second, first.type)) def DIV(self, first, second): - raise NotImplementedError("This must yet be replaced with javascript in order to accomplish this. Sorry") - return '(%s / %s)' % (self.expand(first), self.expand(second, first.type)) + raise NotImplementedError(self.error_messages["javascript_needed"]) + return '(%s / %s)' % (self.expand(first), + self.expand(second, first.type)) def MOD(self, first, second): - raise NotImplementedError("This must yet be replaced with javascript in order to accomplish this. Sorry") - return '(%s %% %s)' % (self.expand(first), self.expand(second, first.type)) + raise NotImplementedError(self.error_messages["javascript_needed"]) + return '(%s %% %s)' % (self.expand(first), + self.expand(second, first.type)) def AS(self, first, second): - raise NotImplementedError("This must yet be replaced with javascript in order to accomplish this. Sorry") + raise NotImplementedError(self.error_messages["javascript_needed"]) return '%s AS %s' % (self.expand(first), second) - #We could implement an option that simulates a full featured SQL database. But I think the option should be set explicit or implemented as another library. + # We could implement an option that simulates a full featured SQL + # database. But I think the option should be set explicit or + # implemented as another library. def ON(self, first, second): - raise NotImplementedError("This is not possible in NoSQL, but can be simulated with a wrapper.") + raise NotImplementedError("This is not possible in NoSQL" + + " but can be simulated with a wrapper.") return '%s ON %s' % (self.expand(first), self.expand(second)) - # # BLOW ARE TWO IMPLEMENTATIONS OF THE SAME FUNCITONS # WHICH ONE IS BEST? - # def COMMA(self, first, second): return '%s, %s' % (self.expand(first), self.expand(second)) def LIKE(self, first, second): #escaping regex operators? - return {self.expand(first) : ('%s' % self.expand(second, 'string').replace('%','/'))} + return {self.expand(first): ('%s' % \ + self.expand(second, 'string').replace('%','/'))} def STARTSWITH(self, first, second): #escaping regex operators? - return {self.expand(first) : ('/^%s/' % self.expand(second, 'string'))} + return {self.expand(first): ('/^%s/' % \ + self.expand(second, 'string'))} def ENDSWITH(self, first, second): #escaping regex operators? - return {self.expand(first) : ('/%s^/' % self.expand(second, 'string'))} + return {self.expand(first): ('/%s^/' % \ + self.expand(second, 'string'))} def CONTAINS(self, first, second): - #There is a technical difference, but mongodb doesn't support that, but the result will be the same - return {self.expand(first) : ('/%s/' % self.expand(second, 'string'))} + #There is a technical difference, but mongodb doesn't support + # that, but the result will be the same + return {self.expand(first) : ('/%s/' % \ + self.expand(second, 'string'))} def LIKE(self, first, second): import re - return {self.expand(first) : {'$regex' : re.escape(self.expand(second, 'string')).replace('%','.*')}} + return {self.expand(first): {'$regex': \ + re.escape(self.expand(second, + 'string')).replace('%','.*')}} #TODO verify full compatibilty with official SQL Like operator def STARTSWITH(self, first, second): #TODO Solve almost the same problem as with endswith import re - return {self.expand(first) : {'$regex' : '^' + re.escape(self.expand(second, 'string'))}} + return {self.expand(first): {'$regex' : '^' + + re.escape(self.expand(second, + 'string'))}} #TODO verify full compatibilty with official SQL Like operator def ENDSWITH(self, first, second): #escaping regex operators? - #TODO if searched for a name like zsa_corbitt and the function is endswith('a') then this is also returned. Aldo it end with a t + #TODO if searched for a name like zsa_corbitt and the function + # is endswith('a') then this is also returned. + # Aldo it end with a t import re - return {self.expand(first) : {'$regex' : re.escape(self.expand(second, 'string')) + '$'}} + return {self.expand(first): {'$regex': \ + re.escape(self.expand(second, 'string')) + '$'}} #TODO verify full compatibilty with official oracle contains operator def CONTAINS(self, first, second): - #There is a technical difference, but mongodb doesn't support that, but the result will be the same + #There is a technical difference, but mongodb doesn't support + # that, but the result will be the same #TODO contains operators need to be transformed to Regex - return {self.expand(first) : {' $regex' : ".*" + re.escape(self.expand(second, 'string')) + ".*"}} + return {self.expand(first) : {' $regex': \ + ".*" + re.escape(self.expand(second, 'string')) + ".*"}} - # - # END REDUNDANCY - # class IMAPAdapter(NoSQLAdapter): drivers = ('imaplib',) @@ -5814,17 +5944,21 @@ class IMAPAdapter(NoSQLAdapter): def create_table(self, *args, **kwargs): # not implemented - LOGGER.debug("Create table feature is not implemented for %s" % type(self)) + # but required by DAL + pass - def _select(self,query,fields,attributes): - """ Search and Fetch records and return web2py - rows + def _select(self, query, fields, attributes): + if use_common_filters(query): + query = self.common_filter(query, [self.get_query_mailbox(query),]) + return str(query) + + def select(self,query,fields,attributes): + """ Search and Fetch records and return web2py rows """ - + # move this statement elsewhere (upper-level) if use_common_filters(query): query = self.common_filter(query, [self.get_query_mailbox(query),]) - # move this statement elsewhere (upper-level) import email import email.header decode_header = email.header.decode_header @@ -5832,68 +5966,62 @@ class IMAPAdapter(NoSQLAdapter): # convert results to a dictionary tablename = None fetch_results = list() - if isinstance(query, (Expression, Query)): + if isinstance(query, Query): tablename = self.get_table(query) mailbox = self.connection.mailbox_names.get(tablename, None) - if isinstance(query, Expression): - pass - elif isinstance(query, Query): - if mailbox is not None: - # select with readonly - selected = self.connection.select(mailbox, True) - self.mailbox_size = int(selected[1][0]) - search_query = "(%s)" % str(query).strip() - search_result = self.connection.uid("search", None, search_query) - # Normal IMAP response OK is assumed (change this) - if search_result[0] == "OK": - # For "light" remote server responses just get the first - # ten records (change for non-experimental implementation) - # However, light responses are not guaranteed with this - # approach, just fewer messages. - # TODO: change limitby single to 2-tuple argument - limitby = attributes.get('limitby', None) - messages_set = search_result[1][0].split() - # descending order - messages_set.reverse() - if limitby is not None: - # TODO: asc/desc attributes - messages_set = messages_set[int(limitby[0]):int(limitby[1])] - # Partial fetches are not used since the email - # library does not seem to support it (it converts - # partial messages to mangled message instances) - imap_fields = "(RFC822)" - if len(messages_set) > 0: - # create fetch results object list - # fetch each remote message and store it in memmory - # (change to multi-fetch command syntax for faster - # transactions) - for uid in messages_set: - # fetch the RFC822 message body - typ, data = self.connection.uid("fetch", uid, imap_fields) - if typ == "OK": - fr = {"message": int(data[0][0].split()[0]), - "uid": int(uid), - "email": email.message_from_string(data[0][1]), - "raw_message": data[0][1] - } - fr["multipart"] = fr["email"].is_multipart() - # fetch flags for the message - ftyp, fdata = self.connection.uid("fetch", uid, "(FLAGS)") - if ftyp == "OK": - fr["flags"] = self.driver.ParseFlags(fdata[0]) - fetch_results.append(fr) - else: - # error retrieving the flags for this message - pass + if mailbox is not None: + # select with readonly + selected = self.connection.select(mailbox, True) + self.mailbox_size = int(selected[1][0]) + search_query = "(%s)" % str(query).strip() + search_result = self.connection.uid("search", None, search_query) + # Normal IMAP response OK is assumed (change this) + if search_result[0] == "OK": + # For "light" remote server responses just get the first + # ten records (change for non-experimental implementation) + # However, light responses are not guaranteed with this + # approach, just fewer messages. + # TODO: change limitby single to 2-tuple argument + limitby = attributes.get('limitby', None) + messages_set = search_result[1][0].split() + # descending order + messages_set.reverse() + if limitby is not None: + # TODO: asc/desc attributes + messages_set = messages_set[int(limitby[0]):int(limitby[1])] + # Partial fetches are not used since the email + # library does not seem to support it (it converts + # partial messages to mangled message instances) + imap_fields = "(RFC822)" + if len(messages_set) > 0: + # create fetch results object list + # fetch each remote message and store it in memmory + # (change to multi-fetch command syntax for faster + # transactions) + for uid in messages_set: + # fetch the RFC822 message body + typ, data = self.connection.uid("fetch", uid, imap_fields) + if typ == "OK": + fr = {"message": int(data[0][0].split()[0]), + "uid": int(uid), + "email": email.message_from_string(data[0][1]), + "raw_message": data[0][1]} + fr["multipart"] = fr["email"].is_multipart() + # fetch flags for the message + ftyp, fdata = self.connection.uid("fetch", uid, "(FLAGS)") + if ftyp == "OK": + fr["flags"] = self.driver.ParseFlags(fdata[0]) + fetch_results.append(fr) else: - # error retrieving the message body + # error retrieving the flags for this message pass - - elif isinstance(query, basestring): - # not implemented - pass + else: + # error retrieving the message body + pass + elif isinstance(query, (Expression, basestring)): + raise NotImplementedError() else: - pass + raise TypeError("Unexpected query type") imapqry_dict = {} imapfields_dict = {} @@ -5981,6 +6109,7 @@ class IMAPAdapter(NoSQLAdapter): item_dict["%s.answered" % tablename] = "\\Answered" in flags if "%s.mime" % tablename in fieldnames: item_dict["%s.mime" % tablename] = message.get_content_type() + # Here goes the whole RFC822 body as an email instance # for controller side custom processing # The message is stored as a raw string @@ -5988,6 +6117,7 @@ class IMAPAdapter(NoSQLAdapter): # returns a Message object for enhanced object processing if "%s.email" % tablename in fieldnames: item_dict["%s.email" % tablename] = self.encode_text(raw_message, charset) + # Size measure as suggested in a Velocity Reviews post # by Tim Williams: "how to get size of email attachment" # Note: len() and server RFC822.SIZE reports doesn't match @@ -6004,11 +6134,9 @@ class IMAPAdapter(NoSQLAdapter): if "%s.size" % tablename in fieldnames: if part is not None: size += len(str(part)) - item_dict["%s.content" % tablename] = bar_encode(content) item_dict["%s.attachments" % tablename] = bar_encode(attachments) item_dict["%s.size" % tablename] = size - imapqry_list.append(item_dict) # extra object mapping for the sake of rows object @@ -6019,23 +6147,18 @@ class IMAPAdapter(NoSQLAdapter): imapqry_array_item.append(item_dict[fieldname]) imapqry_array.append(imapqry_array_item) - return tablename, imapqry_array, fieldnames - - def select(self,query,fields,attributes): - tablename, imapqry_array , fieldnames = self._select(query,fields,attributes) # parse result and return a rows object colnames = fieldnames processor = attributes.get('processor',self.parse) return processor(imapqry_array, fields, colnames) - def update(self, tablename, query, fields): + def _update(self, tablename, query, fields, commit=False): + # TODO: the adapter should implement an .expand method + commands = list() if use_common_filters(query): query = self.common_filter(query, [tablename,]) - mark = [] unmark = [] - rowcount = 0 - query = str(query) if query: for item in fields: field = item[0] @@ -6048,26 +6171,33 @@ class IMAPAdapter(NoSQLAdapter): mark.append(flag) else: unmark.append(flag) - result, data = self.connection.select( self.connection.mailbox_names[tablename]) string_query = "(%s)" % query result, data = self.connection.search(None, string_query) store_list = [item.strip() for item in data[0].split() if item.strip().isdigit()] - # change marked flags + # build commands for marked flags for number in store_list: result = None if len(mark) > 0: - result, data = self.connection.store( - number, "+FLAGS", "(%s)" % " ".join(mark)) + commands.append((number, "+FLAGS", "(%s)" % " ".join(mark))) if len(unmark) > 0: - result, data = self.connection.store( - number, "-FLAGS", "(%s)" % " ".join(unmark)) - if result == "OK": - rowcount += 1 + commands.append((number, "-FLAGS", "(%s)" % " ".join(unmark))) + return commands + + def update(self, tablename, query, fields): + rowcount = 0 + commands = self._update(tablename, query, fields) + for command in commands: + result, data = self.connection.store(*command) + if result == "OK": + rowcount += 1 return rowcount + def _count(self, query, distinct=None): + raise NotImplementedError() + def count(self,query,distinct=None): counter = 0 tablename = self.get_query_mailbox(query) @@ -6343,12 +6473,10 @@ def sqlhtml_validators(field): return r._format(row) else: return id - if field_type == 'string': - requires.append(validators.IS_LENGTH(field_length)) - elif field_type == 'text': - requires.append(validators.IS_LENGTH(field_length)) - elif field_type == 'password': + if field_type in (('string', 'text', 'password')): requires.append(validators.IS_LENGTH(field_length)) + elif field_type == 'json': + requires.append(validators.IS_EMPTY_OR(validators.IS_JSON())) elif field_type == 'double' or field_type == 'float': requires.append(validators.IS_FLOAT_IN_RANGE(-1e100, 1e100)) elif field_type in ('integer','bigint'): @@ -6455,6 +6583,8 @@ class Row(object): def __setitem__(self, key, value): setattr(self, str(key), value) + __copy__ = lambda self: Row(self) + __call__ = __getitem__ def get(self,key,default=None): @@ -6628,7 +6758,7 @@ def smart_query(fields,text): value = constants[item[1:]] else: value = item - if field.type in ('text','string'): + if field.type in ('text', 'string', 'json'): if op == '=': op = 'like' if op == '=': new_query = field==value elif op == '<': new_query = field :db_codec: string encoding of the database (default: 'UTF-8') :check_reserved: list of adapters to check tablenames and column names - against sql reserved keywords. (Default None) + against sql/nosql reserved keywords. (Default None) * 'common' List of sql keywords that are common to all database types such as "SELECT, INSERT". (recommended) @@ -6831,6 +6961,7 @@ class DAL(object): self._migrated = [] self._LAZY_TABLES = {} self._lazy_tables = lazy_tables + self._tables = SQLCallableList() if not str(attempts).isdigit() or attempts < 0: attempts = 5 if uri: @@ -6883,7 +7014,6 @@ class DAL(object): migrate = fake_migrate = False adapter = self._adapter self._uri_hash = hashlib_md5(adapter.uri).hexdigest() - self._tables = SQLCallableList() self.check_reserved = check_reserved if self.check_reserved: from reserved_sql_keywords import ADAPTERS as RSK @@ -6932,7 +7062,7 @@ class DAL(object): for backend in self.check_reserved: if name.upper() in self.RSK[backend]: raise SyntaxError( - 'invalid table/column name "%s" is a "%s" reserved SQL keyword' % (name, backend.upper())) + 'invalid table/column name "%s" is a "%s" reserved SQL/NOSQL keyword' % (name, backend.upper())) def parse_as_rest(self,patterns,args,vars,queries=None,nested_select=True): """ @@ -7023,7 +7153,7 @@ def index(): patterns += auto_table(table,base=tag,depth=depth-1) return patterns - if patterns==DEFAULT: + if patterns == 'auto': patterns=[] for table in db.tables: if not table.startswith('auth_'): @@ -7045,9 +7175,15 @@ def index(): return Row({'status':200,'pattern':'list', 'error':None,'response':patterns}) for pattern in patterns: + if isinstance(pattern,tuple): + pattern, basequery = pattern + else: + basequery = None otable=table=None if not isinstance(queries,dict): dbset=db(queries) + if basequery is not None: + dbset = dbset(basequery) i=0 tags = pattern[1:].split('/') if len(tags)!=len(args): @@ -7094,6 +7230,8 @@ def index(): raise RuntimeError("invalid pattern: %s" % pattern) if not otable and isinstance(queries,dict): dbset = db(queries[table]) + if basequery is not None: + dbset = dbset(basequery) dbset=dbset(query) else: raise RuntimeError("missing relation in pattern: %s" % pattern) @@ -7449,8 +7587,8 @@ class SQLALL(object): def __str__(self): return ', '.join([str(field) for field in self._table]) - -class Reference(int): +# class Reference(int): +class Reference(long): def __allocate(self): if not self._record: @@ -7628,7 +7766,7 @@ class Table(object): field.tablename = field._tablename = tablename field.table = field._table = self field.db = field._db = db - if db and not field.type in ('text','blob') and \ + if db and not field.type in ('text', 'blob', 'json') and \ db._adapter.maxcharlength < field.length: field.length = db._adapter.maxcharlength self.ALL = SQLALL(self) @@ -7914,13 +8052,24 @@ class Table(object): raise RuntimeError("Unable to handle upload") fields[field.name] = new_name + def _defaults(self, fields): + "If there are no fields/values specified, return table defaults" + if not fields: + fields = {} + for field in self: + if field.type != "id": + fields[field.name] = field.default + return fields + def _insert(self, **fields): - return self._db._adapter._insert(self,self._listify(fields)) + fields = self._default(fields) + return self._db._adapter._insert(self, self._listify(fields)) def insert(self, **fields): + fields = self._defaults(fields) self._attempt_upload(fields) if any(f(fields) for f in self._before_insert): return 0 - ret = self._db._adapter.insert(self,self._listify(fields)) + ret = self._db._adapter.insert(self, self._listify(fields)) if ret and self._after_insert: fields = Row(fields) [f(fields,ret) for f in self._after_insert] @@ -8326,13 +8475,13 @@ class Expression(object): def startswith(self, value): db = self.db - if not self.type in ('string', 'text'): + if not self.type in ('string', 'text', 'json'): raise SyntaxError("startswith used with incompatible field type") return Query(db, db._adapter.STARTSWITH, self, value) def endswith(self, value): db = self.db - if not self.type in ('string', 'text'): + if not self.type in ('string', 'text', 'json'): raise SyntaxError("endswith used with incompatible field type") return Query(db, db._adapter.ENDSWITH, self, value) @@ -8344,7 +8493,7 @@ class Expression(object): return self.contains('') else: return reduce(all and AND or OR,subqueries) - if not self.type in ('string', 'text') and not self.type.startswith('list:'): + if not self.type in ('string', 'text', 'json') and not self.type.startswith('list:'): raise SyntaxError("contains used with incompatible field type") return Query(db, db._adapter.CONTAINS, self, value) @@ -8352,8 +8501,8 @@ class Expression(object): db = self.db return Expression(db, db._adapter.AS, self, alias, self.type) - # GIS functions - + # GIS expressions + def st_asgeojson(self, precision=15, options=0, version=1): return Expression(self.db, self.db._adapter.ST_ASGEOJSON, self, dict(precision=precision, options=options, @@ -8363,18 +8512,28 @@ class Expression(object): db = self.db return Expression(db, db._adapter.ST_ASTEXT, self, type='string') - def st_contained(self, value): + def st_x(self): db = self.db - return Query(db, db._adapter.ST_CONTAINS, value, self) + return Expression(db, db._adapter.ST_X, self, type='string') + + def st_y(self): + db = self.db + return Expression(db, db._adapter.ST_Y, self, type='string') + + def st_distance(self, other): + db = self.db + return Expression(db,db._adapter.ST_DISTANCE,self,other, 'double') + + def st_simplify(self, value): + db = self.db + return Expression(db, db._adapter.ST_SIMPLIFY, self, value, self.type) + + # GIS queries def st_contains(self, value): db = self.db return Query(db, db._adapter.ST_CONTAINS, self, value) - def st_distance(self, other): - db = self.db - return Expression(db,db._adapter.ST_DISTANCE,self,other,self.type) - def st_equals(self, value): db = self.db return Query(db, db._adapter.ST_EQUALS, self, value) @@ -8387,10 +8546,6 @@ class Expression(object): db = self.db return Query(db, db._adapter.ST_OVERLAPS, self, value) - def st_simplify(self, value): - db = self.db - return Expression(db, db._adapter.ST_SIMPLIFY, self, value) - def st_touches(self, value): db = self.db return Query(db, db._adapter.ST_TOUCHES, self, value) @@ -9441,7 +9596,9 @@ class Rows(object): items = [[inner_loop(record, col) for col in self.colnames] for record in self] if have_serializers: - return serializers.json(items,default=default or serializers.custom_json) + return serializers.json(items, + default=default or + serializers.custom_json) else: try: import json as simplejson @@ -9467,6 +9624,7 @@ def test_all(): Field('blobf', 'blob'),\ Field('integerf', 'integer', unique=True),\ Field('doublef', 'double', unique=True,notnull=True),\ + Field('jsonf', 'json'),\ Field('datef', 'date', default=datetime.date.today()),\ Field('timef', 'time'),\ Field('datetimef', 'datetime'),\ @@ -9476,6 +9634,7 @@ def test_all(): >>> db.users.insert(stringf='a', booleanf=True, passwordf='p', blobf='0A',\ uploadf=None, integerf=5, doublef=3.14,\ + jsonf={"j": True},\ datef=datetime.date(2001, 1, 1),\ timef=datetime.time(12, 30, 15),\ datetimef=datetime.datetime(2002, 2, 2, 12, 30, 15)) @@ -9675,7 +9834,7 @@ DAL.Table = Table # was necessary in gluon/globals.py session.connect # Geodal utils ################################################################################ -def geoPoint(*line): +def geoPoint(x,y): return "POINT (%f %f)" % (x,y) def geoLine(*line): diff --git a/gluon/html.py b/gluon/html.py index 09783e78..265f5500 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -991,7 +991,7 @@ class DIV(XmlComponent): >>> a=FORM( INPUT(_type='text'), SELECT(range(1)), TEXTAREA() ) >>> for c in a.elements('input, select, textarea'): c['_disabled'] = 'disabled' >>> a.xml() - '
' + '
' Elements that are matched can also be replaced or removed by specifying a "replace" argument (note, a list of the original matching elements @@ -1478,9 +1478,9 @@ class A(DIV): (self['component'], self['target'] or '', d) self['_href'] = self['_href'] or '#null' elif self['callback']: - returnfalse = "var e = arguments[0] || window.event; e.cancelBubble=true; if (e.stopPropagation) e.stopPropagation();" + returnfalse = "var e = arguments[0] || window.event; e.cancelBubble=true; if (e.stopPropagation) {e.stopPropagation(); e.stopImmediatePropagation(); e.preventDefault();}" if d: - self['_onclick'] = "if(confirm(w2p_ajax_confirm_message||'Are you sure you want o delete this object?')){ajax('%s',[],'%s');%s};%s" % \ + self['_onclick'] = "if(confirm(w2p_ajax_confirm_message||'Are you sure you want to delete this object?')){ajax('%s',[],'%s');%s};%s" % \ (self['callback'], self['target'] or '', d, returnfalse) else: self['_onclick'] = "ajax('%s',[],'%s');%sreturn false" % \ @@ -1932,7 +1932,7 @@ class FORM(DIV): >>> from validators import IS_NOT_EMPTY >>> form=FORM(INPUT(_name=\"test\", requires=IS_NOT_EMPTY())) >>> form.xml() - '
' + '
' a FORM is container for INPUT, TEXTAREA, SELECT and other helpers @@ -2033,7 +2033,7 @@ class FORM(DIV): def _postprocessing(self): if not '_action' in self.attributes: - self['_action'] = '' + self['_action'] = '#' if not '_method' in self.attributes: self['_method'] = 'post' if not '_enctype' in self.attributes: @@ -2415,25 +2415,25 @@ def test():
abc
def
123
>>> form=FORM(INPUT(_type='text', _name='myvar', requires=IS_EXPR('int(value)<10'))) >>> print form.xml() -
+
>>> print form.accepts({'myvar':'34'}, formname=None) False >>> print form.xml() -
invalid expression
+
invalid expression
>>> print form.accepts({'myvar':'4'}, formname=None, keepvalues=True) True >>> print form.xml() -
+
>>> form=FORM(SELECT('cat', 'dog', _name='myvar')) >>> print form.accepts({'myvar':'dog'}, formname=None, keepvalues=True) True >>> print form.xml() -
+
>>> form=FORM(INPUT(_type='text', _name='myvar', requires=IS_MATCH('^\w+$', 'only alphanumeric!'))) >>> print form.accepts({'myvar':'as df'}, formname=None) False >>> print form.xml() -
only alphanumeric!
+
only alphanumeric!
>>> session={} >>> form=FORM(INPUT(value=\"Hello World\", _name=\"var\", requires=IS_MATCH('^\w+$'))) >>> if form.accepts({}, session,formname=None): print 'passed' diff --git a/gluon/reserved_sql_keywords.py b/gluon/reserved_sql_keywords.py index e5b0fd14..9ff314ae 100644 --- a/gluon/reserved_sql_keywords.py +++ b/gluon/reserved_sql_keywords.py @@ -1686,6 +1686,9 @@ SQLITE = set(( 'WHERE', )) + +MONGODB_NONRESERVED = set(('SAFE',)) + # remove from here when you add a list. JDBCSQLITE = SQLITE DB2 = INFORMIX = INGRES = JDBCPOSTGRESQL = COMMON @@ -1708,6 +1711,7 @@ ADAPTERS = { 'jdbc:sqlite': JDBCSQLITE, 'jdbc:postgres': JDBCPOSTGRESQL, 'common': COMMON, + 'mongodb_nonreserved': MONGODB_NONRESERVED } ADAPTERS['all'] = reduce(lambda a, b: a.union(b), ( diff --git a/gluon/serializers.py b/gluon/serializers.py index 3cdb91fc..e29061a1 100644 --- a/gluon/serializers.py +++ b/gluon/serializers.py @@ -20,6 +20,10 @@ except ImportError: import contrib.simplejson as json_parser # fallback to pure-Python module +def loads_json(o): + # deserialize a json string + return json_parser.loads(o) + def custom_json(o): if hasattr(o, 'custom_json') and callable(o.custom_json): return o.custom_json() diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index a5827e96..973185c4 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -30,6 +30,7 @@ from utils import md5_hash from validators import IS_EMPTY_OR, IS_NOT_EMPTY, IS_LIST_OF, IS_DATE, \ IS_DATETIME, IS_INT_IN_RANGE, IS_FLOAT_IN_RANGE, IS_STRONG +import serializers import datetime import urllib import re @@ -39,6 +40,9 @@ import inspect import settings is_gae = settings.global_settings.web2py_runtime_gae + + + table_field = re.compile('[\w_]+\.[\w_]+') widget_class = re.compile('^\w*') @@ -164,11 +168,9 @@ class TimeWidget(StringWidget): class DateWidget(StringWidget): _class = 'date' - class DatetimeWidget(StringWidget): _class = 'datetime' - class TextWidget(FormWidget): _class = 'text' @@ -184,6 +186,22 @@ class TextWidget(FormWidget): attr = cls._attributes(field, default, **attributes) return TEXTAREA(**attr) +class JSONWidget(FormWidget): + _class = 'json' + + @classmethod + def widget(cls, field, value, **attributes): + """ + generates a TEXTAREA for JSON notation. + + see also: :meth:`FormWidget.widget` + """ + if not isinstance(value, basestring): + if value is not None: + value = serializers.json(value) + default = dict(value=value) + attr = cls._attributes(field, default, **attributes) + return TEXTAREA(**attr) class BooleanWidget(FormWidget): _class = 'boolean' @@ -235,7 +253,6 @@ class OptionsWidget(FormWidget): raise SyntaxError( 'widget cannot determine options of %s' % field) opts = [OPTION(v, _value=k) for (k, v) in options] - return SELECT(*opts, **attr) @@ -523,20 +540,25 @@ class UploadWidget(FormWidget): requires = attr["requires"] if requires == [] or isinstance(requires, IS_EMPTY_OR): - inp = DIV(inp, '[', - A(current.T( - UploadWidget.GENERIC_DESCRIPTION), _href=url), - '|', - INPUT(_type='checkbox', - _name=field.name + cls.ID_DELETE_SUFFIX, - _id=field.name + cls.ID_DELETE_SUFFIX), - LABEL(current.T(cls.DELETE_FILE), - _for=field.name + cls.ID_DELETE_SUFFIX), - ']', br, image) + inp = DIV(inp, + SPAN('[', + A(current.T( + UploadWidget.GENERIC_DESCRIPTION), _href=url), + '|', + INPUT(_type='checkbox', + _name=field.name + cls.ID_DELETE_SUFFIX, + _id=field.name + cls.ID_DELETE_SUFFIX), + LABEL(current.T(cls.DELETE_FILE), + _for=field.name + cls.ID_DELETE_SUFFIX, + _style='display:inline'), + ']', _style='white-space:nowrap'), + br, image) else: - inp = DIV(inp, '[', - A(cls.GENERIC_DESCRIPTION, _href=url), - ']', br, image) + inp = DIV(inp, + SPAN('[', + A(cls.GENERIC_DESCRIPTION, _href=url), + ']', _style='white-space:nowrap'), + br, image) return inp @classmethod @@ -716,7 +738,7 @@ def formstyle_table2cols(form, fields): def formstyle_divs(form, fields): ''' divs only ''' - table = TAG['']() + table = FIELDSET() for id, label, controls, help in fields: _help = DIV(help, _class='w2p_fc') _controls = DIV(controls, _class='w2p_fw') @@ -748,7 +770,7 @@ def formstyle_ul(form, fields): def formstyle_bootstrap(form, fields): ''' bootstrap format form layout ''' - form['_class'] = 'form-horizontal' + form.add_class('form-horizontal') parent = FIELDSET() for id, label, controls, help in fields: # wrappers @@ -782,12 +804,12 @@ def formstyle_bootstrap(form, fields): if _submit: # submit button has unwrapped label and controls, different class - parent.append(DIV(label, controls, _class='form-actions')) + parent.append(DIV(label, controls, _class='form-actions', _id=id)) # unflag submit (possible side effect) _submit = False else: # unwrapped label - parent.append(DIV(label, _controls, _class='control-group')) + parent.append(DIV(label, _controls, _class='control-group', _id=id)) return parent @@ -843,6 +865,7 @@ class SQLFORM(FORM): widgets = Storage(dict( string=StringWidget, text=TextWidget, + json=JSONWidget, password=PasswordWidget, integer=IntegerWidget, double=DoubleWidget, @@ -1132,7 +1155,7 @@ class SQLFORM(FORM): # # when deletable, add delete? checkbox - self.custom.deletable = '' + self.custom.delete = self.custom.deletable = '' if record and deletable: #add secondary css class for cascade delete warning css = 'delete' @@ -1154,7 +1177,8 @@ class SQLFORM(FORM): _id=self.FIELDKEY_DELETE_RECORD + SQLFORM.ID_LABEL_SUFFIX), widget, col3.get(self.FIELDKEY_DELETE_RECORD, ''))) - self.custom.deletable = widget + self.custom.delete = self.custom.deletable = widget + # when writable, add submit button self.custom.submit = '' @@ -1807,28 +1831,21 @@ class SQLFORM(FORM): buttonurl=url(args=[]), callback=None, delete=None, trap=True): if showbuttontext: - if callback: - return A(SPAN(_class=ui.get(buttonclass)), - SPAN(T(buttontext), _title=buttontext, - _class=ui.get('buttontext')), - callback=callback, delete=delete, - _class=trap_class(ui.get('button'), trap)) - else: - return A(SPAN(_class=ui.get(buttonclass)), - SPAN(T(buttontext), _title=buttontext, - _class=ui.get('buttontext')), - _href=buttonurl, - _class=trap_class(ui.get('button'), trap)) + return A(SPAN(_class=ui.get(buttonclass)), + SPAN(T(buttontext), _title=buttontext, + _class=ui.get('buttontext')), + _href=buttonurl, + callback=callback, + delete=delete, + _class=trap_class(ui.get('button'), trap)) else: - if callback: - return A(SPAN(_class=ui.get(buttonclass)), - callback=callback, delete=delete, - _title=buttontext, - _class=trap_class(ui.get('buttontext'), trap)) - else: - return A(SPAN(_class=ui.get(buttonclass)), - _href=buttonurl, _title=buttontext, - _class=trap_class(ui.get('buttontext'), trap)) + return A(SPAN(_class=ui.get(buttonclass)), + _href=buttonurl, + callback=callback, + delete=delete, + _title=buttontext, + _class=trap_class(ui.get('buttontext'), trap)) + dbset = db(query) tablenames = db._adapter.tables(dbset.query) if left is not None: @@ -1948,8 +1965,8 @@ class SQLFORM(FORM): table = db[request.args[-2]] if ondelete: ondelete(table, request.args[-1]) - ret = db(table[table._id.name] == request.args[-1]).delete() - return ret + db(table[table._id.name] == request.args[-1]).delete() + redirect(referrer) exportManager = dict( csv_with_hidden_cols=(ExporterCSV, 'CSV (hidden cols)'), @@ -2267,7 +2284,7 @@ class SQLFORM(FORM): elif not isinstance(value, DIV): value = field.formatter(value) trcols.append(TD(value)) - row_buttons = TD(_class='row_buttons') + row_buttons = TD(_class='row_buttons',_nowrap=True) if links and links_in_grid: toadd = [] for link in links: @@ -2293,6 +2310,7 @@ class SQLFORM(FORM): if deletable and (not callable(deletable) or deletable(row)): row_buttons.append(gridbutton( 'buttondelete', 'Delete', + url(args=['delete', tablename, id]), callback=url(args=['delete', tablename, id]), delete='tr')) if buttons_placement in ['right', 'both']: diff --git a/gluon/tests/test_dal.py b/gluon/tests/test_dal.py index d92ac0e6..a703801d 100644 --- a/gluon/tests/test_dal.py +++ b/gluon/tests/test_dal.py @@ -31,6 +31,7 @@ ALLOWED_DATATYPES = [ 'datetime', 'upload', 'password', + 'json', ] @@ -62,14 +63,14 @@ class TestFields(unittest.TestCase): def testFieldTypes(self): - # Check that string, text, and password default length is 512 + # Check that string, and password default length is 512 for typ in ['string', 'password']: self.assert_(Field('abc', typ).length == 512, "Default length for type '%s' is not 512 or 255" % typ) # Check that upload default length is 512 self.assert_(Field('abc', 'upload').length == 512, - "Default length for type 'upload' is not 128") + "Default length for type 'upload' is not 512") # Check that Tables passed in the type creates a reference self.assert_(Field('abc', Table(None, 'temp')).type @@ -113,6 +114,10 @@ class TestFields(unittest.TestCase): self.assertEqual(db.t.insert(a=True), 1) self.assertEqual(db().select(db.t.a)[0].a, True) db.t.drop() + db.define_table('t', Field('a', 'json', default={})) + self.assertEqual(db.t.insert(a={}), 1) + self.assertEqual(db().select(db.t.a)[0].a, {}) + db.t.drop() db.define_table('t', Field('a', 'date', default=datetime.date.today())) t0 = datetime.date.today() diff --git a/gluon/tests/test_html.py b/gluon/tests/test_html.py index 336eff9b..1273f2bd 100644 --- a/gluon/tests/test_html.py +++ b/gluon/tests/test_html.py @@ -74,7 +74,7 @@ class TestBareHelpers(unittest.TestCase): def testFORM(self): self.assertEqual(FORM('<>', _a='1', _b='2').xml(), - '
<>
') + '
<>
') def testH1(self): self.assertEqual(H1('<>', _a='1', _b='2').xml(), diff --git a/gluon/tools.py b/gluon/tools.py index 4644cc61..0244fee9 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -272,7 +272,7 @@ class Mail(object): cc=None, bcc=None, reply_to=None, - sender='%(sender)s', + sender=None, encoding='utf-8', raw=False, headers={} @@ -360,9 +360,11 @@ class Mail(object): text = encode_header(text) return text + sender = sender or self.settings.sender + if not isinstance(self.settings.server, str): raise Exception('Server address not specified') - if not isinstance(self.settings.sender, str): + if not isinstance(sender, str): raise Exception('Sender address not specified') if not raw: @@ -458,11 +460,11 @@ class Mail(object): c.set_armor(1) c.signers_clear() # search for signing key for From: - for sigkey in c.op_keylist_all(self.settings.sender, 1): + for sigkey in c.op_keylist_all(sender, 1): if sigkey.can_sign: c.signers_add(sigkey) if not c.signers_enum(0): - self.error = 'No key for signing [%s]' % self.settings.sender + self.error = 'No key for signing [%s]' % sender return False c.set_passphrase_cb(lambda x, y, z: sign_passphrase) try: @@ -619,7 +621,6 @@ class Mail(object): # no cryptography process as usual payload = payload_in - sender = sender % dict(sender=self.settings.sender) payload['From'] = encoded_or_raw(sender.decode(encoding)) origTo = to[:] if to: @@ -655,16 +656,16 @@ class Mail(object): attachments = attachments and [(a.my_filename, a.my_payload) for a in attachments if not raw] if attachments: result = mail.send_mail( - sender=self.settings.sender, to=origTo, + sender=sender, to=origTo, subject=subject, body=text, html=html, attachments=attachments, **xcc) elif html and (not raw): result = mail.send_mail( - sender=self.settings.sender, to=origTo, + sender=sender, to=origTo, subject=subject, body=text, html=html, **xcc) else: result = mail.send_mail( - sender=self.settings.sender, to=origTo, + sender=sender, to=origTo, subject=subject, body=text, **xcc) else: smtp_args = self.settings.server.split(':') @@ -679,7 +680,7 @@ class Mail(object): if self.settings.login: server.login(*self.settings.login.split(':', 1)) result = server.sendmail( - self.settings.sender, to, payload.as_string()) + sender, to, payload.as_string()) server.quit() except Exception, e: logger.warn('Mail.send failure:%s' % e) @@ -881,6 +882,7 @@ class Auth(object): profile_fields=None, email_case_sensitive=True, username_case_sensitive=True, + update_fields = ['email'], ) # ## these are messages that can be customized default_messages = dict( @@ -2047,7 +2049,8 @@ class Auth(object): if not self in self.settings.login_methods: # do not store password in db form.vars[passfield] = None - user = self.get_or_create_user(form.vars) + user = self.get_or_create_user( + form.vars, self.settings.update_fields) break if not user: # alternates have failed, maybe because service inaccessible @@ -2067,7 +2070,8 @@ class Auth(object): if not self in self.settings.login_methods: # do not store password in db form.vars[passfield] = None - user = self.get_or_create_user(form.vars) + user = self.get_or_create_user( + form.vars, self.settings.update_fields) break if not user: self.log_event(self.messages.login_failed_log, @@ -2087,7 +2091,8 @@ class Auth(object): if cas_user: cas_user[passfield] = None user = self.get_or_create_user( - table_user._filter_fields(cas_user)) + table_user._filter_fields(cas_user), + self.settings.update_fields) elif hasattr(cas, 'login_form'): return cas.login_form() else: @@ -3309,14 +3314,14 @@ class Auth(object): restrict_search=False, resolve=True, extra=None, - menugroups=None): + menu_groups=None): if not hasattr(self, '_wiki'): self._wiki = Wiki(self, render=render, manage_permissions=manage_permissions, force_prefix=force_prefix, restrict_search=restrict_search, env=env, extra=extra or {}, - menugroups=menugroups) + menu_groups=menu_groups) else: self._wiki.env.update(env or {}) # if resolve is set to True, process request as wiki call @@ -4694,15 +4699,19 @@ class Expose(object): class Wiki(object): everybody = 'everybody' rows_page = 25 - - def markmin_render(self, page): - html = MARKMIN(page.body, extra=self.extra, + def markmin_base(self,body): + return MARKMIN(body, extra=self.extra, url=True, environment=self.env, autolinks=lambda link: expand_one(link, {})).xml() - html += DIV(_class='w2p_wiki_tags', - *[A(t.strip(), _href=URL(args='_search', vars=dict(q=t))) - for t in page.tags or [] if t.strip()]).xml() - return html + + def render_tags(self, tags): + return DIV( + _class='w2p_wiki_tags', + *[A(t.strip(), _href=URL(args='_search', vars=dict(q=t))) + for t in tags or [] if t.strip()]) + + def markmin_render(self, page): + return self.markmin_base(page.body) + self.render_tags(page.tags).xml() def html_render(self, page): html = page.body @@ -4712,6 +4721,7 @@ class Wiki(object): html = replace_autolinks(html, lambda link: expand_one(link, {})) # @{component:name} -> html = replace_components(html, self.env) + html = html + self.render_tags(page.tags).xml() return html @staticmethod @@ -4726,7 +4736,7 @@ class Wiki(object): def __init__(self, auth, env=None, render='markmin', manage_permissions=False, force_prefix='', - restrict_search=False, extra=None, menugroups=None): + restrict_search=False, extra=None, menu_groups=None): self.env = env or {} self.env['component'] = Wiki.component if render == 'markmin': @@ -4735,7 +4745,7 @@ class Wiki(object): render = self.html_render self.render = render self.auth = auth - self.menugroups = menugroups + self.menu_groups = menu_groups if self.auth.user: self.force_prefix = force_prefix % self.auth.user else: @@ -4853,11 +4863,11 @@ class Wiki(object): return True def can_see_menu(self): - if self.menugroups is None: + if self.menu_groups is None: return True if self.auth.user: groups = self.auth.user_groups.values() - if any(t in self.menugroups for t in groups): + if any(t in self.menu_groups for t in groups): return True return False diff --git a/gluon/validators.py b/gluon/validators.py index 6ec44656..276ed395 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -21,6 +21,16 @@ import unicodedata from cStringIO import StringIO from utils import simple_hash, web2py_uuid, DIGEST_ALG_BY_SIZE +JSONErrors = (NameError, TypeError, ValueError, AttributeError, + KeyError) +try: + import json as simplejson +except ImportError: + from gluon.contrib import simplejson + from gluon.contrib.simplejson.decoder import JSONDecodeError + JSONErrors += (JSONDecodeError,) + + __all__ = [ 'CLEANUP', 'CRYPT', @@ -53,6 +63,7 @@ __all__ = [ 'IS_UPLOAD_FILENAME', 'IS_UPPER', 'IS_URL', + 'IS_JSON', ] try: @@ -300,6 +311,30 @@ class IS_LENGTH(Validator): return (value, translate(self.error_message) % dict(min=self.minsize, max=self.maxsize)) +class IS_JSON(Validator): + """ + example:: + INPUT(_type='text', _name='name', + requires=IS_JSON(error_message="This is not a valid json input") + + >>> IS_JSON()('{"a": 100}') + ('{"a": 100}', None) + + >>> IS_JSON()('spam1234') + ('spam1234', 'invalid json') + """ + + def __init__(self, error_message='invalid json'): + self.error_message = error_message + + def __call__(self, value): + try: + simplejson.loads(value) + return (value, None) + except JSONErrors: + pass + return (value, translate(self.error_message)) + class IS_IN_SET(Validator): """ diff --git a/scripts/setup-ubuntu-12-04-redmine-unicorn-web2py-uwsgi-nginx.sh b/scripts/setup-ubuntu-12-04-redmine-unicorn-web2py-uwsgi-nginx.sh new file mode 100644 index 00000000..84a0b0f2 --- /dev/null +++ b/scripts/setup-ubuntu-12-04-redmine-unicorn-web2py-uwsgi-nginx.sh @@ -0,0 +1,464 @@ +#!/bin/bash + +# ------------------------------------------------------------------------------ +# Description : Installation and basic configuration of web2py, uWSGI, Redmine, +# Unicorn, Nginx and PostgreSQL. +# Usage : Copy the script in /home/username and run it as root, you may +# need to allow exectuion (chmod +x). Ex.: +# sudo ./setup-ubuntu-12-04-redmine-unicorn-web2py-uwsgi-nginx.sh +# File : setup-ubuntu-12-04-redmine-unicorn-web2py-uwsgi-nginx.sh +# Author : Richard V?zina +# Email : ml.richard.vezina@gmail.com +# Copyright : Richard V?zina +# Date : ven 28 d?c 2012 13:27:11 EST +# Disclaimers : This script is provided "as is", without warranty of any kind. +# Licence : CC BY-NC 2.5 CA +# ------------------------------------------------------------------------------ + +echo 'setup-ubuntu-12-04-redmine-unicorn-web2py-uwsgi-nginx.sh' +echo 'Requires Ubuntu = 12.04 (May works with 12.10 not tested) and installs Redmine + Unicorn + Web2py + uWSGI + Nginx + PostgreSQL' +# Check if user has root privileges +if [[ $EUID -ne 0 ]]; then + echo "You must run the script as root or using sudo" + exit 1 +fi + +# ------------------------------------------------------------------------------ +# We concentrate here user prompts!! +# Get Redmine Postgres Database Password +echo -e "Redmine Postgres Database Password: \c " +read REDMINEPASSWORD +# Get Web2py Admin Password +echo -e "Web2py Admin Password: \c " +read PW + +cd ~ +openssl genrsa 1024 > self_signed.key +chmod 400 self_signed.key +openssl req -new -x509 -nodes -sha1 -days 1780 -key self_signed.key > self_signed.cert +openssl x509 -noout -fingerprint -text < self_signed.cert > self_signed.info +# ------------------------------------------------------------------------------ + +apt-get update +apt-get -y upgrade +apt-get autoremove +apt-get autoclean +apt-get -y install postgresql +apt-get -y install nginx-full +apt-get -y install build-essential python-dev libxml2-dev python-pip unzip +apt-get -y install ruby1.9.3 # Ref.: http://askubuntu.com/questions/137485/rails-3-not-using-rvm +apt-get -y install libpq-dev # Required for gem1.9.3 install pg Ref.: http://stackoverflow.com/questions/6040583/unable-to-install-pg-gem-on-ubuntu-cant-find-the-libpq-fe-h-header + +gem1.9.3 install rails --no-rdoc --no-ri # For testing (faster) --no-rdoc --no-ri +gem1.9.3 install unicorn --no-rdoc --no-ri # For testing (faster) --no-rdoc --no-ri +gem1.9.3 install pg --no-rdoc --no-ri # For testing (faster) --no-rdoc --no-ri +cd /opt +wget http://rubyforge.org/frs/download.php/76627/redmine-2.2.0.tar.gz +wget http://rubyforge.org/frs/download.php/76628/redmine-2.2.0.tar.gz.md5 +md5sum --check redmine-2.2.0.tar.gz.md5 > redmine_md5_checked_successfully +if [ -f redmine_md5_checked_successfully ] +then + tar xvfz redmine-2.2.0.tar.gz + rm redmine_md5_checked_successfully +else + echo "Redmine md5 check sum failed..." + exit 1 +fi +cd redmine-2.2.0 +bundle install --without development test rmagick sqlite mysql +mkdir /var/www +ln -s /opt/redmine-2.2.0/public /var/www/redmine +chown -R www-data.www-data /var/www +chown -R www-data.www-data /opt/redmine-2.2.0/public +# To avoid prompt during execution of the script use psql instead of createuser +#echo "Enter a postgres redmine user password twice:" +#createuser -P -S -D -R -l -e redmine +# createuser switch: -P --pwprompt -S --no-superuser -D --no-createdb -R --no-createrole -l --login -e --echo +sudo -u postgres psql -c "CREATE ROLE redmine LOGIN; ALTER ROLE redmine WITH ENCRYPTED PASSWORD '$REDMINEPASSWORD';" +# createdb wouldn't work without having root password +#createdb -U postgres -w -E UTF8 -O redmine -e redmine +# createdb switch: -U username --username=username -w --no-password -E Encoding -O owner --owner=owner -e --echo +sudo -u postgres psql -c "CREATE DATABASE redmine WITH ENCODING='UTF8' OWNER=redmine;" +cd /opt/redmine-2.2.0/config +# Here we change related to an issue with new rails version as far as I understand +# Ref1.: http://www.redmine.org/projects/redmine/wiki/HowTo_Install_Redmine_in_a_sub-URI # Preferred solution used +# Ref2.: http://www.redmine.org/issues/12102 # JS and CSS was not working until I add this line 'RedmineApp::Application.routes.default_scope = { :path => "/redmine", :shallow_path => "/redmine" }' before 'RedmineApp::Application.initialize!' +cp environment.rb environment.rb_original # Backup default environment.rb +sed '/RedmineApp::Application.initialize!/c \RedmineApp::Application.routes.default_scope = { :path => "/redmine", :shallow_path => "/redmine" }\nRedmineApp::Application.initialize!\nRedmine::Utils::relative_url_root = "/redmine"' environment.rb_original > environment.rb +# Now we configure Redmine database access +#nano database.yml +# paste : +echo 'production: + adapter: postgresql + database: redmine + host: localhost + username: redmine + password: "'$REDMINEPASSWORD'" + encoding: utf8' > database.yml +rake generate_secret_token +RAILS_ENV=production rake db:migrate +RAILS_ENV=production rake redmine:load_default_data +mkdir /opt/redmine-2.2.0/tmp/pids +#mkdir /opt/redmine-2.2.0/log # if not there +cd /opt/redmine-2.2.0/config +# Create Unicorn specific Redmine config in /opt/redmine-2.2.0/config/unicorn.rb +echo '#unicorn.rb Starts here +worker_processes 1 +working_directory "/opt/redmine-2.2.0" # needs to be the correct directory for redmine + +# This loads the application in the master process before forking +# worker processes +# Read more about it here: +# http://unicorn.bogomips.org/Unicorn/Configurator.html +preload_app true +timeout 45 + +# This is where we specify the socket. +# We will point the upstream Nginx module to this socket later on +listen "/tmp/unicorn_rails.socket", :backlog => 64 #directory structure needs to be created. +pid "/opt/redmine-2.2.0/tmp/pids/unicorn_rails.pid" # make sure this points to a valid directory. Make sure it is named the same as the real process name in order to allow init.d script start-stop-daemon command to kill unicorn process properly + +# Set the path of the log files inside the log folder of the testapp +stderr_path "/opt/redmine-2.2.0/log/unicorn_rails.stderr.log" +stdout_path "/opt/redmine-2.2.0/log/unicorn_rails.stdout.log" + +before_fork do |server, worker| +# This option works in together with preload_app true setting +# What is does is prevent the master process from holding +# the database connection +defined?(ActiveRecord::Base) and +ActiveRecord::Base.connection.disconnect! +end + +after_fork do |server, worker| +# Here we are establishing the connection after forking worker +# processes +defined?(ActiveRecord::Base) and +ActiveRecord::Base.establish_connection +# change below if your redmine instance is running differently +worker.user('\''www-data'\'', '\''www-data'\'') if Process.euid == 0 +end +#unicorn.rb Ends here' > unicorn.rb +chown www-data:www-data unicorn.rb +chown -R www-data:www-data /opt/redmine-2.2.0/tmp +mkdir /etc/unicorn +# Set some config for Unicorn in /etc/unicorn/redmine +echo 'RAILS_ROOT=/opt/redmine-2 +RAILS_ENV=production' > /etc/unicorn/redmine +# Create a Unicorn Redmine start script in /etc/init.d/redmine +echo '#! /bin/sh +### BEGIN INIT INFO +# Provides: redmine +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: redmine initscript +# Description: This script startup unicorn server and redmine and should +# be placed in /etc/init.d. +### END INIT INFO + +# ------------------------------------------------------------------------------ +# Author: Richard V?zina +# Base on Ubuntu 12.04 : /etc/init.d/skeleton +# ven 21 d?c 2012 11:08:31 EST +# ------------------------------------------------------------------------------ + +# Do NOT "set -e" + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +APP=/opt/redmine-2.2.0/ +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="Unicorn and Redmine" +NAME=unicorn_rails +DAEMON=/usr/local/bin/$NAME +DAEMON_ARGS=" -E production -c $APP/config/unicorn.rb -D" +PIDFILE=/opt/redmine-2.2.0/tmp/pids/$NAME.pid +SCRIPTNAME=/etc/init.d/redmine + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.2-14) to ensure that this file is present +# and status_of_proc is working. +. /lib/lsb/init-functions + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \ + $DAEMON_ARGS \ + || return 2 + # Add code here, if necessary, that waits for the process to be ready + # to handle requests from services started subsequently which depend + # on this one. As a last resort, sleep for some time. +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + # Wait for children to finish too if this is a daemon that forks + # and if the daemon is only ever run from this initscript. + # If the above conditions are not satisfied then add some other code + # that waits for the process to drop all resources that could be + # needed by services started subsequently. A last resort is to + # sleep for some time. + start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON + [ "$?" = 2 ] && return 2 + # Many daemons don'\''t delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + # + # If the daemon can reload its configuration without + # restarting (for example, when it is sent a SIGHUP), + # then implement that here. + # + start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME + return 0 +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? + ;; + #reload|force-reload) + # + # If do_reload() is not implemented then leave this commented out + # and leave '\''force-reload'\'' as an alias for '\''restart'\''. + # + #log_daemon_msg "Reloading $DESC" "$NAME" + #do_reload + #log_end_msg $? + #;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # '\''force-reload'\'' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +:' > /etc/init.d/redmine +chmod +x /etc/init.d/redmine +# Backup default Nginx site and replace it +cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default_original +rm /etc/nginx/sites-available/default +# Create configuration file /etc/nginx/sites-available/default +echo 'upstream unicorn_server { + # This is the socket we configured in unicorn.rb + server unix:/tmp/unicorn_rails.socket + fail_timeout=0; +} +server { + listen 80; + #return 301 https://192.168.1.126$request_uri; # http://$hostname$request_uri; # idem #http://wiki.nginx.org/Pitfalls#Taxing_Rewrites + charset utf-8; + server_name localhost; # $hostname; + root /var/www; + access_log /var/log/nginx/yoursite.access.log; + error_log /var/log/nginx/yoursite.error.log; + #to enable correct use of response.static_version + #location ~* /(\w+)/static(?:/_[\d]+\.[\d]+\.[\d]+)?/(.*)$ { + # alias /home/www-data/web2py/applications/$1/static/$2; + # expires max; + #} + location ~* /(\w+)/static/ { + root /home/www-data/web2py/applications/; + #remove next comment on production + #expires max; + } + location ~^\/(?!redmine(.*)) { + #uwsgi_pass 127.0.0.1:9001; + uwsgi_pass unix:///tmp/web2py.socket; + include uwsgi_params; + uwsgi_param UWSGI_SCHEME $scheme; + uwsgi_param SERVER_SOFTWARE nginx/$nginx_version; + } + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + + if (!-f $request_filename) { + proxy_pass http://unicorn_server; + break; + } + } +} +server { + listen 443 default_server ssl; + charset utf-8; + server_name localhost; # $hostname; + root /var/www; + ssl_certificate /etc/nginx/ssl/self_signed.cert; + ssl_certificate_key /etc/nginx/ssl/self_signed.key; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + ssl_ciphers ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA; + ssl_protocols SSLv3 TLSv1; + keepalive_timeout 70; + location ~^\/(?!redmine(.*)) { + #uwsgi_pass 127.0.0.1:9001; + uwsgi_pass unix:///tmp/web2py.socket; + include uwsgi_params; + uwsgi_param UWSGI_SCHEME $scheme; + uwsgi_param SERVER_SOFTWARE nginx/$nginx_version; + } + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + + if (!-f $request_filename) { + proxy_pass http://unicorn_server; + break; + } + } + +}' >/etc/nginx/sites-available/default + +#ln -s /etc/nginx/sites-available/web2py /etc/nginx/sites-enabled/web2py +#rm /etc/nginx/sites-enabled/default + +# We copy ssl files we previously created +if [ -f /etc/nginx/ssl ] +then + cp ~/self_signed.* /etc/nginx/ssl/ + rm ~/self_signed.* +else + mkdir /etc/nginx/ssl + cp ~/self_signed.* /etc/nginx/ssl/ + rm ~/self_signed.* +fi + +pip install --upgrade pip +PIPPATH=`which pip` +$PIPPATH install --upgrade uwsgi + +# Prepare folders for uwsgi +sudo mkdir /etc/uwsgi +sudo mkdir /var/log/uwsgi + +# Create configuration file /etc/uwsgi/web2py.xml +echo ' + /tmp/web2py.socket + /home/www-data/web2py/ + /=wsgihandler:application + + 4 + 60 + 8 + 1 + /tmp/stats.socket + 2000 + 512 + 256 + 192 + www-data + www-data + 0 0 -1 -1 -1 python /home/www-data/web2py/web2py.py -Q -S welcome -M -R scripts/sessions2trash.py -A -o + +' > /etc/uwsgi/web2py.xml + +#Create a configuration file for uwsgi in emperor-mode +#for Upstart in /etc/init/uwsgi-emperor.conf +echo '# Emperor uWSGI script + +description "uWSGI Emperor" +start on runlevel [2345] +stop on runlevel [06] +## +#remove the comments in the next section to enable static file compression for the welcome app +#in that case, turn on gzip_static on; on /etc/nginx/nginx.conf +## +#pre-start script +# python /home/www-data/web2py/web2py.py -S welcome -R scripts/zip_static_files.py +# chown -R www-data:www-data /home/www-data/web2py/* +#end script +respawn +exec uwsgi --master --die-on-term --emperor /etc/uwsgi --logto /var/log/uwsgi/uwsgi.log +' > /etc/init/uwsgi-emperor.conf +# Install Web2py +mkdir /home/www-data +cd /home/www-data +wget http://web2py.com/examples/static/web2py_src.zip +unzip web2py_src.zip +rm web2py_src.zip +# Download latest version of sessions2trash.py +wget http://web2py.googlecode.com/hg/scripts/sessions2trash.py -O /home/www-data/web2py/scripts/sessions2trash.py +chown -R www-data:www-data web2py +cd /home/www-data/web2py +sudo -u www-data python -c "from gluon.main import save_password; save_password('$PW',443)" +/etc/init.d/redmine start +start uwsgi-emperor +/etc/init.d/nginx restart +ufw allow 80 # Or check your firewall configuration