From 5a23ca6230c0ea5176fb6905b3a0c5bbdf6afb8d Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 16 Jan 2012 21:34:07 -0600 Subject: [PATCH 01/77] http://code.google.com/p/web2py/issues/detail?id=610 DAL IMAP suppoer, thanks spametki --- VERSION | 2 +- gluon/dal.py | 577 ++++++++++++++++++++++++++++++++++++++++++++++- gluon/sqlhtml.py | 8 +- 3 files changed, 571 insertions(+), 16 deletions(-) diff --git a/VERSION b/VERSION index 493dafef..61729010 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-13 23:00:13) stable +Version 1.99.4 (2012-01-16 21:33:37) stable diff --git a/gluon/dal.py b/gluon/dal.py index b73e8b59..19b967ab 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -31,6 +31,7 @@ - MongoDB (in progress) - Google:nosql - Google:sql +- IMAP (experimental) Example of usage: @@ -108,6 +109,7 @@ 'google:datastore' # for google app engine datastore 'google:sql' # for google app engine with sql (mysql compatible) 'teradata://DSN=dsn;UID=user;PWD=pass' # experimental +'imap://user:password@server:port' # experimental For more info: help(DAL) @@ -319,6 +321,12 @@ def web2py_uuid(): return str(uuid.uuid4()) except: logger.debug('no mongoDB driver') + try: + import imaplib + drivers.append('IMAP') + except: + logger.debug('could not import imaplib') + PLURALIZE_RULES = [ (re.compile('child$'), re.compile('child$'), 'children'), (re.compile('oot$'), re.compile('oot$'), 'eet'), @@ -3936,7 +3944,7 @@ def _select(self,query,fields,attributes): limitby = attributes.get('limitby', False) #distinct = attributes.get('distinct', False) if orderby: - #print "in if orderby %s" % orderby + print "in if orderby %s" % orderby if isinstance(orderby, (list, tuple)): print "in xorify" orderby = xorify(orderby) @@ -3948,7 +3956,7 @@ def _select(self,query,fields,attributes): mongosort_list.append((f[1:],-1)) else: mongosort_list.append((f,1)) - print "mongosort_list = %s" % mongosort_list + print "mongosort_list = %s" % mongosort_list if limitby: # a tuple @@ -3958,7 +3966,7 @@ def _select(self,query,fields,attributes): limitby_limit = 0 #if distinct: - #print "in distinct %s" % distinct + # print "in distinct %s" % distinct mongofields_dict = son.SON() mongoqry_dict = {} @@ -3989,15 +3997,15 @@ def select(self,query,fields,attributes): print "mongoqry_dict=%s" % mongoqry_dict except: pass - print "mongofields_dict=%s" % mongofields_dict + # print "mongofields_dict=%s" % mongofields_dict ctable = self.connection[tablename] mongo_list_dicts = ctable.find(mongoqry_dict,mongofields_dict,skip=limitby_skip, limit=limitby_limit, sort=mongosort_list) # pymongo cursor object - print "mongo_list_dicts=%s" % mongo_list_dicts + print "mongo_list_dicts=%s" % mongo_list_dicts #if mongo_list_dicts.count() > 0: # #colnames = mongo_list_dicts[0].keys() # assuming all docs have same "shape", grab colnames from first dictionary (aka row) #else: #colnames = mongofields_dict.keys() - #print "colnames = %s" % colnames + print "colnames = %s" % colnames #rows = [row.values() for row in mongo_list_dicts] rows = mongo_list_dicts return self.parse(rows, fields, mongofields_dict.keys(), False, tablename) @@ -4094,7 +4102,7 @@ def parse(self, rows, fields, colnames, blob_decode=True,tablename=None): return rowsobj def INVERT(self,first): - #print "in invert first=%s" % first + print "in invert first=%s" % first return '-%s' % self.expand(first) def drop(self, table, mode=''): @@ -4241,6 +4249,555 @@ def COMMA(self, first, second): return '%s, %s' % (self.expand(first), self.expand(second)) +class IMAPAdapter(NoSQLAdapter): + """ IMAP server adapter + + This class is intended as an interface with + email IMAP servers to perform simple queries in the + web2py DAL query syntax, so email read, search and + other related IMAP mail services (as those implemented + by brands like Google(r), Hotmail(r) and Yahoo!(r) + can be managed from web2py applications. + + The code uses examples by Yuji Tomita on this post: + http://yuji.wordpress.com/2011/06/22/python-imaplib-imap-example-with-gmail/#comment-1137 + + And IMAP docs for Python imaplib and IETF's RFC2060 + + This adapter was tested with a small set of operations with Gmail(r). Other + services requests could raise command syntax and response data issues. + + + """ + types = { + 'string': str, + 'text': str, + 'date': datetime.date, + 'datetime': datetime.datetime, + 'id': long, + 'boolean': bool, + 'integer': int, + 'blob': str, + } + + dbengine = 'imap' + + def __init__(self, + db, + uri, + pool_size=0, + folder=None, + db_codec ='UTF-8', + credential_decoder=lambda x:x, + driver_args={}, + adapter_args={}): + + # db uri: user@example.com:password@imap.server.com:123 + uri = uri.split("://")[1] + self.db = db + self.uri = uri + self.pool_size=0 + self.folder = folder + self.db_codec = db_codec + self.credential_decoder = credential_decoder + self.driver_args = driver_args + self.adapter_args = adapter_args + self.mailbox_size = None + self.mailbox_names = dict() + self.encoding = sys.getfilesystemencoding() + """ MESSAGE is an identifier for sequence number""" + + self.search_fields = { + 'id': 'MESSAGE', + 'created': 'DATE', + 'uid': 'UID', + 'sender': 'FROM', + 'to': 'TO', + 'content': 'TEXT', + 'deleted': '\\Deleted', + 'draft': '\\Draft', + 'flagged': '\\Flagged', + 'recent': '\\Recent', + 'seen': '\\Seen', + 'subject': 'SUBJECT', + 'answered': '\\Answered', + 'mime': None, + 'email': None, + 'attachments': None + } + + db['_lastsql'] = '' + + m = re.compile('^(?P[^:]+)(\:(?P[^@]*))?@(?P[^\:@/]+)(\:(?P[0-9]+))?$').match(uri) + user = m.group('user') + password = m.group('password') + host = m.group('host') + port = int(m.group('port')) + over_ssl = False + if port==993: + over_ssl = True + + driver_args.update(dict(host=host,port=port, password=password, user=user)) + def connect(driver_args=driver_args): + # it is assumed sucessful authentication alLways + # TODO: support direct connection and login tests + if over_ssl: + imap4 = imaplib.IMAP4_SSL + else: + imap4 = imaplib.IMAP4 + connection = imap4(driver_args["host"], driver_args["port"]) + connection.login(driver_args["user"], driver_args["password"]) + return connection + + self.pool_connection(connect,cursor=False) + self.db.define_tables = self.define_tables + + def get_last_message(self, tablename): + last_message = None + # request mailbox list to the server + # if needed + if not len(self.mailbox_names.keys()) > 0: + self.get_mailboxes() + try: + result = self.connection.select(self.mailbox_names[tablename]) + last_message = int(result[1][0]) + except (IndexError, ValueError, TypeError, KeyError), e: + logger.debug("Error retrieving the last mailbox sequence number. %s" % str(e)) + return last_message + + def get_uid_bounds(self, tablename): + if not len(self.mailbox_names.keys()) > 0: + self.get_mailboxes() + # fetch first and last messages + # return (first, last) messages uid's + last_message = self.get_last_message(tablename) + result, data = self.connection.uid("search", None, "(ALL)") + uid_list = data[0].strip().split() + if len(uid_list) <= 0: + return None + else: + return (uid_list[0], uid_list[-1]) + + def convert_date(self, date, add=None): + if add is None: + add = datetime.timedelta() + """ Convert a date object to a string + with d-Mon-Y style for IMAP or the inverse + case + + add adds to the date object + """ + months = [None, "Jan","Feb","Mar","Apr","May","Jun", + "Jul", "Aug","Sep","Oct","Nov","Dec"] + if isinstance(date, basestring): + # Prevent unexpected date response format + try: + dayname, datestring = date.split(",") + except (ValueError): + logger.debug("Could not parse date text: %s" % date) + return None + date_list = datestring.strip().split() + year = int(date_list[2]) + month = months.index(date_list[1]) + day = int(date_list[0]) + hms = [int(value) for value in date_list[3].split(":")] + return datetime.datetime(year, month, day, + hms[0], hms[1], hms[2]) + add + elif isinstance(date, (datetime.datetime, datetime.date)): + return (date + add).strftime("%d-%b-%Y") + + else: + return None + + def decode_text(self): + """ translate encoded text for mail to unicode""" + # not implemented + pass + + def get_charset(self, message): + charset = message.get_content_charset() + return charset + + def get_mailboxes(self): + mailboxes_list = self.connection.list() + mailboxes = list() + for item in mailboxes_list[1]: + item = item.strip() + if not "NOSELECT" in item.upper(): + sub_items = item.split("\"") + sub_items = [sub_item for sub_item in sub_items if len(sub_item.strip()) > 0] + mailbox = sub_items[len(sub_items) - 1] + # remove unwanted characters and store original names + mailbox_name = mailbox.replace("[", "").replace("]", "").replace("/", "_") + mailboxes.append(mailbox_name) + self.mailbox_names[mailbox_name] = mailbox + return mailboxes + + def define_tables(self): + """ + Auto create common IMAP fileds + + This function creates fields definitions "statically" + meaning that custom fields as in other adapters should + not be supported and definitions handled on a service/mode + basis (local syntax for Gmail(r), Ymail(r) + """ + mailboxes = self.get_mailboxes() + for mailbox_name in mailboxes: + self.db.define_table("%s" % mailbox_name, + Field("uid", "string", writable=False), + Field("answered", "boolean", writable=False), + Field("created", "datetime", writable=False), + Field("content", "list:text", writable=False), + Field("to", "string", writable=False), + Field("deleted", "boolean", writable=False), + Field("draft", "boolean", writable=False), + Field("flagged", "boolean", writable=False), + Field("sender", "string", writable=False), + Field("recent", "boolean", writable=False), + Field("seen", "boolean", writable=False), + Field("subject", "string", writable=False), + Field("mime", "string", writable=False), + Field("email", "text", writable=False), + Field("attachments", "list:text", writable=False), + ) + + def create_table(self, *args, **kwargs): + # not implemented + logger.debug("Create table feature is not implemented for %s" % type(self)) + + def _select(self,query,fields,attributes): + """ Search and Fetch records and return web2py + rows + """ + + # move this statement elsewhere (upper-level) + import email + import email.header + decode_header = email.header.decode_header + # get records from imap server with search + fetch + # convert results to a dictionary + tablename = None + if isinstance(query, (Expression, Query)): + tablename = self.get_table(query) + mailbox = self.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": + fetch_results = list() + # 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. + messages_set = search_result[1][0].split()[:10] + # 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: + typ, data = self.connection.uid("fetch", uid, imap_fields) + fr = {"message": int(data[0][0].split()[0]), + "uid": int(uid), + "email": email.message_from_string(data[0][1]) + } + fr["multipart"] = fr["email"].is_multipart() + fetch_results.append(fr) + + elif isinstance(query, basestring): + pass + else: + pass + + imapqry_dict = {} + imapfields_dict = {} + + if len(fields) == 1 and isinstance(fields[0], SQLALL): + allfields = True + elif len(fields) == 0: + allfields = True + else: + allfields = False + if allfields: + fieldnames = ["%s.%s" % (tablename, field) for field in self.search_fields.keys()] + else: + fieldnames = ["%s.%s" % (tablename, field.name) for field in fields] + + for k in fieldnames: + imapfields_dict[k] = k + + imapqry_list = list() + imapqry_array = list() + for fr in fetch_results: + n = int(fr["message"]) + item_dict = dict() + message = fr["email"] + uid = fr["uid"] + charset = self.get_charset(message) + # Return messages data mapping static fields + # and fetched results. Mapping should be made + # outside the select function (with auxiliary + # instance methods) + + # pending: search flags states trough the email message + # instances for correct output + + if "%s.id" % tablename in fieldnames: + item_dict["%s.id" % tablename] = n + if "%s.created" % tablename in fieldnames: + item_dict["%s.created" % tablename] = self.convert_date(message["Date"]) + if "%s.uid" % tablename in fieldnames: + item_dict["%s.uid" % tablename] = uid + if "%s.sender" % tablename in fieldnames: + # If there is no encoding found in the message header + # force utf-8 replacing characters (change this to + # module's defaults). Applies to .sender and .to fields + if charset is not None: + item_dict["%s.sender" % tablename] = unicode(message["From"], charset, "replace") + else: + item_dict["%s.sender" % tablename] = unicode(message["From"], "utf-8", "replace") + if "%s.to" % tablename in fieldnames: + if charset is not None: + item_dict["%s.to" % tablename] = unicode(message["To"], charset, "replace") + else: + item_dict["%s.to" % tablename] = unicode(message["To"], "utf-8", "replace") + if "%s.content" % tablename in fieldnames: + content = [] + for part in message.walk(): + if "text" in part.get_content_maintype(): + payload = part.get_payload(decode=True) + content.append(payload) + item_dict["%s.content" % tablename] = content + if "%s.deleted" % tablename in fieldnames: + item_dict["%s.deleted" % tablename] = None + if "%s.draft" % tablename in fieldnames: + item_dict["%s.draft" % tablename] = None + if "%s.flagged" % tablename in fieldnames: + item_dict["%s.flagged" % tablename] = None + if "%s.recent" % tablename in fieldnames: + item_dict["%s.recent" % tablename] = None + if "%s.seen" % tablename in fieldnames: + item_dict["%s.seen" % tablename] = None + if "%s.subject" % tablename in fieldnames: + subject = message["Subject"] + decoded_subject = decode_header(subject) + text = decoded_subject[0][0] + encoding = decoded_subject[0][1] + if encoding is not None: + text = unicode(text, encoding) + item_dict["%s.subject" % tablename] = text + if "%s.answered" % tablename in fieldnames: + item_dict["%s.answered" % tablename] = None + 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 + if "%s.email" % tablename in fieldnames: + item_dict["%s.email" % tablename] = message + + if "%s.attachments" % tablename in fieldnames: + attachments = [] + for part in message.walk(): + if not "text" in part.get_content_maintype(): + attachments.append(part.get_payload(decode=True)) + item_dict["%s.attachments" % tablename] = attachments + imapqry_list.append(item_dict) + + # extra object mapping for the sake of rows object + # creation (sends an array or lists) + for item_dict in imapqry_list: + imapqry_array_item = list() + for fieldname in fieldnames: + 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 + result = self.parse(imapqry_array, colnames) + return result + + def count(self,query,distinct=None): + # not implemented + # (count search results without select call) + pass + + def BELONGS(self, first, second): + result = None + name = self.search_fields[first.name] + if name == "MESSAGE": + values = [str(val) for val in second if str(val).isdigit()] + result = "%s" % ",".join(values).strip() + + elif name == "UID": + values = [str(val) for val in second if str(val).isdigit()] + result = "UID %s" % ",".join(values).strip() + + else: + raise Exception("Operation not supported") + # result = "(%s %s)" % (self.expand(first), self.expand(second)) + return result + + def CONTAINS(self, first, second): + result = None + name = self.search_fields[first.name] + + if name in ("FROM", "TO", "SUBJECT", "TEXT"): + result = "%s \"%s\"" % (name, self.expand(second)) + else: + if first.name in ("cc", "bcc"): + result = "%s \"%s\"" % (first.name.upper(), self.expand(second)) + elif first.name == "mime": + result = "HEADER Content-Type \"%s\"" % self.expand(second) + else: + raise Exception("Operation not supported") + return result + + def GT(self, first, second): + result = None + name = self.search_fields[first.name] + if name == "MESSAGE": + last_message = self.get_last_message(first.tablename) + result = "%d:%d" % (int(self.expand(second)) + 1, last_message) + elif name == "UID": + # GT and LT may not return + # expected sets depending on + # the uid format implemented + try: + pedestal, threshold = self.get_uid_bounds(first.tablename) + except TypeError, e: + logger.debug("Error requesting uid bounds: %s", str(e)) + return "" + try: + lower_limit = int(self.expand(second)) + 1 + except (ValueError, TypeError), e: + raise Exception("Operation not supported (non integer UID)") + result = "UID %s:%s" % (lower_limit, threshold) + elif name == "DATE": + result = "SINCE %s" % self.convert_date(second, add=datetime.timedelta(1)) + else: + raise Exception("Operation not supported") + return result + + def GE(self, first, second): + result = None + name = self.search_fields[first.name] + if name == "MESSAGE": + last_message = self.get_last_message(first.tablename) + result = "%s:%s" % (self.expand(second), last_message) + elif name == "UID": + # GT and LT may not return + # expected sets depending on + # the uid format implemented + try: + pedestal, threshold = self.get_uid_bounds(first.tablename) + except TypeError, e: + logger.debug("Error requesting uid bounds: %s", str(e)) + return "" + lower_limit = self.expand(second) + result = "UID %s:%s" % (lower_limit, threshold) + elif name == "DATE": + result = "SINCE %s" % self.convert_date(second) + else: + raise Exception("Operation not supported") + return result + + def LT(self, first, second): + result = None + name = self.search_fields[first.name] + if name == "MESSAGE": + result = "%s:%s" % (1, int(self.expand(second)) - 1) + elif name == "UID": + try: + pedestal, threshold = self.get_uid_bounds(first.tablename) + except TypeError, e: + logger.debug("Error requesting uid bounds: %s", str(e)) + return "" + try: + upper_limit = int(self.expand(second)) - 1 + except (ValueError, TypeError), e: + raise Exception("Operation not supported (non integer UID)") + result = "UID %s:%s" % (pedestal, upper_limit) + elif name == "DATE": + result = "BEFORE %s" % self.convert_date(second) + else: + raise Exception("Operation not supported") + return result + + def LE(self, first, second): + result = None + name = self.search_fields[first.name] + if name == "MESSAGE": + result = "%s:%s" % (1, self.expand(second)) + elif name == "UID": + try: + pedestal, threshold = self.get_uid_bounds(first.tablename) + except TypeError, e: + logger.debug("Error requesting uid bounds: %s", str(e)) + return "" + upper_limit = int(self.expand(second)) + result = "UID %s:%s" % (pedestal, upper_limit) + elif name == "DATE": + result = "BEFORE %s" % self.convert_date(second, add=datetime.timedelta(1)) + else: + raise Exception("Operation not supported") + return result + + def NE(self, first, second): + result = self.NOT(self.EQ(first, second)) + result = result.replace("NOT NOT", "").strip() + return result + + def EQ(self,first,second): + name = self.search_fields[first.name] + result = None + if name is not None: + if name == "MESSAGE": + # query by message sequence number + result = "%s" % self.expand(second) + elif name == "UID": + result = "UID %s" % self.expand(second) + elif name == "DATE": + result = "ON %s" % self.convert_date(second) + + elif name in ('\\Deleted', '\\Draft', '\\Flagged', '\\Recent', '\\Seen', '\\Answered'): + if second: + result = "%s" % (name.upper()[1:]) + else: + result = "NOT %s" % (name.upper()[1:]) + else: + raise Exception("Operation not supported") + else: + raise Exception("Operation not supported") + return result + + def AND(self, first, second): + result = "%s %s" % (self.expand(first), self.expand(second)) + return result + + def OR(self, first, second): + result = "OR %s %s" % (self.expand(first), self.expand(second)) + return "%s" % result.replace("OR OR", "OR") + + def NOT(self, first): + result = "NOT %s" % self.expand(first) + return result ######################################################################## # end of adapters @@ -4271,6 +4828,7 @@ def COMMA(self, first, second): 'google:sql': GoogleSQLAdapter, 'couchdb': CouchDBAdapter, 'mongodb': MongoDBAdapter, + 'imap': IMAPAdapter } @@ -6935,8 +7493,3 @@ def test_all(): import doctest doctest.testmod() - - - - - diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index d7e5ecf6..65431dbb 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -702,7 +702,7 @@ def __init__( labels = None, col3 = {}, submit_button = 'Submit', - delete_label = 'Check to delete:', + delete_label = 'Check to delete', showid = True, readonly = False, comments = True, @@ -919,7 +919,7 @@ def __init__( ) xfields.append((self.FIELDKEY_DELETE_RECORD+SQLFORM.ID_ROW_SUFFIX, LABEL( - delete_label, + delete_label,separator, _for=self.FIELDKEY_DELETE_RECORD, _id=self.FIELDKEY_DELETE_RECORD+SQLFORM.ID_LABEL_SUFFIX), widget, @@ -1577,7 +1577,9 @@ def buttons(edit=False,view=False,record=None): record = table(request.args[-1]) or redirect(URL('error')) edit_form = SQLFORM(table,record,upload=upload,ignore_rw=ignore_rw, deletable=deletable, - _class='web2py_form') + _class='web2py_form', + submit_button = T('Submit'), + delete_label = T('Check to delete')) edit_form.process(formname=formname, onvalidation=onvalidation, onsuccess=onupdate, From 0029b43aac78076853b1b39afc05c109fe3f91d2 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 16 Jan 2012 21:37:33 -0600 Subject: [PATCH 02/77] revert commented unlock in languages or test failure --- VERSION | 2 +- gluon/languages.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 61729010..63357aac 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-16 21:33:37) stable +Version 1.99.4 (2012-01-16 21:37:31) stable diff --git a/gluon/languages.py b/gluon/languages.py index 67db46dd..6683afda 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -41,7 +41,7 @@ def read_dict_aux(filename): fp = open(filename, 'r') portalocker.lock(fp, portalocker.LOCK_SH) lang_text = fp.read().replace('\r\n', '\n') - # needed? portalocker.unlock(fp) + portalocker.unlock(fp) # needed or test_languages.py fails fp.close() if not lang_text.strip(): return {} From f61bec0566387e269e5ffffda2766b8d05d723c5 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 16 Jan 2012 21:39:58 -0600 Subject: [PATCH 03/77] issue 612, fixed problem with oracle adapter, thanks faridgs --- VERSION | 2 +- gluon/dal.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 63357aac..fdae0e0e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-16 21:37:31) stable +Version 1.99.4 (2012-01-16 21:39:52) stable diff --git a/gluon/dal.py b/gluon/dal.py index 19b967ab..e35adff9 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -2120,8 +2120,8 @@ def connect(uri=uri,driver_args=driver_args): self.execute("ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS';") oracle_fix = re.compile("[^']*('[^']*'[^']*)*\:(?PCLOB\('([^']+|'')*'\))") - def execute(self, command): - args = [] + def execute(self, command, args=None): + args = args or [] i = 1 while True: m = self.oracle_fix.match(command) From 74ccb85ed958ca122100e9944e231b0fa26d5206 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 16 Jan 2012 21:46:23 -0600 Subject: [PATCH 04/77] issue 613, not sure if completely resolved, needs more testing --- VERSION | 2 +- gluon/sqlhtml.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/VERSION b/VERSION index fdae0e0e..cdeb49fc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-16 21:39:52) stable +Version 1.99.4 (2012-01-16 21:46:16) stable diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 65431dbb..6be176fe 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -801,12 +801,12 @@ def __init__( self.custom.inpval.id = '' widget = '' if record: - if showid and 'id' in fields and field.readable: - v = record['id'] + if showid and field.name in record and field.readable: + v = record[field.name] widget = SPAN(v, _id=field_id) self.custom.dspval.id = str(v) xfields.append((row_id,label, widget,comment)) - self.record_id = str(record['id']) + self.record_id = str(record[field.name]) self.custom.widget.id = widget continue @@ -944,7 +944,7 @@ def __init__( if not self['hidden']: self['hidden'] = {} if not keyed: - self['hidden']['id'] = record['id'] + self['hidden']['id'] = record[table._id.name] (begin, end) = self._xml() self.custom.begin = XML("<%s %s>" % (self.tag, begin)) From 2a038a067951c2086c4054a8fdcf0fdbb9a4f080 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 16 Jan 2012 21:51:50 -0600 Subject: [PATCH 05/77] possibly fixed issue 614? --- VERSION | 2 +- gluon/dal.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/VERSION b/VERSION index cdeb49fc..37e3af06 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-16 21:46:16) stable +Version 1.99.4 (2012-01-16 21:51:43) stable diff --git a/gluon/dal.py b/gluon/dal.py index e35adff9..23266af4 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -1461,12 +1461,12 @@ def parse_value(self, value, field_type): pass if isinstance(value, unicode): value = value.encode('utf-8') - if isinstance(field_type, SQLCustomType): + elif isinstance(field_type, SQLCustomType): value = field_type.decoder(value) - elif not isinstance(field_type, str) or value is None: - return value - if field_type in ('string', 'text', 'password', 'upload'): + if not isinstance(field_type, str) or value is None: return value + elif field_type in ('string', 'text', 'password', 'upload'): + return value else: key = regex_type.match(field_type).group(0) return self.parsemap[key](value,field_type) From f2eea60fb1f529694235a3f142adc7309b1b8a6f Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 16 Jan 2012 21:58:23 -0600 Subject: [PATCH 06/77] issue 617, separator in tools addrows, thanks hi21alt --- VERSION | 2 +- gluon/tools.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 37e3af06..4cc82eab 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-16 21:51:43) stable +Version 1.99.4 (2012-01-16 21:58:15) stable diff --git a/gluon/tools.py b/gluon/tools.py index 942d0856..fa974359 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1882,7 +1882,7 @@ def register( repr(request.vars.get(passfield, None)), error_message=self.messages.mismatched_password)) - addrow(form, self.messages.verify_password + ':', + addrow(form, self.messages.verify_password + self.settings.label_separator, form.custom.widget.password_two, self.messages.verify_password_comment, formstyle, From 0f43651e0952005aadf149b7b841cdaf5a586695 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 16 Jan 2012 22:03:30 -0600 Subject: [PATCH 07/77] issue 618, input with error gets class invalidinput, thanks David --- VERSION | 2 +- gluon/html.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 4cc82eab..92241d6a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-16 21:58:15) stable +Version 1.99.4 (2012-01-16 22:03:22) stable diff --git a/gluon/html.py b/gluon/html.py index 97c29b35..83c485cd 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -1611,6 +1611,7 @@ def xml(self): if name and hasattr(self, 'errors') \ and self.errors.get(name, None) \ and self['hideerror'] != True: + self['_class'] = (self['_class'] and self['_class']+' ' or '')+'invalidinput' return DIV.xml(self) + DIV(self.errors[name], _class='error', errors=None, _id='%s__error' % name).xml() else: From 76699b6b8e615768728f1cf94c2a691c46b07ff3 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 16 Jan 2012 22:29:53 -0600 Subject: [PATCH 08/77] issue 594 fixed, != in grid query --- VERSION | 2 +- gluon/dal.py | 16 +++++++++------- gluon/sqlhtml.py | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/VERSION b/VERSION index 92241d6a..87af14ac 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-16 22:03:22) stable +Version 1.99.4 (2012-01-16 22:29:44) stable diff --git a/gluon/dal.py b/gluon/dal.py index 23266af4..2cf7f578 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -5059,7 +5059,7 @@ def smart_query(fields,text): for a,b in [('&','and'), ('|','or'), ('~','not'), - ('==','=='), + ('==','='), ('<','<'), ('>','>'), ('<=','<='), @@ -5067,7 +5067,7 @@ def smart_query(fields,text): ('<>','!='), ('=<','<='), ('=>','>='), - ('=','=='), + ('=','='), (' less or equal than ','<='), (' greater or equal than ','>='), (' equal or less than ','<='), @@ -5078,18 +5078,19 @@ def smart_query(fields,text): (' equal or greater ','>='), (' not equal to ','!='), (' not equal ','!='), - (' equal to ','=='), - (' equal ','=='), + (' equal to ','='), + (' equal ','='), (' equals ','!='), (' less than ','<'), (' greater than ','>'), (' starts with ','startswith'), (' ends with ','endswith'), - (' is ','==')]: + (' is ','=')]: if a[0]==' ': text = text.replace(' is'+a,' %s ' % b) text = text.replace(a,' %s ' % b) text = re.sub('\s+',' ',text).lower() + text = re.sub('(?P[\<\>\!\=])\s+(?P[\<\>\!\=])','\g\g',text) query = field = neg = op = logic = None for item in text.split(): if field is None: @@ -5110,12 +5111,13 @@ def smart_query(fields,text): value = constants[item[1:]] else: value = item - if op == '==': op = 'like' - if op == '==': new_query = field==value + if op == '=': op = 'like' + if op == '=': new_query = field==value elif op == '<': new_query = field': new_query = field>value elif op == '<=': new_query = field<=value elif op == '>=': new_query = field>=value + elif op == '!=': new_query = field!=value elif field.type in ('text','string'): if op == 'contains': new_query = field.contains(value) elif op == 'like': new_query = field.like(value) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 6be176fe..f76d096d 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1292,7 +1292,7 @@ def factory(*fields, **attributes): @staticmethod def build_query(fields,keywords): key = keywords.strip() - if key and not ' ' in key: + if key and not ' ' in key and not '"' in key and not "'" in key: SEARCHABLE_TYPES = ('string','text','list:string') parts = [field.contains(key) for field in fields if field.type in SEARCHABLE_TYPES] else: From 81512727fbdafa6abe5fb70aebf08a796fc309a3 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 16 Jan 2012 22:32:12 -0600 Subject: [PATCH 09/77] issue 595 --- VERSION | 2 +- gluon/sqlhtml.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 87af14ac..2126fd06 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-16 22:29:44) stable +Version 1.99.4 (2012-01-16 22:32:03) stable diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index f76d096d..587d3b64 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1780,6 +1780,7 @@ def self_link(name,p): if not field.readable: continue if field.type=='blob': continue value = row[field] + maxlength = maxtextlengths.get(str(field),maxtextlength) if field.represent: try: value=field.represent(value,row) @@ -1800,8 +1801,8 @@ def self_link(name,p): _href='http://github.com/%s/%s' % (upload, value)) else: value = '' - elif isinstance(value,str) and len(value)>maxtextlength: - value=value[:maxtextlengths.get(str(field),maxtextlength)]+'...' + elif isinstance(value,str) and len(value)>maxlength: + value=value[:maxlength]+'...' else: value=field.formatter(value) tr.append(TD(value)) From c5b4e355904bc4c0337123ff0976a66908a450e8 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 16 Jan 2012 23:25:15 -0600 Subject: [PATCH 10/77] oops, some tests were not committed, now they are --- VERSION | 2 +- gluon/tests/test_languages.py | 59 +++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 gluon/tests/test_languages.py diff --git a/VERSION b/VERSION index 2126fd06..c87a46e0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-16 22:32:03) stable +Version 1.99.4 (2012-01-16 23:25:02) stable diff --git a/gluon/tests/test_languages.py b/gluon/tests/test_languages.py new file mode 100644 index 00000000..f1116ee0 --- /dev/null +++ b/gluon/tests/test_languages.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + Unit tests for gluon.languages +""" + +import sys +import os +if os.path.isdir('gluon'): + sys.path.append(os.path.realpath('gluon')) +else: + sys.path.append(os.path.realpath('../')) + +import unittest +import languages +import tempfile +import threading +import logging + +try: + import multiprocessing + + def read_write(args): + (filename, iterations) = args + for i in range(0, iterations): + content = languages.read_dict(filename) + if not len(content): + return False + languages.write_dict(filename, content) + return True + + class TestLanguagesParallel(unittest.TestCase): + + def setUp(self): + self.filename = tempfile.mktemp() + contents = dict() + for i in range(1000): + contents["key%d" % i] = "value%d" % i + languages.write_dict(self.filename, contents) + + def tearDown(self): + try: + os.remove(self.filename) + except: + pass + + def test_reads_and_writes(self): + readwriters = 10 + pool = multiprocessing.Pool(processes = readwriters) + results = pool.map(read_write, [[self.filename, 10]] * readwriters) + for result in results: + self.assertTrue(result) + +except ImportError: + logging.warning("Skipped test case, no multiprocessing module.") + +if __name__ == '__main__': + unittest.main() From 652e6fd461fabf16f27444b1c6c0db46e94b1d3d Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Tue, 17 Jan 2012 21:50:21 -0600 Subject: [PATCH 11/77] scripts/services/service.py, thanks Ross --- VERSION | 2 +- scripts/service/linux.py | 221 +++++++++++++++++++++++++++++++++++++ scripts/service/service.py | 201 +++++++++++++++++++++++++++++++++ 3 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 scripts/service/linux.py create mode 100644 scripts/service/service.py diff --git a/VERSION b/VERSION index c87a46e0..74ae29e3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-16 23:25:02) stable +Version 1.99.4 (2012-01-17 21:50:01) stable diff --git a/scripts/service/linux.py b/scripts/service/linux.py new file mode 100644 index 00000000..eece4801 --- /dev/null +++ b/scripts/service/linux.py @@ -0,0 +1,221 @@ +from service import ServiceBase +import os, sys, time, subprocess, atexit +from signal import SIGTERM + +class LinuxService(ServiceBase): + def __init__(self, name, label, stdout='/dev/null', stderr='/dev/null'): + ServiceBase.__init__(self, name, label, stdout, stderr) + self.pidfile = '/tmp/%s.pid' % name + self.config_file = '/etc/%s.conf' % name + + def daemonize(self): + """ + do the UNIX double-fork magic, see Stevens' "Advanced + Programming in the UNIX Environment" for details (ISBN 0201563177) + http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + """ + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError, e: + sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) + return + + # decouple from parent environment + os.chdir("/") + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + # exit from second parent + sys.exit(0) + except OSError, e: + sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) + return + + # redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = file('/dev/null', 'r') + so = file(self.stdout or '/dev/null', 'a+') + se = file(self.stderr or '/dev/null', 'a+') + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + def getpid(self): + # Check for a pidfile to see if the daemon already runs + try: + pf = file(self.pidfile,'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + return pid + + def status(self): + pid = self.getpid() + if pid: + return 'Service running with PID %s.' % pid + else: + return 'Service is not running.' + + def check_permissions(self): + if not os.geteuid() == 0: + return (False, 'This script must be run with root permissions.') + else: + return (True, '') + + def start(self): + """ + Start the daemon + """ + pid = self.getpid() + + if pid: + message = "Service already running under PID %s\n" + sys.stderr.write(message % self.pidfile) + return + + # Start the daemon + self.daemonize() + self.run() + + def stop(self): + """ + Stop the daemon + """ + pid = self.getpid() + + if not pid: + message = "Service is not running\n" + sys.stderr.write(message) + return # not an error in a restart + + # Try killing the daemon process + try: + while 1: + os.kill(pid, SIGTERM) + time.sleep(0.5) + except OSError, err: + err = str(err) + if err.find("No such process") > 0: + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + else: + print str(err) + return + + def restart(self): + """ + Restart the daemon + """ + self.stop() + self.start() + + def run(self): + atexit.register(self.terminate) + + args = self.load_configuration()[0] + stdout = open(self.stdout, 'a+') + stderr = open(self.stderr, 'a+') + process = subprocess.Popen(args, stdout=stdout, stderr=stderr) + file(self.pidfile,'w+').write("%s\n" % process.pid) + process.wait() + + self.terminate() + + def terminate(self): + try: + os.remove(self.pidfile) + except: + pass + + def install(self): + env = self.detect_environment() + src_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'service.py') + + # make sure this script is executable + self.run_command('chmod', '+x', src_path) + + # link this daemon to the service directory + dest_path = env['rc.d-path'] + self.name + os.symlink(src_path, dest_path) + + # start the service at boot + install_command = self.get_service_installer_command(env) + result = self.run_command(*install_command) + self.start() + + def uninstall(self): + self.stop() + env = self.detect_environment() + + # stop the service from autostarting + uninstall_command = self.get_service_uninstaller_command(env) + result = self.run_command(*uninstall_command) + + # remove link to the script from the service directory + path = env['rc.d-path'] + self.name + os.remove(path) + + def detect_environment(self): + """ + Returns a dictionary of command/path to the required command-line applications. + One key is 'dist' which will either be 'debian' or 'redhat', which is the best + guess as to which Linux distribution the current system is based on. + """ + check_for = [ + 'chkconfig', + 'service', + 'update-rc.d', + 'rpm', + 'dpkg', + ] + + env = dict() + for cmd in check_for: + result = self.run_command('which', cmd) + if result[0]: + env[cmd] = result[0].replace('\n', '') + + if 'rpm' in env: + env['dist'] = 'redhat' + env['rc.d-path'] = '/etc/rc.d/init.d/' + elif 'dpkg' in env: + env['dist'] = 'debian' + env['rc.d-path'] = '/etc/init.d/' + else: + env['dist'] = 'unknown' + env['rc.d-path'] = '/dev/null/' + + return env + + def get_service_installer_command(self, env): + """ + Returns list of args required to set a service to run on boot. + """ + if env['dist'] == 'redhat': + cmd = env['chkconfig'] + return [cmd, self.name, 'on'] + else: + cmd = env['update-rc.d'] + return [cmd, self.name, 'defaults'] + + def get_service_uninstaller_command(self, env): + """ + Returns list of arge required to stop a service from running at boot. + """ + if env['dist'] == 'redhat': + cmd = env['chkconfig'] + return [cmd, self.name, 'off'] + else: + cmd = env['update-rc.d'] + return [cmd, self.name, 'remove'] + diff --git a/scripts/service/service.py b/scripts/service/service.py new file mode 100644 index 00000000..e97eeab3 --- /dev/null +++ b/scripts/service/service.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python + +import sys, os, time, subprocess + +class Base: + def run_command(self, *args): + """ + Returns the output of a command as a tuple (output, error). + """ + p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return p.communicate() + +class ServiceBase(Base): + def __init__(self, name, label, stdout=None, stderr=None): + self.name = name + self.label = label + self.stdout = stdout + self.stderr = stderr + self.config_file = None + + def load_configuration(self): + """ + Loads the configuration required to build the command-line string + for running web2py. Returns a tuple (command_args, config_dict). + """ + s = os.path.sep + + default = dict( + python = 'python', + web2py = os.path.join(s.join(__file__.split(s)[:-3]), 'web2py.py'), + http_enabled = True, + http_ip = '0.0.0.0', + http_port = 8000, + https_enabled = True, + https_ip = '0.0.0.0', + https_port = 8001, + https_key = '', + https_cert = '', + password = '', + ) + + config = default + if self.config_file: + try: + f = open(self.config_file, 'r') + lines = f.readlines() + f.close() + + for line in lines: + fields = line.split('=', 1) + if len(fields) == 2: + key, value = fields + key = key.strip() + value = value.strip() + config[key] = value + except: + pass + + web2py_path = os.path.dirname(config['web2py']) + os.chdir(web2py_path) + + args = [config['python'], config['web2py']] + interfaces = [] + ports = [] + + if config['http_enabled']: + ip = config['http_ip'] + port = config['http_port'] + interfaces.append('%s:%s' % (ip, port)) + ports.append(port) + if config['https_enabled']: + ip = config['https_ip'] + port = config['https_port'] + key = config['https_key'] + cert = config['https_cert'] + if key != '' and cert != '': + interfaces.append('%s:%s:%s:%s' % (ip, port, cert, key)) + ports.append(ports) + if len(interfaces) == 0: + sys.exit('Configuration error. Must have settings for http and/or https') + + password = config['password'] + if not password == '': + from gluon import main + for port in ports: + main.save_password(password, port) + + password = '' + + args.append('-a "%s"' % password) + + interfaces = ';'.join(interfaces) + args.append('--interfaces=%s' % interfaces) + + if 'log_filename' in config.key(): + log_filename = config['log_filename'] + args.append('--log_filename=%s' % log_filename) + + return (args, config) + + def start(self): + pass + + def stop(self): + pass + + def restart(self): + pass + + def status(self): + pass + + def run(self): + pass + + def install(self): + pass + + def uninstall(self): + pass + + def check_permissions(self): + """ + Does the script have permissions to install, uninstall, start, and stop services? + Return value must be a tuple (True/False, error_message_if_False). + """ + return (False, 'Permissions check not implemented') + +class WebServerBase(Base): + def install(self): + pass + + def uninstall(self): + pass + + +def get_service(): + service_name = 'web2py' + service_label = 'web2py Service' + + if sys.platform == 'linux2': + from linux import LinuxService as Service + elif sys.platform == 'darwin': + # from mac import MacService as Service + sys.exit('Mac OS X is not yet supported.\n') + elif sys.platform == 'win32': + # from windows import WindowsService as Service + sys.exit('Windows is not yet supported.\n') + else: + sys.exit('The following platform is not supported: %s.\n' % sys.platform) + + service = Service(service_name, service_label) + return service + +if __name__ == '__main__': + service = get_service() + is_root, error_message = service.check_permissions() + if not is_root: + sys.exit(error_message) + + if len(sys.argv) >= 2: + command = sys.argv[1] + if command == 'start': + service.start() + elif command == 'stop': + service.stop() + elif command == 'restart': + service.restart() + elif command == 'status': + print service.status() + '\n' + elif command == 'run': + service.run() + elif command == 'install': + service.install() + elif command == 'uninstall': + service.uninstall() + elif command == 'install-apache': + # from apache import Apache + # server = Apache() + # server.install() + sys.exit('Configuring Apache is not yet supported.\n') + elif command == 'uninstall-apache': + # from apache import Apache + # server = Apache() + # server.uninstall() + sys.exit('Configuring Apache is not yet supported.\n') + else: + sys.exit('Unknown command: %s' % command) + else: + print 'Usage: %s [command] \n' % sys.argv[0] + \ + '\tCommands:\n' + \ + '\t\tstart Starts the service\n' + \ + '\t\tstop Stop the service\n' + \ + '\t\trestart Restart the service\n' + \ + '\t\tstatus Check if the service is running\n' + \ + '\t\trun Run service is blocking mode\n' + \ + '\t\t (Press Ctrl + C to exit)\n' + \ + '\t\tinstall Install the service\n' + \ + '\t\tuninstall Uninstall the service\n' + \ + '\t\tinstall-apache Install as an Apache site\n' + \ + '\t\tuninstall-apache Uninstall from Apache\n' From 1aef01c48a1fd343a43e60ba3fd4ceb29dd4a30f Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Tue, 17 Jan 2012 21:56:48 -0600 Subject: [PATCH 12/77] pg8000, thanks Mariano --- VERSION | 2 +- gluon/dal.py | 48 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/VERSION b/VERSION index 74ae29e3..0daa0c3f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-17 21:50:01) stable +Version 1.99.4 (2012-01-17 21:56:37) stable diff --git a/gluon/dal.py b/gluon/dal.py index 2cf7f578..3445d3b7 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -95,7 +95,9 @@ 'sqlite:memory' 'jdbc:sqlite://test.db' 'mysql://root:none@localhost/test' -'postgres://mdipierro:none@localhost/test' +'postgres://mdipierro:password@localhost/test' +'postgres:psycopg2://mdipierro:password@localhost/test' +'postgres:pg8000://mdipierro:password@localhost/test' 'jdbc:postgres://mdipierro:none@localhost/test' 'mssql://web2py:none@A64X2/web2py_test' 'mssql2://web2py:none@A64X2/web2py_test' # alternate mappings @@ -241,10 +243,20 @@ def web2py_uuid(): return str(uuid.uuid4()) try: import psycopg2 from psycopg2.extensions import adapt as psycopg2_adapt - drivers.append('PostgreSQL') + drivers.append('psycopg2') except ImportError: logger.debug('no psycopg2 driver') + try: + # first try contrib driver, then from site-packages (if installed) + try: + import contrib.pg8000.dbapi as pg8000 + except ImportError: + import pg8000.dbapi as pg8000 + drivers.append('pg8000') + except ImportError: + logger.debug('no pg8000 driver') + try: import cx_Oracle drivers.append('Oracle') @@ -918,7 +930,7 @@ def insert(self, table, fields): self.execute(query) except Exception, e: if isinstance(e,self.integrity_error_class()): - return None + return None raise e if hasattr(table,'_primarykey'): return dict([(k[0].name, k[1]) for k in fields \ @@ -1855,7 +1867,9 @@ def lastrowid(self,table): class PostgreSQLAdapter(BaseAdapter): - driver = globals().get('psycopg2',None) + driver = None + drivers = {'psycopg2': globals().get('psycopg2', None), + 'pg8000': globals().get('pg8000', None), } support_distributed_transaction = True types = { @@ -1909,8 +1923,8 @@ def create_sequence_and_triggers(self, query, table, **args): def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, adapter_args={}): - if not self.driver: - raise RuntimeError, "Unable to import driver" + if not self.drivers.get('psycopg2') and not self.drivers.get('psycopg2'): + raise RuntimeError, "Unable to import any drivers (psycopg2 or pg8000)" self.db = db self.dbengine = "postgres" self.uri = uri @@ -1918,7 +1932,7 @@ def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', self.folder = folder self.db_codec = db_codec self.find_or_make_work_folder() - uri = uri.split('://')[1] + library, uri = uri.split('://')[:2] m = re.compile('^(?P[^:@]+)(\:(?P[^@]*))?@(?P[^\:@/]+)(\:(?P[0-9]+))?/(?P[^\?]+)(\?sslmode=(?P.+))?$').match(uri) if not m: raise SyntaxError, "Invalid URI string in DAL" @@ -1937,13 +1951,27 @@ def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', port = m.group('port') or '5432' sslmode = m.group('sslmode') if sslmode: - msg = ("dbname='%s' user='%s' host='%s'" + msg = ("dbname='%s' user='%s' host='%s' " "port=%s password='%s' sslmode='%s'") \ % (db, user, host, port, password, sslmode) else: - msg = ("dbname='%s' user='%s' host='%s'" + msg = ("dbname='%s' user='%s' host='%s' " "port=%s password='%s'") \ % (db, user, host, port, password) + # choose diver according uri + if library == "postgres": + if self.drivers.get('psycopg2'): + self.driver = self.drivers['psycopg2'] + elif self.drivers.get('pg8000'): + self.driver = drivers['pg8000'] + elif library == "postgres:psycopg2": + self.driver = self.drivers.get('psycopg2') + elif library == "postgres:pg8000": + self.driver = self.drivers.get('pg8000') + if not self.driver: + raise RuntimeError, "%s is not available" % library + + self.__version__ = "%s %s" % (self.driver.__name__, self.driver.__version__) def connect(msg=msg,driver_args=driver_args): return self.driver.connect(msg,**driver_args) self.pool_connection(connect) @@ -4808,6 +4836,8 @@ def NOT(self, first): 'sqlite:memory': SQLiteAdapter, 'mysql': MySQLAdapter, 'postgres': PostgreSQLAdapter, + 'postgres:psycopg2': PostgreSQLAdapter, + 'postgres:pg8000': PostgreSQLAdapter, 'oracle': OracleAdapter, 'mssql': MSSQLAdapter, 'mssql2': MSSQL2Adapter, From 984294de756d300dffb12300f7a3d9a50f2597e2 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Tue, 17 Jan 2012 22:03:06 -0600 Subject: [PATCH 13/77] issues 620 and 616, customizable function name in auth, thanks Bruno --- VERSION | 2 +- gluon/tools.py | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/VERSION b/VERSION index 0daa0c3f..657a16dd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-17 21:56:37) stable +Version 1.99.4 (2012-01-17 22:02:58) stable diff --git a/gluon/tools.py b/gluon/tools.py index fa974359..cd4d9e2a 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -832,7 +832,7 @@ def here(self): return URL(args=current.request.args,vars=current.request.vars) def __init__(self, environment=None, db=None, mailer=True, - hmac_key=None, controller='default', cas_provider=None): + hmac_key=None, controller='default', function='user', cas_provider=None): """ auth=Auth(db) @@ -890,8 +890,9 @@ def __init__(self, environment=None, db=None, mailer=True, settings.create_user_groups = True settings.controller = controller - settings.login_url = self.url('http://github.com/user',%20args='login') - settings.logged_url = self.url('http://github.com/user',%20args='profile') + settings.function = function + settings.login_url = self.url(http://github.com/function,%20args='login') + settings.logged_url = self.url(http://github.com/function,%20args='profile') settings.download_url = self.url('http://github.com/download') settings.mailer = (mailer==True) and Mail() or mailer settings.login_captcha = None @@ -905,7 +906,7 @@ def __init__(self, environment=None, db=None, mailer=True, settings.allow_basic_login = False settings.allow_basic_login_only = False settings.on_failed_authorization = \ - self.url('http://github.com/user',args='not_authorized') + self.url(http://github.com/function,%20args='not_authorized') settings.on_failed_authentication = lambda x: redirect(x) @@ -954,7 +955,7 @@ def __init__(self, environment=None, db=None, mailer=True, settings.register_fields = None settings.register_verify_password = True - settings.verify_email_next = self.url('http://github.com/user',%20args='login') + settings.verify_email_next = self.url(http://github.com/function,%20args='login') settings.verify_email_onaccept = [] settings.profile_next = self.url('http://github.com/index') @@ -963,8 +964,8 @@ def __init__(self, environment=None, db=None, mailer=True, settings.profile_fields = None settings.retrieve_username_next = self.url('http://github.com/index') settings.retrieve_password_next = self.url('http://github.com/index') - settings.request_reset_password_next = self.url('http://github.com/user',%20args='login') - settings.reset_password_next = self.url('http://github.com/user',%20args='login') + settings.request_reset_password_next = self.url(http://github.com/function,%20args='login') + settings.reset_password_next = self.url(http://github.com/function,%20args='login') settings.change_password_next = self.url('http://github.com/index') settings.change_password_onvalidation = [] @@ -1155,7 +1156,7 @@ def navbar(self, prefix='Welcome', action=None, separators=(' [ ',' | ',' ] ')): if isinstance(prefix,str): prefix = T(prefix) if not action: - action=self.url('http://github.com/user') + action=self.url(http://github.com/self.settings.function) if prefix: prefix = prefix.strip()+' ' s1,s2,s3 = separators @@ -1763,7 +1764,7 @@ def login( return cas.login_form() else: # we need to pass through login again before going on - next = self.url('http://github.com/user',args='login') + next = self.url(http://github.com/self.settings.function,%20args='login') redirect(cas.login_url(http://github.com/next)) # process authenticated users @@ -4105,3 +4106,4 @@ def __contains__(self,key): import doctest doctest.testmod() + From 2640f532ac9df35e5f362c93afc687ae4f27795e Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Tue, 17 Jan 2012 22:09:59 -0600 Subject: [PATCH 14/77] issue 621, formstyle support in grid, thanks mweissen --- VERSION | 2 +- gluon/sqlhtml.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 657a16dd..c097f461 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-17 22:02:58) stable +Version 1.99.4 (2012-01-17 22:09:51) stable diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 587d3b64..be4fcc8f 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1564,7 +1564,7 @@ def buttons(edit=False,view=False,record=None): table = db[request.args[-2]] record = table(request.args[-1]) or redirect(URL('error')) form = SQLFORM(table,record,upload=upload,ignore_rw=ignore_rw, - readonly=True,_class='web2py_form') + formstyle=formstyle, readonly=True,_class='web2py_form') res = DIV(buttons(edit=editable,record=record),form, formfooter,_class=_class) res.create_form = None @@ -1576,7 +1576,7 @@ def buttons(edit=False,view=False,record=None): table = db[request.args[-2]] record = table(request.args[-1]) or redirect(URL('error')) edit_form = SQLFORM(table,record,upload=upload,ignore_rw=ignore_rw, - deletable=deletable, + formstyle=formstyle,deletable=deletable, _class='web2py_form', submit_button = T('Submit'), delete_label = T('Check to delete')) From ddb4205ed193f29e88d4d6b97219b008a450f023 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Fri, 20 Jan 2012 11:19:47 -0600 Subject: [PATCH 15/77] testing possible solution to issue 619 --- VERSION | 2 +- gluon/sqlhtml.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index c097f461..f2ee5998 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-17 22:09:51) stable +Version 1.99.4 (2012-01-20 11:19:18) stable diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index be4fcc8f..9acda6f3 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1956,12 +1956,13 @@ def index(): else: del kwargs[key] for tablename,fieldname in table._referenced_by: + id_field_name = db[tablename]._id.name if linked_tables is None or tablename in linked_tables: args0 = tablename+'.'+fieldname links.append( lambda row,t=T(db[tablename]._plural),nargs=nargs,args0=args0:\ A(SPAN(t),_class=trap_class(),_href=URL( - args=request.args[:nargs]+[args0,row.id]))) + args=request.args[:nargs]+[args0,row[id_field_name]]))) grid=SQLFORM.grid(query,args=request.args[:nargs],links=links, links_in_grid=links_in_grid, user_signature=user_signature,**kwargs) From 1356e5b73f3910b05bc3b8fa702aa356abf816dd Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Fri, 20 Jan 2012 11:23:15 -0600 Subject: [PATCH 16/77] populate can now deal with computed fields, thanks ttmost --- VERSION | 2 +- gluon/contrib/populate.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index f2ee5998..34d96c41 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-20 11:19:18) stable +Version 1.99.4 (2012-01-20 11:23:12) stable diff --git a/gluon/contrib/populate.py b/gluon/contrib/populate.py index ade6e0f8..c1494dfd 100644 --- a/gluon/contrib/populate.py +++ b/gluon/contrib/populate.py @@ -55,7 +55,7 @@ def da_du_ma(n=4): 'pa','po','sa','so','ta','to']\ [random.randint(0,11)] for i in range(n)]) -def populate(table, n, default=True): +def populate(table, n, default=True, compute=False): ell=Learner() #ell.learn(open('20417.txt','r').read()) #ell.save('frequencies.pickle') @@ -72,6 +72,8 @@ def populate(table, n, default=True): continue elif default and field.default: record[fieldname]=field.default + elif compute and field.compute: + continue elif field.type == 'text': record[fieldname]=ell.generate(random.randint(10,100),prefix=None) elif field.type == 'boolean': From 6837da09173c0704221e1403a263ed2a1b0a4029 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Fri, 20 Jan 2012 11:36:42 -0600 Subject: [PATCH 17/77] issue 626 fixes unicode and truncattion, thanks mweissen --- VERSION | 2 +- gluon/html.py | 6 ++++++ gluon/sqlhtml.py | 22 +++++++++------------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/VERSION b/VERSION index 34d96c41..cf5a969d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-20 11:23:12) stable +Version 1.99.4 (2012-01-20 11:36:35) stable diff --git a/gluon/html.py b/gluon/html.py index 83c485cd..ee98d17b 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -124,6 +124,12 @@ def xmlescape(data, quote = True): return data +def truncate_string(text, length, dots='...'): + text = text.decode('utf-8') + if len(text)>length: + text = text[:length-len(dots)].encode('utf-8')+dots + return text + def URL( a=None, c=None, diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 9acda6f3..1fa2a48c 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -18,7 +18,7 @@ from html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG, SCRIPT from html import FORM, INPUT, LABEL, OPTION, SELECT, MENU from html import TABLE, THEAD, TBODY, TR, TD, TH -from html import URL +from html import URL, truncate_string from dal import DAL, Table, Row, CALLABLETYPES, smart_query from storage import Storage from utils import md5_hash @@ -1800,11 +1800,11 @@ def self_link(name,p): value = A('File', _href='http://github.com/%s/%s' % (upload, value)) else: - value = '' - elif isinstance(value,str) and len(value)>maxlength: - value=value[:maxlength]+'...' + value = '' + elif isinstance(value,str): + value = truncate_string(value,maxlength) else: - value=field.formatter(value) + value = field.formatter(value) tr.append(TD(value)) row_buttons = TD(_class='row_buttons') if links and links_in_grid: @@ -2195,16 +2195,12 @@ def __init__( r = '' elif field.type in ['string','text']: r = str(field.formatter(r)) - ur = unicode(r, 'utf8') if headers!={}: #new implement dict if isinstance(headers[colname],dict): - if isinstance(headers[colname]['truncate'], int) \ - and len(ur)>headers[colname]['truncate']: - r = ur[:headers[colname]['truncate'] - 3] - r = r.encode('utf8') + '...' - elif not truncate is None and len(ur) > truncate: - r = ur[:truncate - 3].encode('utf8') + '...' - + if isinstance(headers[colname]['truncate'], int): + r = truncate_string(r, headers[colname]['truncate']) + elif not truncate is None: + r = truncate_string(r, truncate) attrcol = dict()#new implement dict if headers!={}: if isinstance(headers[colname],dict): From 02bd396c03362c2af2dae80ca140abb569bd823a Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sat, 21 Jan 2012 16:59:44 -0600 Subject: [PATCH 18/77] another change to better support arbitrary named id fields --- VERSION | 2 +- gluon/validators.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index cf5a969d..2adfbcf5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-20 11:36:35) stable +Version 1.99.4 (2012-01-21 16:59:06) stable diff --git a/gluon/validators.py b/gluon/validators.py index 36a34ccb..515cbce4 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -546,7 +546,7 @@ def __call__(self, value): for f in self.record_id: if str(getattr(rows[0], f)) != str(self.record_id[f]): return (value, translate(self.error_message)) - elif str(rows[0].id) != str(self.record_id): + elif str(rows[0]._id) != str(self.record_id): return (value, translate(self.error_message)) return (value, None) From 1ee695041dd776d216d59deff3788306811934ff Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sat, 21 Jan 2012 17:22:32 -0600 Subject: [PATCH 19/77] issue 623, thanks Gergely Peli --- VERSION | 2 +- applications/admin/static/js/web2py.js | 6 ++++-- applications/examples/static/js/web2py.js | 6 ++++-- applications/welcome/static/js/web2py.js | 6 ++++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/VERSION b/VERSION index 2adfbcf5..b41da809 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-21 16:59:06) stable +Version 1.99.4 (2012-01-21 17:22:25) stable diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index 0b8dbf5b..ec9599fc 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -87,8 +87,10 @@ function web2py_ajax_page(method,action,data,target) { web2py_trap_form(action,target); web2py_trap_link(target); web2py_ajax_init('#'+target); - if(command) eval(command); - if(flash) jQuery('.flash').html(flash).slideDown(); + if(command) + eval(decodeURIComponent(escape(command))); + if(flash) + jQuery('.flash').html(decodeURIComponent(escape(flash))).slideDown(); } }); } diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js index 0b8dbf5b..ec9599fc 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -87,8 +87,10 @@ function web2py_ajax_page(method,action,data,target) { web2py_trap_form(action,target); web2py_trap_link(target); web2py_ajax_init('#'+target); - if(command) eval(command); - if(flash) jQuery('.flash').html(flash).slideDown(); + if(command) + eval(decodeURIComponent(escape(command))); + if(flash) + jQuery('.flash').html(decodeURIComponent(escape(flash))).slideDown(); } }); } diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index 0b8dbf5b..ec9599fc 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -87,8 +87,10 @@ function web2py_ajax_page(method,action,data,target) { web2py_trap_form(action,target); web2py_trap_link(target); web2py_ajax_init('#'+target); - if(command) eval(command); - if(flash) jQuery('.flash').html(flash).slideDown(); + if(command) + eval(decodeURIComponent(escape(command))); + if(flash) + jQuery('.flash').html(decodeURIComponent(escape(flash))).slideDown(); } }); } From e385065b2017897e4980cbcdb4da074a55486164 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sat, 21 Jan 2012 17:23:47 -0600 Subject: [PATCH 20/77] issue 628, thanks hi21alt --- VERSION | 2 +- gluon/tools.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index b41da809..cab4faca 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-21 17:22:25) stable +Version 1.99.4 (2012-01-21 17:23:43) stable diff --git a/gluon/tools.py b/gluon/tools.py index cd4d9e2a..0b8560cc 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -2840,7 +2840,7 @@ def archive(form, current_record='parent_record')) """ - if archive_current and not form.record: + if not archive_current and not form.record: return None table = form.table if not archive_table: From b03c40f4420caa302597e545d49d9a99f790f42c Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sat, 21 Jan 2012 17:26:12 -0600 Subject: [PATCH 21/77] issue 629, thanks makitalo --- VERSION | 2 +- gluon/sqlhtml.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index cab4faca..f7d816aa 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-21 17:23:43) stable +Version 1.99.4 (2012-01-21 17:26:09) stable diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 1fa2a48c..3491fa5a 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1495,6 +1495,7 @@ def gridbutton(buttonclass='buttonadd',buttontext='Add', _class=trap_class(ui.get('buttontext',''),trap)) dbset = db(query) tablenames = db._adapter.tables(dbset.query) + if left!=None: tablenames+=db._adapter.tables(left) tables = [db[tablename] for tablename in tablenames] if not fields: fields = reduce(lambda a,b:a+b, From 44c7f576e536ef6c2ba8bbd7c7475170e668735d Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sat, 21 Jan 2012 17:28:51 -0600 Subject: [PATCH 22/77] issue 630, encoding and plain emails, thanks spametki --- VERSION | 2 +- gluon/tools.py | 43 ++++++++++++++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/VERSION b/VERSION index f7d816aa..7dcb07fe 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-21 17:26:09) stable +Version 1.99.4 (2012-01-21 17:28:44) stable diff --git a/gluon/tools.py b/gluon/tools.py index 0b8560cc..bf74b837 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -241,6 +241,7 @@ def send( bcc=None, reply_to=None, encoding='utf-8', + raw=False, headers={} ): """ @@ -320,11 +321,29 @@ def encode_header(key): else: return key + # encoded or raw text + def encoded_or_raw(text): + if raw: + text = encode_header(text) + return text + if not isinstance(self.settings.server, str): raise Exception('Server address not specified') if not isinstance(self.settings.sender, str): raise Exception('Sender address not specified') - payload_in = MIMEMultipart.MIMEMultipart('mixed') + + if not raw: + payload_in = MIMEMultipart.MIMEMultipart('mixed') + else: + # no encoding configuration for raw messages + if isinstance(message, basestring): + text = message.decode(encoding).encode('utf-8') + else: + text = message.read().decode(encoding).encode('utf-8') + # No charset passed to avoid transport encoding + # NOTE: some unicode encoded strings will produce + # unreadable mail contents. + payload_in = MIMEText.MIMEText(text) if to: if not isinstance(to, (list,tuple)): to = [to] @@ -346,7 +365,8 @@ def encode_header(key): else: text = message html = None - if not text is None or not html is None: + + if (not text is None or not html is None) and (not raw): attachment = MIMEMultipart.MIMEMultipart('alternative') if not text is None: if isinstance(text, basestring): @@ -361,7 +381,7 @@ def encode_header(key): html = html.read().decode(encoding).encode('utf-8') attachment.attach(MIMEText.MIMEText(html, 'html',_charset='utf-8')) payload_in.attach(attachment) - if attachments is None: + if (attachments is None) or raw: pass elif isinstance(attachments, (list, tuple)): for attachment in attachments: @@ -546,22 +566,23 @@ def encode_header(key): else: # no cryptography process as usual payload=payload_in - payload['From'] = encode_header(self.settings.sender.decode(encoding)) + + payload['From'] = encoded_or_raw(self.settings.sender.decode(encoding)) origTo = to[:] if to: - payload['To'] = encode_header(', '.join(to).decode(encoding)) + payload['To'] = encoded_or_raw(', '.join(to).decode(encoding)) if reply_to: - payload['Reply-To'] = encode_header(reply_to.decode(encoding)) + payload['Reply-To'] = encoded_or_raw(reply_to.decode(encoding)) if cc: - payload['Cc'] = encode_header(', '.join(cc).decode(encoding)) + payload['Cc'] = encoded_or_raw(', '.join(cc).decode(encoding)) to.extend(cc) if bcc: to.extend(bcc) - payload['Subject'] = encode_header(subject.decode(encoding)) + payload['Subject'] = encoded_or_raw(subject.decode(encoding)) payload['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()) for k,v in headers.iteritems(): - payload[k] = encode_header(v.decode(encoding)) + payload[k] = encoded_or_raw(v.decode(encoding)) result = {} try: if self.settings.server == 'logging': @@ -576,12 +597,12 @@ def encode_header(key): if bcc: xcc['bcc'] = bcc from google.appengine.api import mail - attachments = attachments and [(a.my_filename,a.my_payload) for a in attachments] + 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, subject=subject, body=text, html=html, attachments=attachments, **xcc) - elif html: + elif html and (not raw): result = mail.send_mail(sender=self.settings.sender, to=origTo, subject=subject, body=text, html=html, **xcc) else: From 7ea1dd78c960ed940410e7e0c43e735f88ddc656 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sat, 21 Jan 2012 17:32:54 -0600 Subject: [PATCH 23/77] issue 631, double :: after click to delete, thanks hi21alt --- VERSION | 2 +- gluon/tools.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 7dcb07fe..ad5f7731 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-21 17:28:44) stable +Version 1.99.4 (2012-01-21 17:32:51) stable diff --git a/gluon/tools.py b/gluon/tools.py index bf74b837..ff659050 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1007,7 +1007,7 @@ def __init__(self, environment=None, db=None, mailer=True, messages.profile_save_button = 'Save profile' messages.submit_button = 'Submit' messages.verify_password = 'Verify Password' - messages.delete_label = 'Check to delete:' + messages.delete_label = 'Check to delete' messages.function_disabled = 'Function disabled' messages.access_denied = 'Insufficient privileges' messages.registration_verifying = 'Registration needs verification' From 64a97cd040782dfacec785aa2e721d8ac633dd13 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sun, 22 Jan 2012 16:09:23 -0600 Subject: [PATCH 24/77] postgresql string concatenation --- VERSION | 2 +- gluon/dal.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index ad5f7731..b6093104 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-21 17:32:51) stable +Version 1.99.4 (2012-01-22 16:08:57) stable diff --git a/gluon/dal.py b/gluon/dal.py index 3445d3b7..a0ddefa6 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -1901,6 +1901,13 @@ def sequence_name(self,table): def RANDOM(self): return 'RANDOM()' + def ADD(self, first, second): + t = first.type + if t in ('text','string','password','upload','blob'): + return '(%s || %s)' % (self.expand(first), self.expand(second, t)) + else: + return '(%s + %s)' % (self.expand(first), self.expand(second, t)) + def distributed_transaction_begin(self,key): return From 4c8d978977bf5bd9e4d20cefc6ac37587db57d06 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 23 Jan 2012 08:26:28 -0600 Subject: [PATCH 25/77] improved IMAP adapter, thanks spametki --- VERSION | 2 +- gluon/dal.py | 504 +++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 410 insertions(+), 96 deletions(-) diff --git a/VERSION b/VERSION index b6093104..33edee66 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-22 16:08:57) stable +Version 1.99.4 (2012-01-23 08:26:04) stable diff --git a/gluon/dal.py b/gluon/dal.py index a0ddefa6..8c50a748 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -4286,24 +4286,103 @@ def COMMA(self, first, second): class IMAPAdapter(NoSQLAdapter): """ IMAP server adapter - + This class is intended as an interface with email IMAP servers to perform simple queries in the web2py DAL query syntax, so email read, search and other related IMAP mail services (as those implemented - by brands like Google(r), Hotmail(r) and Yahoo!(r) + by brands like Google(r), and Yahoo!(r) can be managed from web2py applications. The code uses examples by Yuji Tomita on this post: http://yuji.wordpress.com/2011/06/22/python-imaplib-imap-example-with-gmail/#comment-1137 - - And IMAP docs for Python imaplib and IETF's RFC2060 + and is based in docs for Python imaplib, python email + and email IETF's (i.e. RFC2060 and RFC3501) This adapter was tested with a small set of operations with Gmail(r). Other services requests could raise command syntax and response data issues. + It creates its table and field names "statically", + meaning that the developer should leave the table and field + definitions to the DAL instance by calling the adapter's + .define_tables() method. The tables are defined with the + IMAP server mailbox list information. + + Here is a list of supported fields: + + Field Type Description + ################################################################ + uid string + answered boolean Flag + created date + content list:string A list of text or html parts + to string + cc string + bcc string + size integer the amount of octets of the message* + deleted boolean Flag + draft boolean Flag + flagged boolean Flag + sender string + recent boolean Flag + seen boolean Flag + subject string + mime string The mime header declaration + email string The complete RFC822 message** + attachments list:string Each non text decoded part as string + + *At the application side it is measured as the length of the RFC822 + message string + + WARNING: As row id's are mapped to email sequence numbers, + make sure your imap client web2py app does not delete messages + during select or update actions, to prevent + updating or deleting different messages. + Sequence numbers change whenever the mailbox is updated. + To avoid this sequence numbers issues, it is recommended the use + of uid fields in query references (although the update and delete + in separate actions rule still applies). + + # This is the code recommended to start imap support + # at the app's model: + + imapdb = DAL("imap://user:password@server:port", pool_size=1) # port 993 for ssl + imapdb.define_tables() + + Here is an (incomplete) list of possible imap commands: + + # Count today's unseen messages + # smaller than 6000 octets from the + # inbox mailbox + + q = imapdb.INBOX.seen == False + q &= imapdb.INBOX.created == datetime.date.today() + q &= imapdb.INBOX.size < 6000 + unread = imapdb(q).count() + + # Fetch last query messages + rows = imapdb(q).select() + + # it is also possible to filter query select results with limitby and + # sequences of mailbox fields + + set.select(, limitby=(, )) + + # Mark last query messages as seen + messages = [row.uid for row in rows] + seen = imapdb(imapdb.INBOX.uid.belongs(messages)).update(seen=True) + + # Delete messages in the imap database that have mails from mr. Gumby + + deleted = 0 + for mailbox in imapdb.tables + deleted += imapdb(imapdb[mailbox].sender.contains("gumby")).delete() + + # It is possible also to mark messages for deletion instead of ereasing them + # directly with set.update(deleted=True) """ + types = { 'string': str, 'text': str, @@ -4313,9 +4392,11 @@ class IMAPAdapter(NoSQLAdapter): 'boolean': bool, 'integer': int, 'blob': str, + 'list:string': str, } dbengine = 'imap' + driver = globals().get('imaplib',None) def __init__(self, db, @@ -4326,41 +4407,41 @@ def __init__(self, credential_decoder=lambda x:x, driver_args={}, adapter_args={}): - + # db uri: user@example.com:password@imap.server.com:123 + # TODO: max size adapter argument for preventing large mail transfers + uri = uri.split("://")[1] self.db = db self.uri = uri - self.pool_size=0 + self.pool_size=pool_size self.folder = folder self.db_codec = db_codec self.credential_decoder = credential_decoder self.driver_args = driver_args self.adapter_args = adapter_args self.mailbox_size = None - self.mailbox_names = dict() - self.encoding = sys.getfilesystemencoding() + self.charset = sys.getfilesystemencoding() + # imap class + self.imap4 = None + """ MESSAGE is an identifier for sequence number""" + self.flags = ['\\Deleted', '\\Draft', '\\Flagged', + '\\Recent', '\\Seen', '\\Answered'] self.search_fields = { - 'id': 'MESSAGE', - 'created': 'DATE', - 'uid': 'UID', - 'sender': 'FROM', - 'to': 'TO', - 'content': 'TEXT', - 'deleted': '\\Deleted', - 'draft': '\\Draft', - 'flagged': '\\Flagged', - 'recent': '\\Recent', - 'seen': '\\Seen', - 'subject': 'SUBJECT', - 'answered': '\\Answered', - 'mime': None, - 'email': None, + 'id': 'MESSAGE', 'created': 'DATE', + 'uid': 'UID', 'sender': 'FROM', + 'to': 'TO', 'cc': 'CC', + 'bcc': 'BCC', 'content': 'TEXT', + 'size': 'SIZE', 'deleted': '\\Deleted', + 'draft': '\\Draft', 'flagged': '\\Flagged', + 'recent': '\\Recent', 'seen': '\\Seen', + 'subject': 'SUBJECT', 'answered': '\\Answered', + 'mime': None, 'email': None, 'attachments': None } - + db['_lastsql'] = '' m = re.compile('^(?P[^:]+)(\:(?P[^@]*))?@(?P[^\:@/]+)(\:(?P[0-9]+))?$').match(uri) @@ -4377,31 +4458,88 @@ def connect(driver_args=driver_args): # it is assumed sucessful authentication alLways # TODO: support direct connection and login tests if over_ssl: - imap4 = imaplib.IMAP4_SSL + self.imap4 = self.driver.IMAP4_SSL else: - imap4 = imaplib.IMAP4 - connection = imap4(driver_args["host"], driver_args["port"]) - connection.login(driver_args["user"], driver_args["password"]) + self.imap4 = self.driver.IMAP4 + connection = self.imap4(driver_args["host"], driver_args["port"]) + data = connection.login(driver_args["user"], driver_args["password"]) + # print "Connected to remote server" + # print data + # static mailbox list + connection.mailbox_names = None + + # dummy cursor function + connection.cursor = lambda : True + return connection - self.pool_connection(connect,cursor=False) + self.pool_connection(connect) self.db.define_tables = self.define_tables + def pool_connection(self, f, cursor=True): + """ + IMAP4 Pool connection method + + imap connection lacks of self cursor command. + A custom command should be provided as a replacement + for connection pooling to prevent uncaught remote session + closing + + """ + # print "Pool Connection" + if not self.pool_size: + self.connection = f() + self.cursor = cursor and self.connection.cursor() + else: + uri = self.uri + # print "uri", self.uri + while True: + sql_locker.acquire() + if not uri in ConnectionPool.pools: + ConnectionPool.pools[uri] = [] + if ConnectionPool.pools[uri]: + self.connection = ConnectionPool.pools[uri].pop() + sql_locker.release() + self.cursor = cursor and self.connection.cursor() + # print "self.cursor", self.cursor + if self.cursor and self.check_active_connection: + try: + # check if connection is alive or close it + result, data = self.connection.list() + # print "Checked connection" + # print result, data + # self.execute('SELECT 1;') + except: + # Possible connection reset error + # TODO: read exception class + # print "Re-connecting to IMAP server" + self.connection = f() + break + else: + sql_locker.release() + self.connection = f() + self.cursor = cursor and self.connection.cursor() + break + + if not hasattr(thread,'instances'): + thread.instances = [] + thread.instances.append(self) + def get_last_message(self, tablename): last_message = None # request mailbox list to the server # if needed - if not len(self.mailbox_names.keys()) > 0: + if not isinstance(self.connection.mailbox_names, dict): self.get_mailboxes() try: - result = self.connection.select(self.mailbox_names[tablename]) + result = self.connection.select(self.connection.mailbox_names[tablename]) last_message = int(result[1][0]) except (IndexError, ValueError, TypeError, KeyError), e: logger.debug("Error retrieving the last mailbox sequence number. %s" % str(e)) return last_message def get_uid_bounds(self, tablename): - if not len(self.mailbox_names.keys()) > 0: + if not isinstance(self.connection.mailbox_names, dict): self.get_mailboxes() # fetch first and last messages # return (first, last) messages uid's @@ -4444,17 +4582,32 @@ def convert_date(self, date, add=None): else: return None - def decode_text(self): - """ translate encoded text for mail to unicode""" - # not implemented - pass + def encode_text(self, text, charset, errors="replace"): + """ convert text for mail to unicode""" + if text is None: + text = "" + else: + if isinstance(text, str): + if charset is not None: + text = unicode(text, charset, errors) + else: + text = unicode(text, "utf-8", errors) + else: + raise Exception("Unsupported mail text type %s" % type(text)) + return text.encode("utf-8") def get_charset(self, message): charset = message.get_content_charset() return charset + def reset_mailboxes(self): + self.connection.mailbox_names = None + self.get_mailboxes() + def get_mailboxes(self): + """ Query the mail database for mailbox names """ mailboxes_list = self.connection.list() + self.connection.mailbox_names = dict() mailboxes = list() for item in mailboxes_list[1]: item = item.strip() @@ -4465,9 +4618,33 @@ def get_mailboxes(self): # remove unwanted characters and store original names mailbox_name = mailbox.replace("[", "").replace("]", "").replace("/", "_") mailboxes.append(mailbox_name) - self.mailbox_names[mailbox_name] = mailbox + self.connection.mailbox_names[mailbox_name] = mailbox + # print "Mailboxes query", mailboxes return mailboxes + def get_query_mailbox(self, query): + nofield = True + tablename = None + attr = query + while nofield: + if hasattr(attr, "first"): + attr = attr.first + if isinstance(attr, Field): + return attr.tablename + elif isinstance(attr, Query): + pass + else: + return None + else: + return None + return tablename + + def is_flag(self, flag): + if self.search_fields.get(flag, None) in self.flags: + return True + else: + return False + def define_tables(self): """ Auto create common IMAP fileds @@ -4477,24 +4654,29 @@ def define_tables(self): not be supported and definitions handled on a service/mode basis (local syntax for Gmail(r), Ymail(r) """ - mailboxes = self.get_mailboxes() + if not isinstance(self.connection.mailbox_names, dict): + self.get_mailboxes() + mailboxes = self.connection.mailbox_names.keys() for mailbox_name in mailboxes: self.db.define_table("%s" % mailbox_name, Field("uid", "string", writable=False), - Field("answered", "boolean", writable=False), + Field("answered", "boolean"), Field("created", "datetime", writable=False), - Field("content", "list:text", writable=False), + Field("content", "list:string", writable=False), Field("to", "string", writable=False), - Field("deleted", "boolean", writable=False), - Field("draft", "boolean", writable=False), - Field("flagged", "boolean", writable=False), + Field("cc", "string", writable=False), + Field("bcc", "string", writable=False), + Field("size", "integer", writable=False), + Field("deleted", "boolean"), + Field("draft", "boolean"), + Field("flagged", "boolean"), Field("sender", "string", writable=False), Field("recent", "boolean", writable=False), - Field("seen", "boolean", writable=False), + Field("seen", "boolean"), Field("subject", "string", writable=False), Field("mime", "string", writable=False), - Field("email", "text", writable=False), - Field("attachments", "list:text", writable=False), + Field("email", "string", writable=False, readable=False), + Field("attachments", "list:string", writable=False, readable=False), ) def create_table(self, *args, **kwargs): @@ -4506,6 +4688,9 @@ def _select(self,query,fields,attributes): rows """ + if not query.ignore_common_filters: + query = self.common_filter(query, [self.get_query_mailbox(query),]) + # move this statement elsewhere (upper-level) import email import email.header @@ -4513,9 +4698,10 @@ def _select(self,query,fields,attributes): # get records from imap server with search + fetch # convert results to a dictionary tablename = None + fetch_results = list() if isinstance(query, (Expression, Query)): tablename = self.get_table(query) - mailbox = self.mailbox_names.get(tablename, None) + mailbox = self.connection.mailbox_names.get(tablename, None) if isinstance(query, Expression): pass elif isinstance(query, Query): @@ -4524,15 +4710,25 @@ def _select(self,query,fields,attributes): selected = self.connection.select(mailbox, True) self.mailbox_size = int(selected[1][0]) search_query = "(%s)" % str(query).strip() + # print "Query", query + # print "Search query", search_query search_result = self.connection.uid("search", None, search_query) + # print "Search result", search_result + # print search_result # Normal IMAP response OK is assumed (change this) if search_result[0] == "OK": - fetch_results = list() # 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. - messages_set = search_result[1][0].split()[:10] + # 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) @@ -4543,19 +4739,35 @@ def _select(self,query,fields,attributes): # (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) - fr = {"message": int(data[0][0].split()[0]), - "uid": int(uid), - "email": email.message_from_string(data[0][1]) - } - fr["multipart"] = fr["email"].is_multipart() - fetch_results.append(fr) + 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": + # print "Raw flags", fdata + fr["flags"] = self.driver.ParseFlags(fdata[0]) + # print "Flags", fr["flags"] + fetch_results.append(fr) + else: + # error retrieving the flags for this message + pass + else: + # error retrieving the message body + pass elif isinstance(query, basestring): + # not implemented pass else: pass - + imapqry_dict = {} imapfields_dict = {} @@ -4576,11 +4788,16 @@ def _select(self,query,fields,attributes): imapqry_list = list() imapqry_array = list() for fr in fetch_results: + attachments = [] + content = [] + size = 0 n = int(fr["message"]) item_dict = dict() message = fr["email"] uid = fr["uid"] charset = self.get_charset(message) + flags = fr["flags"] + raw_message = fr["raw_message"] # Return messages data mapping static fields # and fetched results. Mapping should be made # outside the select function (with auxiliary @@ -4588,7 +4805,7 @@ def _select(self,query,fields,attributes): # pending: search flags states trough the email message # instances for correct output - + if "%s.id" % tablename in fieldnames: item_dict["%s.id" % tablename] = n if "%s.created" % tablename in fieldnames: @@ -4598,57 +4815,75 @@ def _select(self,query,fields,attributes): if "%s.sender" % tablename in fieldnames: # If there is no encoding found in the message header # force utf-8 replacing characters (change this to - # module's defaults). Applies to .sender and .to fields - if charset is not None: - item_dict["%s.sender" % tablename] = unicode(message["From"], charset, "replace") - else: - item_dict["%s.sender" % tablename] = unicode(message["From"], "utf-8", "replace") + # module's defaults). Applies to .sender, .to, .cc and .bcc fields + ############################################################################# + # TODO: External function to manage encoding and decoding of message strings + ############################################################################# + item_dict["%s.sender" % tablename] = self.encode_text(message["From"], charset) if "%s.to" % tablename in fieldnames: - if charset is not None: - item_dict["%s.to" % tablename] = unicode(message["To"], charset, "replace") + item_dict["%s.to" % tablename] = self.encode_text(message["To"], charset) + if "%s.cc" % tablename in fieldnames: + if "Cc" in message.keys(): + # print "cc field found" + item_dict["%s.cc" % tablename] = self.encode_text(message["Cc"], charset) else: - item_dict["%s.to" % tablename] = unicode(message["To"], "utf-8", "replace") - if "%s.content" % tablename in fieldnames: - content = [] - for part in message.walk(): - if "text" in part.get_content_maintype(): - payload = part.get_payload(decode=True) - content.append(payload) - item_dict["%s.content" % tablename] = content + item_dict["%s.cc" % tablename] = "" + if "%s.bcc" % tablename in fieldnames: + if "Bcc" in message.keys(): + # print "bcc field found" + item_dict["%s.bcc" % tablename] = self.encode_text(message["Bcc"], charset) + else: + item_dict["%s.bcc" % tablename] = "" if "%s.deleted" % tablename in fieldnames: - item_dict["%s.deleted" % tablename] = None + item_dict["%s.deleted" % tablename] = "\\Deleted" in flags if "%s.draft" % tablename in fieldnames: - item_dict["%s.draft" % tablename] = None + item_dict["%s.draft" % tablename] = "\\Draft" in flags if "%s.flagged" % tablename in fieldnames: - item_dict["%s.flagged" % tablename] = None + item_dict["%s.flagged" % tablename] = "\\Flagged" in flags if "%s.recent" % tablename in fieldnames: - item_dict["%s.recent" % tablename] = None + item_dict["%s.recent" % tablename] = "\\Recent" in flags if "%s.seen" % tablename in fieldnames: - item_dict["%s.seen" % tablename] = None + item_dict["%s.seen" % tablename] = "\\Seen" in flags if "%s.subject" % tablename in fieldnames: subject = message["Subject"] decoded_subject = decode_header(subject) text = decoded_subject[0][0] encoding = decoded_subject[0][1] - if encoding is not None: - text = unicode(text, encoding) - item_dict["%s.subject" % tablename] = text + if encoding in (None, ""): + encoding = charset + item_dict["%s.subject" % tablename] = self.encode_text(text, encoding) if "%s.answered" % tablename in fieldnames: - item_dict["%s.answered" % tablename] = None + 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 + # Here goes the whole RFC822 body as an email instance # for controller side custom processing + # The message is stored as a raw string + # >> email.message_from_string(raw string) + # returns a Message object for enhanced object processing if "%s.email" % tablename in fieldnames: - item_dict["%s.email" % tablename] = message - - if "%s.attachments" % tablename in fieldnames: - attachments = [] - for part in message.walk(): + 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 + # To retrieve the server size for representation would add a new + # fetch transaction to the process + for part in message.walk(): + if "%s.attachments" % tablename in fieldnames: if not "text" in part.get_content_maintype(): attachments.append(part.get_payload(decode=True)) - item_dict["%s.attachments" % tablename] = attachments + if "%s.content" % tablename in fieldnames: + if "text" in part.get_content_maintype(): + payload = self.encode_text(part.get_payload(decode=True), charset) + content.append(payload) + 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 @@ -4665,13 +4900,88 @@ def select(self,query,fields,attributes): tablename, imapqry_array , fieldnames = self._select(query,fields,attributes) # parse result and return a rows object colnames = fieldnames - result = self.parse(imapqry_array, colnames) + result = self.parse(imapqry_array, fields, colnames) return result + def update(self, tablename, query, fields): + # print "_update" + + if not query.ignore_common_filters: + query = self.common_filter(query, [tablename,]) + + mark = [] + unmark = [] + rowcount = 0 + query = str(query) + if query: + for item in fields: + field = item[0] + name = field.name + value = item[1] + if self.is_flag(name): + flag = self.search_fields[name] + if (value is not None) and (flag != "\\Recent"): + if value: + mark.append(flag) + else: + unmark.append(flag) + + # print "Selecting mailbox ..." + result, data = self.connection.select(self.connection.mailbox_names[tablename]) + # print "Retrieving sequence numbers remotely" + string_query = "(%s)" % query + # print "string query", string_query + result, data = self.connection.search(None, string_query) + store_list = [item.strip() for item in data[0].split() if item.strip().isdigit()] + # print "Storing values..." + # change marked flags + for number in store_list: + result = None + if len(mark) > 0: + # print "Marking flags ..." + result, data = self.connection.store(number, "+FLAGS", "(%s)" % " ".join(mark)) + if len(unmark) > 0: + # print "Unmarking flags ..." + result, data = self.connection.store(number, "-FLAGS", "(%s)" % " ".join(unmark)) + if result == "OK": + rowcount += 1 + return rowcount + def count(self,query,distinct=None): - # not implemented - # (count search results without select call) - pass + counter = 0 + tablename = self.get_query_mailbox(query) + if query and tablename is not None: + if not query.ignore_common_filters: + query = self.common_filter(query, [tablename,]) + # print "Selecting mailbox ..." + result, data = self.connection.select(self.connection.mailbox_names[tablename]) + # print "Retrieving sequence numbers remotely" + 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()] + counter = len(store_list) + return counter + + def delete(self, tablename, query): + counter = 0 + if query: + # print "Selecting mailbox ..." + if not query.ignore_common_filters: + query = self.common_filter(query, [tablename,]) + result, data = self.connection.select(self.connection.mailbox_names[tablename]) + # print "Retrieving sequence numbers remotely" + 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()] + for number in store_list: + result, data = self.connection.store(number, "+FLAGS", "(\\Deleted)") + # print "Deleting message", result, data + if result == "OK": + counter += 1 + if counter > 0: + # print "Ereasing permanently" + result, data = self.connection.expunge() + return counter def BELONGS(self, first, second): result = None @@ -4726,6 +5036,8 @@ def GT(self, first, second): result = "UID %s:%s" % (lower_limit, threshold) elif name == "DATE": result = "SINCE %s" % self.convert_date(second, add=datetime.timedelta(1)) + elif name == "SIZE": + result = "LARGER %s" % self.expand(second) else: raise Exception("Operation not supported") return result @@ -4771,6 +5083,8 @@ def LT(self, first, second): result = "UID %s:%s" % (pedestal, upper_limit) elif name == "DATE": result = "BEFORE %s" % self.convert_date(second) + elif name == "SIZE": + result = "SMALLER %s" % self.expand(second) else: raise Exception("Operation not supported") return result @@ -4810,8 +5124,8 @@ def EQ(self,first,second): result = "UID %s" % self.expand(second) elif name == "DATE": result = "ON %s" % self.convert_date(second) - - elif name in ('\\Deleted', '\\Draft', '\\Flagged', '\\Recent', '\\Seen', '\\Answered'): + + elif name in self.flags: if second: result = "%s" % (name.upper()[1:]) else: From 000f16744af52060ae8baccfd092842cb061e24d Mon Sep 17 00:00:00 2001 From: bulatsh Date: Tue, 24 Jan 2012 00:09:00 +0400 Subject: [PATCH 26/77] added russian translation for admin application --- applications/admin/languages/ru-ru.py | 343 ++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 applications/admin/languages/ru-ru.py diff --git a/applications/admin/languages/ru-ru.py b/applications/admin/languages/ru-ru.py new file mode 100644 index 00000000..5bf12c10 --- /dev/null +++ b/applications/admin/languages/ru-ru.py @@ -0,0 +1,343 @@ +# coding: utf8 +{ +'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"Update" ist ein optionaler Ausdruck wie "Feld1 = \'newvalue". JOIN Ergebnisse können nicht aktualisiert oder gelöscht werden', +'%Y-%m-%d': '%Y-%m-%d', +'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'%s rows deleted': '%s строк удалено', +'%s rows updated': '%s строк обновлено', +'(requires internet access)': '(требует подключения к интернету)', +'(something like "it-it")': '(наподобие "it-it")', +'A new version of web2py is available': 'Доступна новая версия web2py', +'A new version of web2py is available: %s': 'Доступна новая версия web2py: %s', +'A new version of web2py is available: Version 1.85.3 (2010-09-18 07:07:46)\n': 'Доступна новая версия web2py: Версия 1.85.3 (2010-09-18 07:07:46)\n', +'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ВНИМАНИЕ: Для входа требуется бесопасное (HTTPS) соединение либо запуск на localhost.', +'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ВНИМАНИЕ: Тестирование не потокобезопасно, поэтому не запускайте несколько тестов параллельно.', +'ATTENTION: This is an experimental feature and it needs more testing.': 'ВНИМАНИЕ: Это экспериментальная возможность и требует тестирования.', +'ATTENTION: you cannot edit the running application!': 'ВНИМАНИЕ: Вы не можете редактировать работающее приложение!', +'Abort': 'Отмена', +'About': 'О', +'About application': 'О приложении', +'Additional code for your application': 'Допольнительный код для вашего приложения', +'Admin is disabled because insecure channel': 'Админпанель выключена из-за небезопасного соединения', +'Admin is disabled because unsecure channel': 'Админпанель выключен из-за небезопасного соединения', +'Admin language': 'Язык админпанели', +'Administrator Password:': 'Пароль администратора:', +'Application name:': 'Название приложения:', +'Are you sure you want to delete file "%s"?': 'Вы действительно хотите удалить файл "%s"?', +'Are you sure you want to uninstall application "%s"': 'Вы действительно хотите удалить приложение "%s"', +'Are you sure you want to uninstall application "%s"?': 'Вы действительно хотите удалить приложение "%s"?', +'Are you sure you want to upgrade web2py now?': 'Вы действительно хотите обновить web2py сейчас?', +'Authentication': 'Аутентификация', +'Available databases and tables': 'Доступные базы данных и таблицы', +'Cannot be empty': 'Не может быть пустым', +'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': 'Невозможно компилировать: в приложении присутствуют ошибки. Отладьте его, исправьте ошибки и попробуйте заново.', +'Change Password': 'Изменить пароль', +'Check to delete': 'Поставьте для удаления', +'Checking for upgrades...': 'Проверка обновлений...', +'Client IP': 'IP клиента', +'Controller': 'Контроллер', +'Controllers': 'Контроллеры', +'Copyright': 'Copyright', +'Create new simple application': 'Создать новое простое приложение', +'Current request': 'Текущий запрос', +'Current response': 'Текущий ответ', +'Current session': 'Текущая сессия', +'DB Model': 'Модель БД', +'DESIGN': 'ДИЗАЙН', +'Database': 'База данных', +'Date and Time': 'Дата и время', +'Delete': 'Удалить', +'Delete:': 'Удалить:', +'Deploy on Google App Engine': 'Развернуть на Google App Engine', +'Description': 'Описание', +'Design for': 'Дизайн для', +'E-mail': 'E-mail', +'EDIT': 'ПРАВКА', +'Edit': 'Правка', +'Edit Profile': 'Правка профиля', +'Edit This App': 'Правка данного приложения', +'Edit application': 'Правка приложения', +'Edit current record': 'Правка текущей записи', +'Editing Language file': 'Правка языкового файла', +'Editing file': 'Правка файла', +'Editing file "%s"': 'Правка файла "%s"', +'Enterprise Web Framework': 'Enterprise Web Framework', +'Error logs for "%(app)s"': 'Журнал ошибок для "%(app)"', +'Exception instance attributes': 'Атрибуты объекта исключения', +'Expand Abbreviation': 'Раскрыть аббревиатуру', +'First name': 'Имя', +'Functions with no doctests will result in [passed] tests.': 'Функции без doctest будут давать [прошел] в тестах.', +'Go to Matching Pair': 'К подходящей паре', +'Group ID': 'ID группы', +'Hello World': 'Привет, Мир', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Если отчет выше содержит номер ошибки, это указывает на ошибку при работе контроллера, до попытки выполнить doctest. Причиной чаще является неверные отступы или ошибки в коде вне функции. \nЗеленый заголовок указывает на успешное выполнение всех тестов. В этом случае результаты тестов не показываются.', +'If you answer "yes", be patient, it may take a while to download': 'Если вы ответили "Да", потерпите, загрузка может потребовать времени', +'If you answer yes, be patient, it may take a while to download': 'Если вы ответили "Да", потерпите, загрузка может потребовать времени', +'Import/Export': 'Импорт/Экспорт', +'Index': 'Индекс', +'Installed applications': 'Установленные приложения', +'Internal State': 'Внутренний статус', +'Invalid Query': 'Неверный запрос', +'Invalid action': 'Неверное действие', +'Invalid email': 'Неверный email', +'Key bindings': 'Связываник клавиш', +'Key bindings for ZenConding Plugin': 'Связывание клавиш для плагина ZenConding', +'Language files (static strings) updated': 'Языковые файлы (статичные строки) обновлены', +'Languages': 'Языки', +'Last name': 'Фамилия', +'Last saved on:': 'Последнее сохранение:', +'Layout': 'Верстка', +'License for': 'Лицензия для', +'Login': 'Логин', +'Login to the Administrative Interface': 'Вход в интерфейс администратора', +'Logout': 'Выход', +'Lost Password': 'Забыли пароль', +'Main Menu': 'Главное меню', +'Match Pair': 'Найти пару', +'Menu Model': 'Модель меню', +'Merge Lines': 'Объединить линии', +'Models': 'Модели', +'Modules': 'Модули', +'NO': 'НЕТ', +'Name': 'Название', +'New Record': 'Новая запись', +'New application wizard': 'Мастер нового приложения', +'New simple application': 'Новое простое приложение', +'Next Edit Point': 'Следующее место правки', +'No databases in this application': 'В приложении нет базы данных', +'Origin': 'Оригинал', +'Original/Translation': 'Оригинал/Перевод', +'Password': 'Пароль', +'Peeking at file': 'Просмотр', +'Plugin "%s" in application': 'Плагин "%s" в приложении', +'Plugins': 'Плагины', +'Powered by': 'Обеспечен', +'Previous Edit Point': 'Предыдущее место правки', +'Query:': 'Запрос:', +'Record ID': 'ID записи', +'Register': 'Зарегистрироваться', +'Registration key': 'Ключ регистрации', +'Reset Password key': 'Сброс пароля', +'Resolve Conflict file': 'Решить конфликт в файле', +'Role': 'Роль', +'Rows in table': 'Строк в таблице', +'Rows selected': 'Выбрано строк', +'Save via Ajax': 'Сохранить через Ajax', +'Saved file hash:': 'Хэш сохраненного файла:', +'Searching:': 'Поиск:', +'Static files': 'Статические файлы', +'Stylesheet': 'Таблицы стилей', +'Sure you want to delete this object?': 'Действительно хотите удалить данный объект?', +'TM': 'TM', +'Table name': 'Название таблицы', +'Testing application': 'Тест приложения', +'Testing controller': 'Тест контроллера', +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"query" является условием вида "db.table1.field1 == \'значение\'". Что-либо типа "db.table1.field1 db.table2.field2 ==" ведет к SQL JOIN.', +'The application logic, each URL path is mapped in one exposed function in the controller': 'Логика приложения, каждый URL отображается к одной функции в контроллере', +'The data representation, define database tables and sets': 'Представление данных, определите таблицы базы данных и наборы', +'The output of the file is a dictionary that was rendered by the view': 'Выводом файла является словарь, который создан в виде', +'The presentations layer, views are also known as templates': 'Слой презентации, виды так же известны, как шаблоны', +'There are no controllers': 'Отсутствуют контроллеры', +'There are no models': 'Отсутствуют модели', +'There are no modules': 'Отсутствуют модули', +'There are no plugins': 'Отсутствуют плагины', +'There are no static files': 'Отсутствуют статичные файлы', +'There are no translators, only default language is supported': 'Отсутствуют переводчики, поддерживается только стандартный язык', +'There are no views': 'Отсутствуют виды', +'These files are served without processing, your images go here': 'Эти файлы обслуживаются без обработки, ваши изображения попадут сюда', +'This is a copy of the scaffolding application': 'Это копия сгенерированного приложения', +'This is the %(filename)s template': 'Это шаблон %(filename)s', +'Ticket': 'Тикет', +'Timestamp': 'Время', +'To create a plugin, name a file/folder plugin_[name]': 'Для создания плагина назовите файл/папку plugin_[название]', +'Translation strings for the application': 'Строки перевода для приложения', +'Unable to check for upgrades': 'Невозможно проверить обновления', +'Unable to download': 'Невозможно загрузить', +'Unable to download app': 'Невозможно загрузить', +'Update:': 'Обновить:', +'Upload & install packed application': 'Загрузить и установить приложение в архиве', +'Upload a package:': 'Загрузить пакет:', +'Upload existing application': 'Загрузить существующее приложение', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Используйте (...)&(...) для AND, (...)|(...) для OR, и ~(...) для NOT при создании сложных запросов.', +'Use an url:': 'Используйте url:', +'User ID': 'ID пользователя', +'Version': 'Версия', +'View': 'Вид', +'Views': 'Виды', +'Welcome %s': 'Добро пожаловать, %s', +'Welcome to web2py': 'Добро пожаловать в web2py', +'Which called the function': 'Который вызвал функцию', +'Wrap with Abbreviation': 'Заключить в аббревиатуру', +'YES': 'ДА', +'You are successfully running web2py': 'Вы успешно запустили web2by', +'You can modify this application and adapt it to your needs': 'Вы можете изменить это приложение и подогнать под свои нужды', +'You visited the url': 'Вы посетили URL', +'About': 'О', +'additional code for your application': 'добавочный код для вашего приложения', +'admin disabled because no admin password': 'админка отключена, потому что отсутствует пароль администратора', +'admin disabled because not supported on google apps engine': 'админка отключена, т.к. не поддерживается на google app engine', +'admin disabled because unable to access password file': 'админка отключена, т.к. невозможно получить доступ к файлу с паролями ', +'administrative interface': 'интерфейс администратора', +'and rename it (required):': 'и переименуйте его (необходимо):', +'and rename it:': ' и переименуйте его:', +'appadmin': 'appadmin', +'appadmin is disabled because insecure channel': 'Appadmin отключен, т.к. соединение не безопасно', +'application "%s" uninstalled': 'Приложение "%s" удалено', +'application compiled': 'Приложение скомпилировано', +'application is compiled and cannot be designed': 'Приложение скомпилировано и дизайн не может быть изменен', +'arguments': 'аргументы', +'back': 'назад', +'beautify': 'Раскрасить', +'cache': 'кэш', +'cache, errors and sessions cleaned': 'Кэш, ошибки и сессии очищены', +'call': 'вызов', +'cannot create file': 'Невозможно создать файл', +'cannot upload file "%(filename)s"': 'Невозможно загрузить файл "%(filename)s"', +'Change admin password': 'Изменить пароль администратора', +'change password': 'изменить пароль', +'check all': 'проверить все', +'Check for upgrades': 'проверить обновления', +'Clean': 'Очистить', +'click here for online examples': 'нажмите здесь для онлайн примеров', +'click here for the administrative interface': 'нажмите здесь для интерфейса администратора', +'click to check for upgrades': 'нажмите для проверки обновления', +'code': 'код', +'collapse/expand all': 'свернуть/развернуть все', +'Compile': 'Компилировать', +'compiled application removed': 'скомпилированное приложение удалено', +'controllers': 'контроллеры', +'Create': 'Создать', +'create file with filename:': 'Создать файл с названием:', +'create new application:': 'создать новое приложение:', +'created by': 'создано', +'crontab': 'crontab', +'currently running': 'сейчас работает', +'currently saved or': 'сейчас сохранено или', +'customize me!': 'настрой меня!', +'data uploaded': 'дата загрузки', +'database': 'база данных', +'database %s select': 'Выбор базы данных %s ', +'database administration': 'администраторирование базы данных', +'db': 'бд', +'defines tables': 'определить таблицы', +'delete': 'удалить', +'delete all checked': 'удалить выбранные', +'delete plugin': 'удалить плагин', +'Deploy': 'Развернуть', +'design': 'дизайн', +'direction: ltr': 'направление: ltr', +'documentation': 'документация', +'done!': 'выполнено!', +'download layouts': 'загрузить шаблоны', +'download plugins': 'загрузить плагины', +'Edit': 'Правка', +'edit controller': 'правка контроллера', +'edit profile': 'правка профиля', +'edit views:': 'правка видов:', +'Errors': 'Ошибка', +'escape': 'escape', +'export as csv file': 'Экспорт в CSV', +'exposes': 'открывает', +'extends': 'расширяет', +'failed to reload module': 'невозможно загрузить модуль', +'file "%(filename)s" created': 'файл "%(filename)s" создан', +'file "%(filename)s" deleted': 'файл "%(filename)s" удален', +'file "%(filename)s" uploaded': 'файл "%(filename)s" загружен', +'file "%(filename)s" was not deleted': 'файл "%(filename)s" не был удален', +'file "%s" of %s restored': 'файл "%s" из %s восстановлен', +'file changed on disk': 'файл изменился на диске', +'file does not exist': 'файл не существует', +'file saved on %(time)s': 'файл сохранен %(time)s', +'file saved on %s': 'файл сохранен %s', +'files': 'файлы', +'filter': 'фильтр', +'Help': 'Помощь', +'htmledit': 'htmledit', +'includes': 'включает', +'index': 'index', +'insert new': 'вставить новый', +'insert new %s': 'вставить новый %s', +'Install': 'Установить', +'internal error': 'внутренняя ошибка', +'invalid password': 'неверный пароль', +'invalid request': 'неверный запрос', +'invalid ticket': 'неверный тикет', +'language file "%(filename)s" created/updated': 'Языковой файл "%(filename)s" создан/обновлен', +'languages': 'языки', +'languages updated': 'языки обновлены', +'loading...': 'загрузка...', +'located in the file': 'расположенный в файле', +'login': 'логин', +'Logout': 'выход', +'lost password?': 'Пароль утерен?', +'merge': 'объединить', +'models': 'модели', +'modules': 'модули', +'new application "%s" created': 'новое приложение "%s" создано', +'new record inserted': 'новая запись вставлена', +'next 100 rows': 'следующие 100 строк', +'or import from csv file': 'или испорт из cvs файла', +'or provide app url:': 'или URL приложения:', +'or provide application url:': 'или URL приложения:', +'Overwrite installed app': 'Переписать на установленное приложение', +'Pack all': 'упаковать все', +'Pack compiled': 'Архив скомпилирован', +'pack plugin': 'Упаковать плагин', +'please wait!': 'подождите, пожалуйста!', +'plugins': 'плагины', +'previous 100 rows': 'предыдущие 100 строк', +'record': 'запись', +'record does not exist': 'запись не существует', +'record id': 'id записи', +'register': 'зарегистрироваться', +'Remove compiled': 'Удалить скомпилированное', +'restore': 'восстановить', +'revert': 'откатиться', +'save': 'сохранить', +'selected': 'выбрано', +'session expired': 'сессия истекла', +'shell': 'shell', +'Site': 'сайт', +'some files could not be removed': 'некоторые файлы нельзя удалить', +'Start wizard': 'запустить мастер', +'state': 'статус', +'static': 'статичные файлы', +'submit': 'Отправить', +'table': 'таблица', +'test': 'тест', +'test_def': 'test_def', +'test_for': 'test_for', +'test_if': 'test_if', +'test_try': 'test_try', +'the application logic, each URL path is mapped in one exposed function in the controller': 'Логика приложения, каждый URL отображается в открытую функцию в контроллере', +'the data representation, define database tables and sets': 'представление данных, определить таблицы и наборы', +'the presentations layer, views are also known as templates': 'слой представления, виды известные так же как шаблоны', +'these files are served without processing, your images go here': 'Эти файлы обслуживаются без обработки, ваши изображения попадут сюда', +'to previous version.': 'на предыдущую версию.', +'translation strings for the application': 'строки перевода для приложения', +'try': 'try', +'try something like': 'попробовать что-либо вида', +'unable to create application "%s"': 'невозможно создать приложение "%s" nicht möglich', +'unable to delete file "%(filename)s"': 'невозможно удалить файл "%(filename)s"', +'unable to parse csv file': 'невозможно разобрать файл csv', +'unable to uninstall "%s"': 'невозможно удалить "%s"', +'uncheck all': 'снять выбор всего', +'Uninstall': 'Удалить', +'update': 'обновить', +'update all languages': 'обновить все языки', +'upgrade web2py now': 'обновить web2py сейчас', +'upload': 'загрузить', +'upload application:': 'загрузить файл:', +'upload file:': 'загрузить файл:', +'upload plugin file:': 'загрузить файл плагина:', +'user': 'пользователь', +'variables': 'переменные', +'versioning': 'версии', +'view': 'вид', +'views': 'виды', +'web2py Recent Tweets': 'последние твиты по web2py', +'web2py is up to date': 'web2py обновлен', +'xml': 'xml', +} + + From d383152af53502007aa8027bcad65eabab5018a2 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 23 Jan 2012 20:30:08 -0600 Subject: [PATCH 27/77] admin in Russian, thanks bulat --- VERSION | 2 +- applications/admin/languages/ru-ru.py | 343 ++++++++++++++++++++++++++ 2 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 applications/admin/languages/ru-ru.py diff --git a/VERSION b/VERSION index 33edee66..19a62034 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-23 08:26:04) stable +Version 1.99.4 (2012-01-23 20:29:32) stable diff --git a/applications/admin/languages/ru-ru.py b/applications/admin/languages/ru-ru.py new file mode 100644 index 00000000..5bf12c10 --- /dev/null +++ b/applications/admin/languages/ru-ru.py @@ -0,0 +1,343 @@ +# coding: utf8 +{ +'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"Update" ist ein optionaler Ausdruck wie "Feld1 = \'newvalue". JOIN Ergebnisse können nicht aktualisiert oder gelöscht werden', +'%Y-%m-%d': '%Y-%m-%d', +'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'%s rows deleted': '%s строк удалено', +'%s rows updated': '%s строк обновлено', +'(requires internet access)': '(требует подключения к интернету)', +'(something like "it-it")': '(наподобие "it-it")', +'A new version of web2py is available': 'Доступна новая версия web2py', +'A new version of web2py is available: %s': 'Доступна новая версия web2py: %s', +'A new version of web2py is available: Version 1.85.3 (2010-09-18 07:07:46)\n': 'Доступна новая версия web2py: Версия 1.85.3 (2010-09-18 07:07:46)\n', +'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ВНИМАНИЕ: Для входа требуется бесопасное (HTTPS) соединение либо запуск на localhost.', +'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ВНИМАНИЕ: Тестирование не потокобезопасно, поэтому не запускайте несколько тестов параллельно.', +'ATTENTION: This is an experimental feature and it needs more testing.': 'ВНИМАНИЕ: Это экспериментальная возможность и требует тестирования.', +'ATTENTION: you cannot edit the running application!': 'ВНИМАНИЕ: Вы не можете редактировать работающее приложение!', +'Abort': 'Отмена', +'About': 'О', +'About application': 'О приложении', +'Additional code for your application': 'Допольнительный код для вашего приложения', +'Admin is disabled because insecure channel': 'Админпанель выключена из-за небезопасного соединения', +'Admin is disabled because unsecure channel': 'Админпанель выключен из-за небезопасного соединения', +'Admin language': 'Язык админпанели', +'Administrator Password:': 'Пароль администратора:', +'Application name:': 'Название приложения:', +'Are you sure you want to delete file "%s"?': 'Вы действительно хотите удалить файл "%s"?', +'Are you sure you want to uninstall application "%s"': 'Вы действительно хотите удалить приложение "%s"', +'Are you sure you want to uninstall application "%s"?': 'Вы действительно хотите удалить приложение "%s"?', +'Are you sure you want to upgrade web2py now?': 'Вы действительно хотите обновить web2py сейчас?', +'Authentication': 'Аутентификация', +'Available databases and tables': 'Доступные базы данных и таблицы', +'Cannot be empty': 'Не может быть пустым', +'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': 'Невозможно компилировать: в приложении присутствуют ошибки. Отладьте его, исправьте ошибки и попробуйте заново.', +'Change Password': 'Изменить пароль', +'Check to delete': 'Поставьте для удаления', +'Checking for upgrades...': 'Проверка обновлений...', +'Client IP': 'IP клиента', +'Controller': 'Контроллер', +'Controllers': 'Контроллеры', +'Copyright': 'Copyright', +'Create new simple application': 'Создать новое простое приложение', +'Current request': 'Текущий запрос', +'Current response': 'Текущий ответ', +'Current session': 'Текущая сессия', +'DB Model': 'Модель БД', +'DESIGN': 'ДИЗАЙН', +'Database': 'База данных', +'Date and Time': 'Дата и время', +'Delete': 'Удалить', +'Delete:': 'Удалить:', +'Deploy on Google App Engine': 'Развернуть на Google App Engine', +'Description': 'Описание', +'Design for': 'Дизайн для', +'E-mail': 'E-mail', +'EDIT': 'ПРАВКА', +'Edit': 'Правка', +'Edit Profile': 'Правка профиля', +'Edit This App': 'Правка данного приложения', +'Edit application': 'Правка приложения', +'Edit current record': 'Правка текущей записи', +'Editing Language file': 'Правка языкового файла', +'Editing file': 'Правка файла', +'Editing file "%s"': 'Правка файла "%s"', +'Enterprise Web Framework': 'Enterprise Web Framework', +'Error logs for "%(app)s"': 'Журнал ошибок для "%(app)"', +'Exception instance attributes': 'Атрибуты объекта исключения', +'Expand Abbreviation': 'Раскрыть аббревиатуру', +'First name': 'Имя', +'Functions with no doctests will result in [passed] tests.': 'Функции без doctest будут давать [прошел] в тестах.', +'Go to Matching Pair': 'К подходящей паре', +'Group ID': 'ID группы', +'Hello World': 'Привет, Мир', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Если отчет выше содержит номер ошибки, это указывает на ошибку при работе контроллера, до попытки выполнить doctest. Причиной чаще является неверные отступы или ошибки в коде вне функции. \nЗеленый заголовок указывает на успешное выполнение всех тестов. В этом случае результаты тестов не показываются.', +'If you answer "yes", be patient, it may take a while to download': 'Если вы ответили "Да", потерпите, загрузка может потребовать времени', +'If you answer yes, be patient, it may take a while to download': 'Если вы ответили "Да", потерпите, загрузка может потребовать времени', +'Import/Export': 'Импорт/Экспорт', +'Index': 'Индекс', +'Installed applications': 'Установленные приложения', +'Internal State': 'Внутренний статус', +'Invalid Query': 'Неверный запрос', +'Invalid action': 'Неверное действие', +'Invalid email': 'Неверный email', +'Key bindings': 'Связываник клавиш', +'Key bindings for ZenConding Plugin': 'Связывание клавиш для плагина ZenConding', +'Language files (static strings) updated': 'Языковые файлы (статичные строки) обновлены', +'Languages': 'Языки', +'Last name': 'Фамилия', +'Last saved on:': 'Последнее сохранение:', +'Layout': 'Верстка', +'License for': 'Лицензия для', +'Login': 'Логин', +'Login to the Administrative Interface': 'Вход в интерфейс администратора', +'Logout': 'Выход', +'Lost Password': 'Забыли пароль', +'Main Menu': 'Главное меню', +'Match Pair': 'Найти пару', +'Menu Model': 'Модель меню', +'Merge Lines': 'Объединить линии', +'Models': 'Модели', +'Modules': 'Модули', +'NO': 'НЕТ', +'Name': 'Название', +'New Record': 'Новая запись', +'New application wizard': 'Мастер нового приложения', +'New simple application': 'Новое простое приложение', +'Next Edit Point': 'Следующее место правки', +'No databases in this application': 'В приложении нет базы данных', +'Origin': 'Оригинал', +'Original/Translation': 'Оригинал/Перевод', +'Password': 'Пароль', +'Peeking at file': 'Просмотр', +'Plugin "%s" in application': 'Плагин "%s" в приложении', +'Plugins': 'Плагины', +'Powered by': 'Обеспечен', +'Previous Edit Point': 'Предыдущее место правки', +'Query:': 'Запрос:', +'Record ID': 'ID записи', +'Register': 'Зарегистрироваться', +'Registration key': 'Ключ регистрации', +'Reset Password key': 'Сброс пароля', +'Resolve Conflict file': 'Решить конфликт в файле', +'Role': 'Роль', +'Rows in table': 'Строк в таблице', +'Rows selected': 'Выбрано строк', +'Save via Ajax': 'Сохранить через Ajax', +'Saved file hash:': 'Хэш сохраненного файла:', +'Searching:': 'Поиск:', +'Static files': 'Статические файлы', +'Stylesheet': 'Таблицы стилей', +'Sure you want to delete this object?': 'Действительно хотите удалить данный объект?', +'TM': 'TM', +'Table name': 'Название таблицы', +'Testing application': 'Тест приложения', +'Testing controller': 'Тест контроллера', +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"query" является условием вида "db.table1.field1 == \'значение\'". Что-либо типа "db.table1.field1 db.table2.field2 ==" ведет к SQL JOIN.', +'The application logic, each URL path is mapped in one exposed function in the controller': 'Логика приложения, каждый URL отображается к одной функции в контроллере', +'The data representation, define database tables and sets': 'Представление данных, определите таблицы базы данных и наборы', +'The output of the file is a dictionary that was rendered by the view': 'Выводом файла является словарь, который создан в виде', +'The presentations layer, views are also known as templates': 'Слой презентации, виды так же известны, как шаблоны', +'There are no controllers': 'Отсутствуют контроллеры', +'There are no models': 'Отсутствуют модели', +'There are no modules': 'Отсутствуют модули', +'There are no plugins': 'Отсутствуют плагины', +'There are no static files': 'Отсутствуют статичные файлы', +'There are no translators, only default language is supported': 'Отсутствуют переводчики, поддерживается только стандартный язык', +'There are no views': 'Отсутствуют виды', +'These files are served without processing, your images go here': 'Эти файлы обслуживаются без обработки, ваши изображения попадут сюда', +'This is a copy of the scaffolding application': 'Это копия сгенерированного приложения', +'This is the %(filename)s template': 'Это шаблон %(filename)s', +'Ticket': 'Тикет', +'Timestamp': 'Время', +'To create a plugin, name a file/folder plugin_[name]': 'Для создания плагина назовите файл/папку plugin_[название]', +'Translation strings for the application': 'Строки перевода для приложения', +'Unable to check for upgrades': 'Невозможно проверить обновления', +'Unable to download': 'Невозможно загрузить', +'Unable to download app': 'Невозможно загрузить', +'Update:': 'Обновить:', +'Upload & install packed application': 'Загрузить и установить приложение в архиве', +'Upload a package:': 'Загрузить пакет:', +'Upload existing application': 'Загрузить существующее приложение', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Используйте (...)&(...) для AND, (...)|(...) для OR, и ~(...) для NOT при создании сложных запросов.', +'Use an url:': 'Используйте url:', +'User ID': 'ID пользователя', +'Version': 'Версия', +'View': 'Вид', +'Views': 'Виды', +'Welcome %s': 'Добро пожаловать, %s', +'Welcome to web2py': 'Добро пожаловать в web2py', +'Which called the function': 'Который вызвал функцию', +'Wrap with Abbreviation': 'Заключить в аббревиатуру', +'YES': 'ДА', +'You are successfully running web2py': 'Вы успешно запустили web2by', +'You can modify this application and adapt it to your needs': 'Вы можете изменить это приложение и подогнать под свои нужды', +'You visited the url': 'Вы посетили URL', +'About': 'О', +'additional code for your application': 'добавочный код для вашего приложения', +'admin disabled because no admin password': 'админка отключена, потому что отсутствует пароль администратора', +'admin disabled because not supported on google apps engine': 'админка отключена, т.к. не поддерживается на google app engine', +'admin disabled because unable to access password file': 'админка отключена, т.к. невозможно получить доступ к файлу с паролями ', +'administrative interface': 'интерфейс администратора', +'and rename it (required):': 'и переименуйте его (необходимо):', +'and rename it:': ' и переименуйте его:', +'appadmin': 'appadmin', +'appadmin is disabled because insecure channel': 'Appadmin отключен, т.к. соединение не безопасно', +'application "%s" uninstalled': 'Приложение "%s" удалено', +'application compiled': 'Приложение скомпилировано', +'application is compiled and cannot be designed': 'Приложение скомпилировано и дизайн не может быть изменен', +'arguments': 'аргументы', +'back': 'назад', +'beautify': 'Раскрасить', +'cache': 'кэш', +'cache, errors and sessions cleaned': 'Кэш, ошибки и сессии очищены', +'call': 'вызов', +'cannot create file': 'Невозможно создать файл', +'cannot upload file "%(filename)s"': 'Невозможно загрузить файл "%(filename)s"', +'Change admin password': 'Изменить пароль администратора', +'change password': 'изменить пароль', +'check all': 'проверить все', +'Check for upgrades': 'проверить обновления', +'Clean': 'Очистить', +'click here for online examples': 'нажмите здесь для онлайн примеров', +'click here for the administrative interface': 'нажмите здесь для интерфейса администратора', +'click to check for upgrades': 'нажмите для проверки обновления', +'code': 'код', +'collapse/expand all': 'свернуть/развернуть все', +'Compile': 'Компилировать', +'compiled application removed': 'скомпилированное приложение удалено', +'controllers': 'контроллеры', +'Create': 'Создать', +'create file with filename:': 'Создать файл с названием:', +'create new application:': 'создать новое приложение:', +'created by': 'создано', +'crontab': 'crontab', +'currently running': 'сейчас работает', +'currently saved or': 'сейчас сохранено или', +'customize me!': 'настрой меня!', +'data uploaded': 'дата загрузки', +'database': 'база данных', +'database %s select': 'Выбор базы данных %s ', +'database administration': 'администраторирование базы данных', +'db': 'бд', +'defines tables': 'определить таблицы', +'delete': 'удалить', +'delete all checked': 'удалить выбранные', +'delete plugin': 'удалить плагин', +'Deploy': 'Развернуть', +'design': 'дизайн', +'direction: ltr': 'направление: ltr', +'documentation': 'документация', +'done!': 'выполнено!', +'download layouts': 'загрузить шаблоны', +'download plugins': 'загрузить плагины', +'Edit': 'Правка', +'edit controller': 'правка контроллера', +'edit profile': 'правка профиля', +'edit views:': 'правка видов:', +'Errors': 'Ошибка', +'escape': 'escape', +'export as csv file': 'Экспорт в CSV', +'exposes': 'открывает', +'extends': 'расширяет', +'failed to reload module': 'невозможно загрузить модуль', +'file "%(filename)s" created': 'файл "%(filename)s" создан', +'file "%(filename)s" deleted': 'файл "%(filename)s" удален', +'file "%(filename)s" uploaded': 'файл "%(filename)s" загружен', +'file "%(filename)s" was not deleted': 'файл "%(filename)s" не был удален', +'file "%s" of %s restored': 'файл "%s" из %s восстановлен', +'file changed on disk': 'файл изменился на диске', +'file does not exist': 'файл не существует', +'file saved on %(time)s': 'файл сохранен %(time)s', +'file saved on %s': 'файл сохранен %s', +'files': 'файлы', +'filter': 'фильтр', +'Help': 'Помощь', +'htmledit': 'htmledit', +'includes': 'включает', +'index': 'index', +'insert new': 'вставить новый', +'insert new %s': 'вставить новый %s', +'Install': 'Установить', +'internal error': 'внутренняя ошибка', +'invalid password': 'неверный пароль', +'invalid request': 'неверный запрос', +'invalid ticket': 'неверный тикет', +'language file "%(filename)s" created/updated': 'Языковой файл "%(filename)s" создан/обновлен', +'languages': 'языки', +'languages updated': 'языки обновлены', +'loading...': 'загрузка...', +'located in the file': 'расположенный в файле', +'login': 'логин', +'Logout': 'выход', +'lost password?': 'Пароль утерен?', +'merge': 'объединить', +'models': 'модели', +'modules': 'модули', +'new application "%s" created': 'новое приложение "%s" создано', +'new record inserted': 'новая запись вставлена', +'next 100 rows': 'следующие 100 строк', +'or import from csv file': 'или испорт из cvs файла', +'or provide app url:': 'или URL приложения:', +'or provide application url:': 'или URL приложения:', +'Overwrite installed app': 'Переписать на установленное приложение', +'Pack all': 'упаковать все', +'Pack compiled': 'Архив скомпилирован', +'pack plugin': 'Упаковать плагин', +'please wait!': 'подождите, пожалуйста!', +'plugins': 'плагины', +'previous 100 rows': 'предыдущие 100 строк', +'record': 'запись', +'record does not exist': 'запись не существует', +'record id': 'id записи', +'register': 'зарегистрироваться', +'Remove compiled': 'Удалить скомпилированное', +'restore': 'восстановить', +'revert': 'откатиться', +'save': 'сохранить', +'selected': 'выбрано', +'session expired': 'сессия истекла', +'shell': 'shell', +'Site': 'сайт', +'some files could not be removed': 'некоторые файлы нельзя удалить', +'Start wizard': 'запустить мастер', +'state': 'статус', +'static': 'статичные файлы', +'submit': 'Отправить', +'table': 'таблица', +'test': 'тест', +'test_def': 'test_def', +'test_for': 'test_for', +'test_if': 'test_if', +'test_try': 'test_try', +'the application logic, each URL path is mapped in one exposed function in the controller': 'Логика приложения, каждый URL отображается в открытую функцию в контроллере', +'the data representation, define database tables and sets': 'представление данных, определить таблицы и наборы', +'the presentations layer, views are also known as templates': 'слой представления, виды известные так же как шаблоны', +'these files are served without processing, your images go here': 'Эти файлы обслуживаются без обработки, ваши изображения попадут сюда', +'to previous version.': 'на предыдущую версию.', +'translation strings for the application': 'строки перевода для приложения', +'try': 'try', +'try something like': 'попробовать что-либо вида', +'unable to create application "%s"': 'невозможно создать приложение "%s" nicht möglich', +'unable to delete file "%(filename)s"': 'невозможно удалить файл "%(filename)s"', +'unable to parse csv file': 'невозможно разобрать файл csv', +'unable to uninstall "%s"': 'невозможно удалить "%s"', +'uncheck all': 'снять выбор всего', +'Uninstall': 'Удалить', +'update': 'обновить', +'update all languages': 'обновить все языки', +'upgrade web2py now': 'обновить web2py сейчас', +'upload': 'загрузить', +'upload application:': 'загрузить файл:', +'upload file:': 'загрузить файл:', +'upload plugin file:': 'загрузить файл плагина:', +'user': 'пользователь', +'variables': 'переменные', +'versioning': 'версии', +'view': 'вид', +'views': 'виды', +'web2py Recent Tweets': 'последние твиты по web2py', +'web2py is up to date': 'web2py обновлен', +'xml': 'xml', +} + + From d5a69dda9702f302edc0c40d50ef57753958a7bb Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 23 Jan 2012 20:41:51 -0600 Subject: [PATCH 28/77] new installe,d thanks Ross --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 19a62034..06cd516e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-23 20:29:32) stable +Version 1.99.4 (2012-01-23 20:41:39) stable From 61a3cc946136e2090d39bb6de3261d2762d2e72c Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 23 Jan 2012 21:15:57 -0600 Subject: [PATCH 29/77] merged with latest mongodbadapter, thanks Mark --- VERSION | 2 +- gluon/dal.py | 390 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 266 insertions(+), 126 deletions(-) diff --git a/VERSION b/VERSION index 06cd516e..bdb962ad 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-23 20:41:39) stable +Version 1.99.4 (2012-01-23 21:15:53) stable diff --git a/gluon/dal.py b/gluon/dal.py index 8c50a748..8762a719 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -3820,9 +3820,9 @@ def cleanup(text): % text return text - class MongoDBAdapter(NoSQLAdapter): uploads_in_blob = True + types = { 'boolean': bool, 'string': str, @@ -3846,6 +3846,20 @@ def __init__(self,db,uri='mongodb://127.0.0.1:5984/db', pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, adapter_args={}): + 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.db = db self.uri = uri self.dbengine = 'mongodb' @@ -3857,23 +3871,27 @@ def __init__(self,db,uri='mongodb://127.0.0.1:5984/db', 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 - self.defaultsafe = adapter_args.get('safe',True) + self.safe = adapter_args.get('safe',True) - m = re.compile('^(?P[^\:/]+)(\:(?P[0-9]+))?/(?P.+)$').match(self.uri[10:]) - if not m: - raise SyntaxError, "Invalid URI string in DAL: %s" % self.uri - host = m.group('host') - if not host: - raise SyntaxError, 'mongodb: host name required' - dbname = m.group('db') - if not dbname: - raise SyntaxError, 'mongodb: db name required' - port = int(m.group('port') or 27017) - driver_args.update(dict(host=host,port=port)) - def connect(dbname=dbname,driver_args=driver_args): - return pymongo.Connection(**driver_args)[dbname] + + if isinstance(m,tuple): + m = {"database" : m[1]} + if m.get('database')==None: + raise SyntaxError("Database is required!") + def connect(uri=self.uri,m=m): + try: + return pymongo.Connection(uri)[m.get('database')] + except pymongo.errors.ConnectionFailure as inst: + raise SyntaxError, "The connection to " + uri + " could not be made" + except Exception as inst: + 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(Mer("This is not an official Mongodb uri (http://www.mongodb.org/display/DOCS/Connections) Error : %s" % inst)) self.pool_connection(connect,cursor=False) + + def represent(self, obj, fieldtype): value = NoSQLAdapter.represent(self, obj, fieldtype) if fieldtype =='date': @@ -3892,7 +3910,9 @@ def represent(self, obj, fieldtype): #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=True): + def insert(self,table,fields,safe=None): + if safe==None: + 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) @@ -3904,18 +3924,18 @@ def create_table(self, table, migrate=True, fake_migrate=False, polymodel=None, else: pass - def count(self,query,distinct=None): + 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) - rows = self.select(query,[self.db[tablename]._id],{}) + 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 len(rows) def expand(self, expression, field_type=None): + import pymongo.objectid #if isinstance(expression,Field): # if expression.type=='id': # return {_id}" @@ -3979,7 +3999,7 @@ def _select(self,query,fields,attributes): limitby = attributes.get('limitby', False) #distinct = attributes.get('distinct', False) if orderby: - print "in if orderby %s" % orderby + #print "in if orderby %s" % orderby if isinstance(orderby, (list, tuple)): print "in xorify" orderby = xorify(orderby) @@ -3991,7 +4011,7 @@ def _select(self,query,fields,attributes): mongosort_list.append((f[1:],-1)) else: mongosort_list.append((f,1)) - print "mongosort_list = %s" % mongosort_list + print "mongosort_list = %s" % mongosort_list if limitby: # a tuple @@ -4000,8 +4020,11 @@ def _select(self,query,fields,attributes): limitby_skip = 0 limitby_limit = 0 + + + #if distinct: - # print "in distinct %s" % distinct + #print "in distinct %s" % distinct mongofields_dict = son.SON() mongoqry_dict = {} @@ -4025,134 +4048,107 @@ def _select(self,query,fields,attributes): # need to define all the 'sql' methods gt,lt etc.... - def select(self,query,fields,attributes): - + def select(self,query,fields,attributes,count=False,snapshot=False): + withId=False tablename, mongoqry_dict , mongofields_dict, mongosort_list, limitby_limit, limitby_skip = self._select(query,fields,attributes) + for key in mongofields_dict.keys(): + if key == 'id': + withId = True + break; try: print "mongoqry_dict=%s" % mongoqry_dict except: pass - # print "mongofields_dict=%s" % mongofields_dict + print "mongofields_dict=%s" % mongofields_dict ctable = self.connection[tablename] - mongo_list_dicts = ctable.find(mongoqry_dict,mongofields_dict,skip=limitby_skip, limit=limitby_limit, sort=mongosort_list) # pymongo cursor object - print "mongo_list_dicts=%s" % mongo_list_dicts + 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 + print "mongo_list_dicts=%s" % mongo_list_dicts #if mongo_list_dicts.count() > 0: # #colnames = mongo_list_dicts[0].keys() # assuming all docs have same "shape", grab colnames from first dictionary (aka row) #else: #colnames = mongofields_dict.keys() - print "colnames = %s" % colnames + #print "colnames = %s" % colnames #rows = [row.values() for row in mongo_list_dicts] - rows = mongo_list_dicts - return self.parse(rows, fields, mongofields_dict.keys(), False, tablename) - - def parse(self, rows, fields, colnames, blob_decode=True,tablename=None): - self.build_parsemap() - import pymongo.objectid - print "in parse" - print "colnames=%s" % colnames - db = self.db - virtualtables = [] - table_colnames = [] - new_rows = [] - for (i,row) in enumerate(rows): - print "i,row = %s,%s" % (i,row) - new_row = Row() - for j,colname in enumerate(colnames): - # hack to get past 'id' key error, we seem to need to keep the 'id' key so lets create an id row value - if colname == 'id': - #try: - if isinstance(row['_id'],pymongo.objectid.ObjectId): - row[colname] = int(str(row['_id']),16) + rows = [] + for record in mongo_list_dicts: + row=[] + for column in record: + if withId and (column == '_id'): + if isinstance(record[column],pymongo.objectid.ObjectId): + row.append( int(str(record[column]),16)) else: #in case of alternative key - row[colname] = row['_id'] - #except: - #an id can also be user defined - #row[colname] = row['_id'] - #Alternative solutions are UUID's, counter function in mongo - #del row['_id'] - #colnames.append('_id') - print "j = %s" % j - value = row.get(colname,None) # blob field not implemented, or missing key:value in a mongo document - colname = "%s.%s" % (tablename, colname) # hack to match re (table_field) - if i == 0: #only on first row - table_colnames.append(colname) - - - if not regex_table_field.match(colnames[j]): - if not '_extra' in new_row: - new_row['_extra'] = Row() - new_row['_extra'][colnames[j]] = self.parse_value(value, fields[j].type) - new_column_name = regex_select_as_parser.search(colnames[j]) - if not new_column_name is None: - column_name = new_column_name.groups(0) - setattr(new_row,column_name[0],value) - else: - (tablename, fieldname) = colname.split('.') - table = db[tablename] - field = table[fieldname] - if not tablename in new_row: - colset = new_row[tablename] = Row() - if tablename not in virtualtables: - virtualtables.append(tablename) - else: - colset = new_row[tablename] - colset[fieldname] = value = self.parse_value(value,field.type) - - if field.type == 'id': - id = value - colset.update_record = lambda _ = (colset, table, id), **a: update_record(_, a) - colset.delete_record = lambda t = table, i = id: t._db(t._id==i).delete() - for (referee_table, referee_name) in table._referenced_by: - s = db[referee_table][referee_name] - referee_link = db._referee_name and \ - db._referee_name % dict(table=referee_table,field=referee_name) - if referee_link and not referee_link in colset: - colset[referee_link] = Set(db, s == id) + row.append( record[column] ) + elif not (column == '_id'): + row.append(record[column]) + rows.append(row) + #else the id is not supposed to be included. Work around error. mongo always sends key:( - new_rows.append(new_row) - print "table_colnames = %s" % table_colnames - rowsobj = Rows(db, new_rows, table_colnames, rawrows=rows) - - for tablename in virtualtables: - ### new style virtual fields - table = db[tablename] - fields_virtual = [(f,v) for (f,v) in table.items() if isinstance(v,FieldVirtual)] - fields_lazy = [(f,v) for (f,v) in table.items() if isinstance(v,FieldLazy)] - if fields_virtual or fields_lazy: - for row in rowsobj.records: - box = row[tablename] - for f,v in fields_virtual: - box[f] = v.f(row) - for f,v in fields_lazy: - box[f] = (v.handler or VirtualCommand)(v.f,row) - - ### old style virtual fields - for item in table.virtualfields: - try: - rowsobj = rowsobj.setvirtualfields(**{tablename:item}) - except KeyError: - # to avoid breaking virtualfields when partial select - pass - return rowsobj + return self.parse(rows,fields,mongofields_dict.keys(),False) def INVERT(self,first): - print "in invert first=%s" % first + #print "in invert first=%s" % first return '-%s' % self.expand(first) def drop(self, table, mode=''): ctable = self.connection[table._tablename] ctable.drop() - - def truncate(self,table,mode): + + + def truncate(self,table,mode,safe=None): + if safe==None: + safe=self.safe ctable = self.connection[table._tablename] ctable.remove(None, safe=True) - def update(self,tablename,query,fields): + #the update function should return a string + def oupdate(self,tablename,query,fields): if not isinstance(query,Query): raise SyntaxError, "Not Supported" - - raise RuntimeError, "Not implemented" + 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 + + #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): + raise RuntimeError, "Not implemented" + amount = self.count(query,False) + modify,filter = self.oupdate(tablename,query,fields) + try: + if safe: + return self.connection[tablename].update(filter,modify,multi=True,safe=safe).n + 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 + """ + + + + #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)) def bulk_insert(self, table, items): return [self.insert(table,item) for item in items] @@ -4185,7 +4181,7 @@ def BELONGS(self, first, second): items.append(self.expand(item, first.type) for item in second) return {self.expand(first) : {"$in" : items} } - def ILIKE(self, first, second): + def LIKE(self, first, second): #escaping regex operators? return {self.expand(first) : ('%s' % self.expand(second, 'string').replace('%','/'))} @@ -4283,6 +4279,150 @@ def ON(self, first, second): def COMMA(self, first, second): return '%s, %s' % (self.expand(first), self.expand(second)) + def bulk_insert(self, table, items): + return [self.insert(table,item) for item in items] + + #TODO This will probably not work:( + def NOT(self, first): + result = {} + result["$not"] = self.expand(first) + return result + + def AND(self,first,second): + f = self.expand(first) + s = self.expand(second) + f.update(s) + return f + + def OR(self,first,second): + # pymongo expects: .find( {'$or' : [{'name':'1'}, {'name':'2'}] } ) + result = {} + f = self.expand(first) + s = self.expand(second) + result['$or'] = [f,s] + return result + + def BELONGS(self, first, second): + if isinstance(second, str): + return {self.expand(first) : {"$in" : [ second[:-1]]} } + elif second==[] or second==(): + return {1:0} + items.append(self.expand(item, first.type) for item in second) + return {self.expand(first) : {"$in" : items} } + + #TODO verify full compatibilty with official SQL Like operator + def LIKE(self, first, second): + import re + 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'))}} + + #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 + import re + 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 + #TODO contains operators need to be transformed to Regex + return {self.expand(first) : {' $regex' : ".*" + re.escape(self.expand(second, 'string')) + ".*"}} + + 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 + + def NE(self, first, second=None): + print "in NE" + result = {} + result[self.expand(first)] = {'$ne': self.expand(second)} + return result + + def LT(self,first,second=None): + if second is None: + raise RuntimeError, "Cannot compare %s < None" % first + print "in LT" + result = {} + result[self.expand(first)] = {'$lt': self.expand(second)} + return result + + def LE(self,first,second=None): + if second is None: + raise RuntimeError, "Cannot compare %s <= None" % first + print "in LE" + result = {} + result[self.expand(first)] = {'$lte': self.expand(second)} + return result + + def GT(self,first,second): + print "in GT" + #import pymongo.objectid + result = {} + #if expanded_first == '_id': + #if expanded_second != 0 and not isinstance(second,pymongo.objectid.ObjectId): + #raise SyntaxError, 'second argument must be of type bson.objectid.ObjectId' + #elif expanded_second == 0: + #expanded_second = pymongo.objectid.ObjectId('000000000000000000000000') + result[self.expand(first)] = {'$gt': self.expand(second)} + return result + + def GE(self,first,second=None): + if second is None: + raise RuntimeError, "Cannot compare %s >= None" % first + print "in GE" + result = {} + result[self.expand(first)] = {'$gte': self.expand(second)} + return result + + #TODO javascript has math + def ADD(self, first, second): + raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + return '%s + %s' % (self.expand(first), self.expand(second, first.type)) + + #TODO javascript has math + def SUB(self, first, second): + raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + return '(%s - %s)' % (self.expand(first), self.expand(second, first.type)) + + #TODO javascript has math + def MUL(self, first, second): + raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + return '(%s * %s)' % (self.expand(first), self.expand(second, first.type)) + #TODO javascript has math + + def DIV(self, first, second): + raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + return '(%s / %s)' % (self.expand(first), self.expand(second, first.type)) + #TODO javascript has math + def MOD(self, first, second): + raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + return '(%s %% %s)' % (self.expand(first), self.expand(second, first.type)) + + #TODO javascript can do this + def AS(self, first, second): + raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + 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. + def ON(self, first, second): + raise NotSupported, "This is not possible in NoSQL, but can be simulated with a wrapper." + return '%s ON %s' % (self.expand(first), self.expand(second)) + + #TODO is this used in mongodb? + def COMMA(self, first, second): + return '%s, %s' % (self.expand(first), self.expand(second)) + + class IMAPAdapter(NoSQLAdapter): """ IMAP server adapter From f98ef25d323f5194c7a72c160239cfe73d06e5ae Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Tue, 24 Jan 2012 08:27:17 -0600 Subject: [PATCH 30/77] fixed a problem with keyed tables and record_id, thanks Ben Goosman --- VERSION | 2 +- gluon/dal.py | 2 ++ gluon/sqlhtml.py | 9 ++++----- gluon/tools.py | 3 +-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/VERSION b/VERSION index bdb962ad..38f82bf8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-23 21:15:53) stable +Version 1.99.4 (2012-01-24 08:27:12) stable diff --git a/gluon/dal.py b/gluon/dal.py index 8762a719..94cfec3b 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -2073,6 +2073,8 @@ class OracleAdapter(BaseAdapter): 'datetime': 'DATE', 'id': 'NUMBER PRIMARY KEY', 'reference': 'NUMBER, CONSTRAINT %(constraint_name)s FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s', + 'reference FK': ', CONSTRAINT FK_%(constraint_name)s FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s', + 'reference TFK': ' CONSTRAINT FK_%(foreign_table)s_PK FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_table)s (%(foreign_key)s) ON DELETE %(on_delete_action)s', 'list:integer': 'CLOB', 'list:string': 'CLOB', 'list:reference': 'CLOB', diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 3491fa5a..3951ab33 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -754,10 +754,8 @@ def __init__( self.record_id = record_id if keyed: - if record: - self.record_id = dict([(k,record[k]) for k in table._primarykey]) - else: - self.record_id = dict([(k,None) for k in table._primarykey]) + self.record_id = dict([(k,record and str(record[k]) or None) \ + for k in table._primarykey]) self.field_parent = {} xfields = [] self.fields = fields @@ -1037,7 +1035,8 @@ def accepts( formname_id = '.'.join(str(self.record[k]) for k in self.table._primarykey if hasattr(self.record,k)) - record_id = dict((k, request_vars[k]) for k in self.table._primarykey) + record_id = dict((k, request_vars.get(k,None)) \ + for k in self.table._primarykey) else: (formname_id, record_id) = (self.record.id, request_vars.get('id', None)) diff --git a/gluon/tools.py b/gluon/tools.py index ff659050..1edd1245 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -3177,8 +3177,7 @@ def delete( method: Crud.delete(table, record_id, [next=DEFAULT [, message=DEFAULT]]) """ - if not (isinstance(table, self.db.Table) or table in self.db.tables) \ - or not str(record_id).isdigit(): + if not (isinstance(table, self.db.Table) or table in self.db.tables): raise HTTP(404) if not isinstance(table, self.db.Table): table = self.db[table] From ccd9ddeb6993cde60bf8e763a30aee51f4665dad Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Tue, 24 Jan 2012 08:31:23 -0600 Subject: [PATCH 31/77] updated who.html --- VERSION | 2 +- applications/examples/views/default/who.html | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 38f82bf8..952b0a4c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-24 08:27:12) stable +Version 1.99.4 (2012-01-24 08:31:01) stable diff --git a/applications/examples/views/default/who.html b/applications/examples/views/default/who.html index bb57110e..4c9c218a 100644 --- a/applications/examples/views/default/who.html +++ b/applications/examples/views/default/who.html @@ -25,6 +25,7 @@

  • Alexey Nezhdanov (GAE and database performance) +
  • Alan Etkin (DAL IMAP adapter)
  • Alvaro Justen (dynamical translations)
  • Anders Roos (file locking)
  • Andrew Willimott (documentation) @@ -32,6 +33,7 @@

  • Anthony Bastardi (book, poweredby site, multiple contributions)
  • Arun K. Rajeevan (plugin_wiki)
  • Attila Csipa (cron job) +
  • Ben Goosman (keyed table and Oracle adapter)
  • Bill Ferrett (modular DAL design)
  • Boris Manojlovic (ajax edit)
  • Branko Vukelic (new admin app) From ea7868f4a4785923ae0a8e7d889238d68b0f61a9 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Tue, 24 Jan 2012 08:32:14 -0600 Subject: [PATCH 32/77] issue 632, make_min_web2py on windws, thanks mweissen --- VERSION | 2 +- scripts/make_min_web2py.py | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/VERSION b/VERSION index 952b0a4c..cdb60005 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-24 08:31:01) stable +Version 1.99.4 (2012-01-24 08:32:11) stable diff --git a/scripts/make_min_web2py.py b/scripts/make_min_web2py.py index 410e10e2..b98bd4b0 100644 --- a/scripts/make_min_web2py.py +++ b/scripts/make_min_web2py.py @@ -41,39 +41,48 @@ import sys, os, shutil, glob def main(): + global REQUIRED, IGNORED + if len(sys.argv)<2: print USAGE # make target folder target = sys.argv[1] os.mkdir(target) + + # change to os specificsep + REQUIRED = REQUIRED.replace('/',os.sep) + IGNORED = IGNORED.replace('/',os.sep) + # make a list of all files to include files = [x.strip() for x in REQUIRED.split('\n') \ if x and not x[0]=='#'] ignore = [x.strip() for x in IGNORED.split('\n') \ if x and not x[0]=='#'] + def accept(filename): for p in ignore: if filename.startswith(p): return False return True - pattern = 'gluon/*.py' + pattern = os.path.join('gluon','*.py') while True: newfiles = [x for x in glob.glob(pattern) if accept(x)] if not newfiles: break files += newfiles - pattern = pattern[:-3]+'/*.py' + pattern = os.path.join(pattern[:-3],'*.py') # copy all files, make missing folder, build default.py files.sort() + defaultpy = os.path.join('applications','welcome','controllers','default.py') for f in files: dirs = f.split(os.path.sep) for i in range(1,len(dirs)): - try: os.mkdir(target+'/'+os.path.join(*dirs[:i])) + try: os.mkdir(target+os.sep+os.path.join(*dirs[:i])) except OSError: pass - if f=='applications/welcome/controllers/default.py': - open(target+'/'+f,'w').write('def index(): return "hello"\n') + if f==defaultpy: + open(os.path.join(target,f),'w').write('def index(): return "hello"\n') else: - shutil.copyfile(f,target+'/'+f) + shutil.copyfile(f,os.path.join(target,f)) if __name__=='__main__': main() From c0a63aeaa87ed1bf0ac13a4621cf8707effbadb7 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Tue, 24 Jan 2012 08:37:42 -0600 Subject: [PATCH 33/77] issue 634, thanks spametki --- VERSION | 2 +- gluon/dal.py | 4 ++-- gluon/sqlhtml.py | 23 +++++++++++++++-------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/VERSION b/VERSION index cdb60005..e5ed0b4e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-24 08:32:11) stable +Version 1.99.4 (2012-01-24 08:37:21) stable diff --git a/gluon/dal.py b/gluon/dal.py index 94cfec3b..2acbc58d 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -3883,9 +3883,9 @@ def __init__(self,db,uri='mongodb://127.0.0.1:5984/db', def connect(uri=self.uri,m=m): try: return pymongo.Connection(uri)[m.get('database')] - except pymongo.errors.ConnectionFailure as inst: + except pymongo.errors.ConnectionFailure, inst: raise SyntaxError, "The connection to " + uri + " could not be made" - except Exception as inst: + except Exception, inst: 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: diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 3951ab33..ddf790b3 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -767,6 +767,9 @@ def __init__( self.custom.widget = Storage() self.custom.linkto = Storage() + # default id field name + self.id_field_name = table._id.name + sep = separator or '' for fieldname in self.fields: @@ -798,6 +801,10 @@ def __init__( self.custom.dspval.id = nbsp self.custom.inpval.id = '' widget = '' + + # store the id field name (for legacy databases) + self.id_field_name = field.name + if record: if showid and field.name in record and field.readable: v = record[field.name] @@ -893,7 +900,7 @@ def __init__( rfld = table._db[rtable][rfield] query = urllib.quote('%s.%s==%s' % (db,rfld,record[rfld.type[10:].split('.')[1]])) else: - query = urllib.quote('%s.%s==%s' % (db,table._db[rtable][rfield],record.id)) + query = urllib.quote('%s.%s==%s' % (db,table._db[rtable][rfield],record[self.id_field_name])) lname = olname = '%s.%s' % (rtable, rfield) if ofields and not olname in ofields: continue @@ -1038,7 +1045,7 @@ def accepts( record_id = dict((k, request_vars.get(k,None)) \ for k in self.table._primarykey) else: - (formname_id, record_id) = (self.record.id, + (formname_id, record_id) = (self.record[self.id_field_name], request_vars.get('id', None)) keepvalues = True else: @@ -1139,7 +1146,7 @@ def accepts( '%s != %s' % (record_id, self.record_id) if record_id and dbio and not keyed: - self.vars.id = self.record.id + self.vars.id = self.record[self.id_field_name] if self.deleted and self.custom.deletable: if dbio: @@ -1148,7 +1155,7 @@ def accepts( [self.table[k] == record_id[k] \ for k in self.table._primarykey]) else: - qry = self.table._id == self.record.id + qry = self.table._id == self.record[self.id_field_name] self.table._db(qry).delete() self.errors.clear() for component in self.elements('input, select, textarea'): @@ -1259,9 +1266,9 @@ def accepts( ret = False else: if record_id: - self.vars.id = self.record.id + self.vars.id = self.record[self.id_field_name] if fields: - self.table._db(self.table._id == self.record.id).update(**fields) + self.table._db(self.table._id == self.record[self.id_field_name]).update(**fields) else: self.vars.id = self.table.insert(**fields) self.accepted = ret @@ -1595,7 +1602,7 @@ def buttons(edit=False,view=False,record=None): table = db[request.args[-2]] if ondelete: ondelete(table,request.args[-1]) - ret = db(table.id==request.args[-1]).delete() + ret = db(table[self.id_field_name]==request.args[-1]).delete() return ret elif csv and len(request.args)>0 and request.args[-1]=='csv': if request.vars.keywords: @@ -2124,7 +2131,7 @@ def __init__( _class = 'odd' if not selectid is None: #new implement - if record.id==selectid: + if record[self.id_field_name]==selectid: _class += ' rowselected' for colname in columns: From c67d95c92802c520d525ae2ae43f2be895668e38 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Tue, 24 Jan 2012 21:15:20 -0600 Subject: [PATCH 34/77] string query and ingore_common_filters --- VERSION | 2 +- gluon/dal.py | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/VERSION b/VERSION index e5ed0b4e..3d8c6003 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-24 08:37:21) stable +Version 1.99.4 (2012-01-24 21:14:55) stable diff --git a/gluon/dal.py b/gluon/dal.py index 2acbc58d..76167408 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -1316,7 +1316,8 @@ def response(sql): def _count(self, query, distinct=None): tablenames = self.tables(query) if query: - if not query.ignore_common_filters: + if hasattr(query,'ignore_common_filters') and \ + not query.ignore_common_filters: query = self.common_filter(query, tablenames) sql_w = ' WHERE ' + self.expand(query) else: @@ -3503,8 +3504,10 @@ def select_raw(self,query,fields=None,attributes=None): else: raise SyntaxError, "Unable to determine a tablename" - if query and not query.ignore_common_filters: - query = self.common_filter(query,[tablename]) + if query: + if hasattr(query,'ignore_common_filters') and \ + not query.ignore_common_filters: + query = self.common_filter(query,[tablename]) tableobj = self.db[tablename]._tableobj items = tableobj.all() @@ -4830,7 +4833,8 @@ def _select(self,query,fields,attributes): rows """ - if not query.ignore_common_filters: + if hasattr(query,'ignore_common_filters') and \ + not query.ignore_common_filters: query = self.common_filter(query, [self.get_query_mailbox(query),]) # move this statement elsewhere (upper-level) @@ -5048,7 +5052,8 @@ def select(self,query,fields,attributes): def update(self, tablename, query, fields): # print "_update" - if not query.ignore_common_filters: + if hasattr(query,'ignore_common_filters') and \ + not query.ignore_common_filters: query = self.common_filter(query, [tablename,]) mark = [] @@ -5093,7 +5098,8 @@ def count(self,query,distinct=None): counter = 0 tablename = self.get_query_mailbox(query) if query and tablename is not None: - if not query.ignore_common_filters: + if hasattr(query,'ignore_common_filters') and \ + not query.ignore_common_filters: query = self.common_filter(query, [tablename,]) # print "Selecting mailbox ..." result, data = self.connection.select(self.connection.mailbox_names[tablename]) @@ -5108,7 +5114,8 @@ def delete(self, tablename, query): counter = 0 if query: # print "Selecting mailbox ..." - if not query.ignore_common_filters: + if hasattr(query,'ignore_common_filters') and \ + not query.ignore_common_filters: query = self.common_filter(query, [tablename,]) result, data = self.connection.select(self.connection.mailbox_names[tablename]) # print "Retrieving sequence numbers remotely" From 937559bdfb2e5faa2f81cd4bc63ab5c67fbb56e4 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Thu, 26 Jan 2012 10:58:53 -0600 Subject: [PATCH 35/77] possible fix for issue 619 --- VERSION | 2 +- gluon/sqlhtml.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 3d8c6003..aa53d6f0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-24 21:14:55) stable +Version 1.99.4 (2012-01-26 10:58:26) stable diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index ddf790b3..941695e2 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1963,7 +1963,7 @@ def index(): else: del kwargs[key] for tablename,fieldname in table._referenced_by: - id_field_name = db[tablename]._id.name + id_field_name = table._id.name if linked_tables is None or tablename in linked_tables: args0 = tablename+'.'+fieldname links.append( From 96d6791df5444e31408a07ef24e6f89209b6eb66 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Thu, 26 Jan 2012 11:06:30 -0600 Subject: [PATCH 36/77] issue 635 nbjahan --- VERSION | 2 +- gluon/dal.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index aa53d6f0..435e1d55 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-26 10:58:26) stable +Version 1.99.4 (2012-01-26 11:06:17) stable diff --git a/gluon/dal.py b/gluon/dal.py index 76167408..63619b6e 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -2270,7 +2270,8 @@ def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', except SyntaxError, e: logger.error('NdGpatch error') raise e - cnxn = 'DSN=%s' % dsn + # was cnxn = 'DSN=%s' % dsn + cnxn = dsn else: m = re.compile('^(?P[^:@]+)(\:(?P[^@]*))?@(?P[^\:/]+)(\:(?P[0-9]+))?/(?P[^\?]+)(\?(?P.*))?$').match(uri) if not m: From 5ae682b703afdd04bb80ee9955b4acb976c80e50 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Thu, 26 Jan 2012 11:12:52 -0600 Subject: [PATCH 37/77] possible fix for issue 637 --- VERSION | 2 +- gluon/validators.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index 435e1d55..0d1f5cb5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-26 11:06:17) stable +Version 1.99.4 (2012-01-26 11:12:37) stable diff --git a/gluon/validators.py b/gluon/validators.py index 515cbce4..276013da 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -539,15 +539,16 @@ def __call__(self, value): if value in self.allowed_override: return (value, None) (tablename, fieldname) = str(self.field).split('.') - field = self.dbset.db[tablename][fieldname] + table = self.dbset.db[tablename] + field = table[fieldname] rows = self.dbset(field == value, ignore_common_filters = self.ignore_common_filters).select(limitby=(0, 1)) if len(rows) > 0: if isinstance(self.record_id, dict): for f in self.record_id: if str(getattr(rows[0], f)) != str(self.record_id[f]): return (value, translate(self.error_message)) - elif str(rows[0]._id) != str(self.record_id): - return (value, translate(self.error_message)) + elif str(rows[0][table._id.name]) != str(self.record_id): + return (value, translate(self.error_message)) return (value, None) From 3c88491a81ba012df0dc1086ed3c966d4c8b42db Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Thu, 26 Jan 2012 12:53:24 -0600 Subject: [PATCH 38/77] improved ignore_common_filters handling --- VERSION | 2 +- gluon/dal.py | 30 +++++++++++++----------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/VERSION b/VERSION index 0d1f5cb5..e3688f5b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-26 11:12:37) stable +Version 1.99.4 (2012-01-26 12:53:05) stable diff --git a/gluon/dal.py b/gluon/dal.py index 63619b6e..8896636e 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -1104,7 +1104,7 @@ def close(self): def _update(self, tablename, query, fields): if query: - if not query.ignore_common_filters: + if use_common_filters(query): query = self.common_filter(query, [tablename]) sql_w = ' WHERE ' + self.expand(query) else: @@ -1123,7 +1123,7 @@ def update(self, tablename, query, fields): def _delete(self, tablename, query): if query: - if not query.ignore_common_filters: + if use_common_filters(query): query = self.common_filter(query, [tablename]) sql_w = ' WHERE ' + self.expand(query) else: @@ -1191,7 +1191,7 @@ def _select(self, query, fields, attributes): if not tablename in tablenames: tablenames.append(tablename) - if query and not query.ignore_common_filters: + if use_common_filters(query): query = self.common_filter(query,tablenames) if len(tablenames) < 1: @@ -1316,8 +1316,7 @@ def response(sql): def _count(self, query, distinct=None): tablenames = self.tables(query) if query: - if hasattr(query,'ignore_common_filters') and \ - not query.ignore_common_filters: + if use_common_filters(query): query = self.common_filter(query, tablenames) sql_w = ' WHERE ' + self.expand(query) else: @@ -3506,8 +3505,7 @@ def select_raw(self,query,fields=None,attributes=None): raise SyntaxError, "Unable to determine a tablename" if query: - if hasattr(query,'ignore_common_filters') and \ - not query.ignore_common_filters: + if use_common_filters(query): query = self.common_filter(query,[tablename]) tableobj = self.db[tablename]._tableobj @@ -4834,8 +4832,7 @@ def _select(self,query,fields,attributes): rows """ - if hasattr(query,'ignore_common_filters') and \ - not query.ignore_common_filters: + if use_common_filters(query): query = self.common_filter(query, [self.get_query_mailbox(query),]) # move this statement elsewhere (upper-level) @@ -5053,8 +5050,7 @@ def select(self,query,fields,attributes): def update(self, tablename, query, fields): # print "_update" - if hasattr(query,'ignore_common_filters') and \ - not query.ignore_common_filters: + if use_common_filters(query): query = self.common_filter(query, [tablename,]) mark = [] @@ -5099,8 +5095,7 @@ def count(self,query,distinct=None): counter = 0 tablename = self.get_query_mailbox(query) if query and tablename is not None: - if hasattr(query,'ignore_common_filters') and \ - not query.ignore_common_filters: + if use_common_filters(query): query = self.common_filter(query, [tablename,]) # print "Selecting mailbox ..." result, data = self.connection.select(self.connection.mailbox_names[tablename]) @@ -5115,8 +5110,7 @@ def delete(self, tablename, query): counter = 0 if query: # print "Selecting mailbox ..." - if hasattr(query,'ignore_common_filters') and \ - not query.ignore_common_filters: + if use_common_filters(query): query = self.common_filter(query, [tablename,]) result, data = self.connection.select(self.connection.mailbox_names[tablename]) # print "Retrieving sequence numbers remotely" @@ -7305,6 +7299,8 @@ def xorify(orderby): orderby2 = orderby2 | item return orderby2 +def use_common_filters(query): + return (query and hasattr(query,'ignore_common_filters') and not query.ignore_common_filters) class Set(object): @@ -7326,8 +7322,8 @@ class Set(object): def __init__(self, db, query, ignore_common_filters = None): self.db = db self._db = db # for backward compatibility - if query and not ignore_common_filters is None and \ - query.ignore_common_filters != ignore_common_filters: + if not ignore_common_filters is None and \ + use_common_filters(query) == ignore_common_filters: query = copy.copy(query) query.ignore_common_filters = ignore_common_filters self.query = query From e7e5358977870fdc5d1d6f0aaaaf44bae42d0437 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sun, 29 Jan 2012 15:14:45 -0600 Subject: [PATCH 39/77] auth.mygroups --- VERSION | 2 +- gluon/tools.py | 28 ++++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/VERSION b/VERSION index e3688f5b..05fe4b0f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-26 12:53:05) stable +Version 1.99.4 (2012-01-29 15:13:44) stable diff --git a/gluon/tools.py b/gluon/tools.py index 1edd1245..9f663f02 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -872,6 +872,7 @@ def __init__(self, environment=None, db=None, mailer=True, request = current.request session = current.session auth = session.auth + self.mygroups = auth.mygroups or {} if auth and auth.last_visit and auth.last_visit + \ datetime.timedelta(days=0, seconds=auth.expiration) > request.now: self.user = auth.user @@ -1517,6 +1518,7 @@ def login_bare(self, username, password): expiration=self.settings.expiration, hmac_key = web2py_uuid()) self.user = user + self.update_groups() return user else: # user not in database try other login methods @@ -1807,6 +1809,8 @@ def login( self.log_event(log, user) session.flash = self.messages.logged_in + self.update_groups() + # how to continue if self.settings.login_form == self: if accepted_form: @@ -1951,7 +1955,8 @@ def register( session.auth = Storage(user=user, last_visit=request.now, expiration=self.settings.expiration, hmac_key = web2py_uuid()) - self.user = user + self.user = user + self.update_groups() session.flash = self.messages.logged_in self.log_event(log, form.vars) callback(onaccept,form) @@ -2485,6 +2490,17 @@ def impersonate(self, user_id=DEFAULT): return SQLFORM.factory(Field('user_id', 'integer')) return self.user + def update_groups(self): + if not self.user: + return + mygroups = self.mygroups = current.session.auth.mygroups = {} + memberships = self.db(self.settings.table_membership.user_id + == self.user.id).select() + for membership in memberships: + group = self.settings.table_group(membership.group_id) + if group: + mygroups[membership.group_id] = group.role + def groups(self): """ displays the groups and their roles for the logged in user @@ -2607,6 +2623,7 @@ def del_group(self, group_id): self.db(self.settings.table_group.id == group_id).delete() self.db(self.settings.table_membership.group_id == group_id).delete() self.db(self.settings.table_permission.group_id == group_id).delete() + self.update_groups() self.log_event(self.messages.del_group_log,dict(group_id=group_id)) def id_group(self, role): @@ -2669,6 +2686,7 @@ def add_membership(self, group_id=None, user_id=None, role=None): return record.id else: id = membership.insert(group_id=group_id, user_id=user_id) + self.update_groups() self.log_event(self.messages.add_membership_log, dict(user_id=user_id, group_id=group_id)) return id @@ -2685,9 +2703,11 @@ def del_membership(self, group_id, user_id=None, role=None): membership = self.settings.table_membership self.log_event(self.messages.del_membership_log, dict(user_id=user_id,group_id=group_id)) - return self.db(membership.user_id - == user_id)(membership.group_id - == group_id).delete() + ret = self.db(membership.user_id + == user_id)(membership.group_id + == group_id).delete() + self.update_groups() + return ret def has_permission( self, From b3b5c35267034ac959cd7cc4337964ae2a29f5e2 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sun, 29 Jan 2012 15:16:18 -0600 Subject: [PATCH 40/77] added http://www.wadecybertech.com to support --- VERSION | 2 +- applications/examples/views/default/support.html | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 05fe4b0f..93ce4afb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-29 15:13:44) stable +Version 1.99.4 (2012-01-29 15:16:03) stable diff --git a/applications/examples/views/default/support.html b/applications/examples/views/default/support.html index 7dfd9098..00d593ed 100644 --- a/applications/examples/views/default/support.html +++ b/applications/examples/views/default/support.html @@ -17,6 +17,7 @@

    Affiliated Companies

    • MetaCryption, LLC (USA)
    • Secution, Inc (USA)
    • +
    • Wade Cybertech (USA)
    • Blouweb Consultoria Digital (Brasil)
    • Tecnodoc (Argentina)
    • OneMeWebServices (Canada)
    • From 77e88893987f1e416d116dc94d207e4b73472db3 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sun, 29 Jan 2012 15:17:14 -0600 Subject: [PATCH 41/77] new redic cache, thanks nihlod --- VERSION | 2 +- gluon/contrib/redis_cache.py | 103 ++++++++++++++++++++++++----------- 2 files changed, 72 insertions(+), 33 deletions(-) diff --git a/VERSION b/VERSION index 93ce4afb..d74d7979 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-29 15:16:03) stable +Version 1.99.4 (2012-01-29 15:17:09) stable diff --git a/gluon/contrib/redis_cache.py b/gluon/contrib/redis_cache.py index cabccb5b..fa4bfd2f 100644 --- a/gluon/contrib/redis_cache.py +++ b/gluon/contrib/redis_cache.py @@ -4,21 +4,24 @@ """ import redis +from redis.exceptions import ConnectionError from gluon import current from gluon.cache import CacheAbstract import cPickle as pickle import time import re - +import logging import thread +logger = logging.getLogger("web2py.cache.redis") + locker = thread.allocate_lock() def RedisCache(*args, **vars): """ Usage example: put in models - from gluon.contrib.redis import RedisCache + from gluon.contrib.redis_cache import RedisCache cache.redis = RedisCache('localhost:6379',db=None, debug=True) cache.redis.stats() @@ -42,14 +45,17 @@ def RedisCache(*args, **vars): class RedisClient(object): meta_storage = {} - + MAX_RETRIES = 5 + RETRIES = 0 def __init__(self, server='localhost:6379', db=None, debug=False): - host,port = (address.split(':')+['6379'])[:2] + self.server = server + self.db = db or 0 + host,port = (self.server.split(':')+['6379'])[:2] port = int(port) - self.request=current.request + self.request = current.request self.debug = debug - if request: - app = request.application + if self.request: + app = self.request.application else: app = '' @@ -61,36 +67,69 @@ def __init__(self, server='localhost:6379', db=None, debug=False): }} else: self.storage = self.meta_storage[app] - - self.r_server = redis.Redis(host=host, port=port, db=db or 0) + + self.r_server = redis.Redis(host=host, port=port, db=self.db) def __call__(self, key, f, time_expire=300): - if time_expire == None: - time_expire = 10**10 - key = self.__keyFormat__(key) - value = None - obj = self.r_server.get(key) - if obj: - if self.debug: - self.r_server.incr('web2py_cache_statistics:hit_total') - value = pickle.loads(obj) - elif f is None: - if obj: self.r_server.delete(key) + try: + if time_expire == None: + time_expire = 24*60*60 + newKey = self.__keyFormat__(key) + value = None + obj = self.r_server.get(newKey) + ttl = self.r_server.ttl(newKey) or 0 + if ttl > time_expire: + obj = None + if obj: + if self.debug: + self.r_server.incr('web2py_cache_statistics:hit_total') + value = pickle.loads(obj) + elif f is None: + self.r_server.delete(newKey) + else: + if self.debug: + self.r_server.incr('web2py_cache_statistics:misses') + value = f() + if time_expire == 0: + time_expire = 1 + self.r_server.setex(newKey, pickle.dumps(value), time_expire) + return value + except ConnectionError: + return self.retry_call(key, f, time_expire) + + def retry_call(self, key, f, time_expire): + self.RETRIES += 1 + if self.RETRIES <= self.MAX_RETRIES: + logger.error("sleeping %s seconds before reconnecting" % (2 * self.RETRIES)) + time.sleep(2 * self.RETRIES) + self.__init__(self.server, self.db, self.debug) + return self.__call__(key, f, time_expire) else: - if self.debug: - self.r_server.incr('web2py_cache_statistics:misses') - value = f() - self.r_server.setex(key, pickle.dumps(value), time_expire) - return value - + self.RETRIES = 0 + raise ConnectionError , 'Redis instance is unavailable at %s' % (self.server) + def increment(self, key, value=1, time_expire=300): - newKey = self.__keyFormat__(key) - obj = self.r_server.get(newKey) - if obj: - return self.r_server.incr(newKey, value) + try: + newKey = self.__keyFormat__(key) + obj = self.r_server.get(newKey) + if obj: + return self.r_server.incr(newKey, value) + else: + self.r_server.setex(newKey, value, time_expire) + return value + except ConnectionError: + return self.retry_increment(key, value, time_expire) + + def retry_increment(self, key, value, time_expire): + self.RETRIES += 1 + if self.RETRIES <= self.MAX_RETRIES: + logger.error("sleeping some seconds before reconnecting") + time.sleep(2 * self.RETRIES) + self.__init__(self.server, self.db, self.debug) + return self.increment(key, value, time_expire) else: - self.r_server.setex(newKey, value, time_expire) - return value + self.RETRIES = 0 + raise ConnectionError , 'Redis instance is unavailable at %s' % (self.server) def clear(self, regex): """ From 4db6337faef0d6a916010d8e28878ee2862b9da3 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sun, 29 Jan 2012 15:21:35 -0600 Subject: [PATCH 42/77] http://en.wikipedia.org/wiki/One-time_password, thanks Madhukar R Pai --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index d74d7979..27ed510d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-29 15:17:09) stable +Version 1.99.4 (2012-01-29 15:21:17) stable From 9264fe7ec743044f61d6f7b9c7a297467e94dfd2 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sun, 29 Jan 2012 15:22:11 -0600 Subject: [PATCH 43/77] http://en.wikipedia.org/wiki/One-time_password, thanks Madhukar R Pai --- VERSION | 2 +- gluon/contrib/login_methods/motp_auth.py | 104 +++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 gluon/contrib/login_methods/motp_auth.py diff --git a/VERSION b/VERSION index 27ed510d..06ba1e44 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-29 15:21:17) stable +Version 1.99.4 (2012-01-29 15:22:07) stable diff --git a/gluon/contrib/login_methods/motp_auth.py b/gluon/contrib/login_methods/motp_auth.py new file mode 100644 index 00000000..a776b3e4 --- /dev/null +++ b/gluon/contrib/login_methods/motp_auth.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python + +import time +from hashlib import md5 +from gluon.dal import DAL + +def motp_auth(db=DAL('sqlite://storage.sqlite'), + time_offset=60): + + """ + motp allows you to login with a one time password(OTP) generated on a motp client, + motp clients are available for practically all platforms. + to know more about OTP visit http://en.wikipedia.org/wiki/One-time_password + to know more visit http://motp.sourceforge.net + + + Written by Madhukar R Pai (madspai@gmail.com) + License : MIT or GPL v2 + + thanks and credits to the web2py community + + to use motp_auth: + motp_auth.py has to be located in gluon/contrib/login_methods/ folder + first auth_user has to have 2 extra fields - motp_secret and motp_pin + for that define auth like shown below: + + ## after auth = Auth(db) + db.define_table( + auth.settings.table_user_name, + Field('first_name', length=128, default=''), + Field('last_name', length=128, default=''), + Field('email', length=128, default='', unique=True), # required + Field('password', 'password', length=512, # required + readable=False, label='Password'), + Field('motp_secret',length=512,default='', + label='MOTP Seceret'), + Field('motp_pin',length=128,default='', + label='MOTP PIN'), + Field('registration_key', length=512, # required + writable=False, readable=False, default=''), + Field('reset_password_key', length=512, # required + writable=False, readable=False, default=''), + Field('registration_id', length=512, # required + writable=False, readable=False, default='')) + + ##validators + custom_auth_table = db[auth.settings.table_user_name] # get the custom_auth_table + custom_auth_table.first_name.requires = \ + IS_NOT_EMPTY(error_message=auth.messages.is_empty) + custom_auth_table.last_name.requires = \ + IS_NOT_EMPTY(error_message=auth.messages.is_empty) + custom_auth_table.password.requires = CRYPT() + custom_auth_table.email.requires = [ + IS_EMAIL(error_message=auth.messages.invalid_email), + IS_NOT_IN_DB(db, custom_auth_table.email)] + + auth.settings.table_user = custom_auth_table # tell auth to use custom_auth_table + ## before auth.define_tables() + + ##after that: + + from gluon.contrib.login_methods.motp_auth import motp_auth + auth.settings.login_methods.append(motp_auth(db=db)) + + ##Instructions for using MOTP + - after configuring motp for web2py, Install a MOTP client on your phone (android,IOS, java, windows phone, etc) + - initialize the motp client (to reset a motp secret type in #**#), + During user creation enter the secret generated during initialization into the motp_secret field in auth_user and + similarly enter a pre-decided pin into the motp_pin + - done.. to login, just generate a fresh OTP by typing in the pin and use the OTP as password + + ###To Dos### + - both motp_secret and pin are stored in plain text! need to have some way of encrypting + - web2py stores the password in db on successful login (should not happen) + - maybe some utility or page to check the otp would be useful + - as of now user field is hardcoded to email. Some way of selecting user table and user field. + """ + + def verify_otp(otp,pin,secret,offset=60): + epoch_time = int(time.time()) + time_start = int(str(epoch_time - offset)[:-1]) + time_end = int(str(epoch_time + offset)[:-1]) + for t in range(time_start-1,time_end+1): + to_hash = str(t)+secret+pin + hash = md5(to_hash).hexdigest()[:6] + if otp == hash: return True + return False + + def motp_auth_aux(email, + password, + db=db, + offset=time_offset): + if db: + user_data = db(db.auth_user.email == email ).select().first() + if user_data: + if user_data['motp_secret'] and user_data['motp_pin']: + motp_secret = user_data['motp_secret'] + motp_pin = user_data['motp_pin'] + otp_check = verify_otp(password,motp_pin,motp_secret,offset=offset) + if otp_check: return True + else: return False + else: return False + return False + return motp_auth_aux From f84238c82771f2b840561bea78046070452ac267 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sun, 29 Jan 2012 15:27:16 -0600 Subject: [PATCH 44/77] some better doctests and response.menu=[(*,*,*,*,condition)], thanks Yair --- VERSION | 2 +- gluon/html.py | 41 +++++++++++++++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/VERSION b/VERSION index 06ba1e44..a5764db9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-29 15:22:07) stable +Version 1.99.4 (2012-01-29 15:27:03) stable diff --git a/gluon/html.py b/gluon/html.py index ee98d17b..26cc7f65 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -148,6 +148,7 @@ def URL( host=None, port=None, encode_embedded_slash=False, + url_encode=True ): """ generate a URL @@ -179,6 +180,24 @@ def URL( >>> str(URL(a='a', c='c', f='f', args=['w/x', 'y/z'], encode_embedded_slash=True)) '/a/c/f/w%2Fx/y%2Fz' + >>> str(URL(a='a', c='c', f='f', args=['%(id)d'], url_encode=False)) + '/a/c/f/%(id)d' + + >>> str(URL(a='a', c='c', f='f', args=['%(id)d'], url_encode=True)) + '/a/c/f/%25%28id%29d' + + >>> str(URL(a='a', c='c', f='f', vars={'id' : '%(id)d' }, url_encode=False)) + '/a/c/f?id=%(id)d' + + >>> str(URL(a='a', c='c', f='f', vars={'id' : '%(id)d' }, url_encode=True)) + '/a/c/f?id=%25%28id%29d' + + >>> str(URL(a='a', c='c', f='f', anchor='%(id)d', url_encode=False)) + '/a/c/f#%(id)d' + + >>> str(URL(a='a', c='c', f='f', anchor='%(id)d', url_encode=True)) + '/a/c/f#%25%28id%29d' + generates a url '/a/c/f' corresponding to application a, controller c and function f. If r=request is passed, a, c, f are set, respectively, to r.application, r.controller, r.function. @@ -251,10 +270,13 @@ def URL( args = [args] if args: - if encode_embedded_slash: - other = '/' + '/'.join([urllib.quote(str(x), '') for x in args]) + if url_encode: + if encode_embedded_slash: + other = '/' + '/'.join([urllib.quote(str(x), '') for x in args]) + else: + other = args and urllib.quote('/' + '/'.join([str(x) for x in args])) else: - other = args and urllib.quote('/' + '/'.join([str(x) for x in args])) + other = args and ('/' + '/'.join([str(x) for x in args])) else: other = '' @@ -298,9 +320,15 @@ def URL( list_vars.append(('_signature', sig)) if list_vars: - other += '?%s' % urllib.urlencode(list_vars) + if url_encode: + other += '?%s' % urllib.urlencode(list_vars) + else: + other += '?%s' % '&'.join([var[0]+'='+var[1] for var in list_vars]) if anchor: - other += '#' + urllib.quote(str(anchor)) + if url_encode: + other += '#' + urllib.quote(str(anchor)) + else: + other += '#' + (str(anchor)) if extension: function += '.' + extension @@ -2090,7 +2118,8 @@ def serialize(self, data, level=0): li['_class'] = li['_class']+' '+self['li_active'] else: li['_class'] = self['li_active'] - ul.append(li) + if len(item) <= 4 or item[4] == True: + ul.append(li) return ul def serialize_mobile(self, data, select=None, prefix=''): From 891f04106b87f3826ad090ca82ec6c187014ff7a Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sun, 29 Jan 2012 15:53:25 -0600 Subject: [PATCH 45/77] fixed probelm in auth.mygroups --- VERSION | 2 +- applications/examples/views/default/support.html | 2 +- gluon/tools.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index a5764db9..f2292126 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-29 15:27:03) stable +Version 1.99.4 (2012-01-29 15:53:22) stable diff --git a/applications/examples/views/default/support.html b/applications/examples/views/default/support.html index 00d593ed..807074ec 100644 --- a/applications/examples/views/default/support.html +++ b/applications/examples/views/default/support.html @@ -17,7 +17,7 @@

      Affiliated Companies

      • MetaCryption, LLC (USA)
      • Secution, Inc (USA)
      • -
      • Wade Cybertech (USA)
      • +
      • Wade Cybertech (Canada)
      • Blouweb Consultoria Digital (Brasil)
      • Tecnodoc (Argentina)
      • OneMeWebServices (Canada)
      • diff --git a/gluon/tools.py b/gluon/tools.py index 9f663f02..432adb13 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -872,7 +872,7 @@ def __init__(self, environment=None, db=None, mailer=True, request = current.request session = current.session auth = session.auth - self.mygroups = auth.mygroups or {} + self.mygroups = auth and auth.mygroups or {} if auth and auth.last_visit and auth.last_visit + \ datetime.timedelta(days=0, seconds=auth.expiration) > request.now: self.user = auth.user From 209f88b7b50dba068e4365f4bafc721a58eef6bf Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sun, 29 Jan 2012 16:19:11 -0600 Subject: [PATCH 46/77] fixed 627 --- VERSION | 2 +- gluon/sqlhtml.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/VERSION b/VERSION index f2292126..7f279823 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-29 15:53:22) stable +Version 1.99.4 (2012-01-29 16:18:48) stable diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 941695e2..dd09d812 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1352,12 +1352,12 @@ def search_menu(fields,search_options=None): _class='w2p_query_row hidden')) criteria.insert(0,SELECT( _id="w2p_query_fields", - _onchange="jQuery('.w2p_query_row').hide();jQuery('#w2p_field_'+jQuery(this).val().replace('.','-')).show();", + _onchange="jQuery('.w2p_query_row').hide();jQuery('#w2p_field_'+jQuery('#w2p_query_fields').val().replace('.','-')).show();", *[OPTION(label, _value=fname) for fname,label in selectfields])) fadd = SCRIPT(""" jQuery('#w2p_query_panel input,#w2p_query_panel select').css( 'width','auto').css('float','left'); - jQuery(function(){web2py_ajax_fields();}); + jQuery(function(){web2py_ajax_fields('#w2p_query_panel');}); function w2p_build_query(aggregator,a){ var b=a.replace('.','-'); var option = jQuery('#w2p_field_'+b+' select').val(); @@ -1366,14 +1366,13 @@ def search_menu(fields,search_options=None): var k=jQuery('#web2py_keywords'); var v=k.val(); if(aggregator=='new') k.val(s); else k.val((v?(v+' '+ aggregator +' '):'')+s); - jQuery('#w2p_query_fields').val('').change(); - jQuery('#w2p_query_panel').slideUp(); - } + jQuery('#w2p_query_panel').slideUp(); + } """) return CAT( INPUT( _value=T("Query"),_type="button",_id="w2p_query_trigger", - _onclick="jQuery('#w2p_query_fields').val('');jQuery('#w2p_query_panel').slideToggle();"), + _onclick="jQuery('#w2p_query_fields').change();jQuery('#w2p_query_panel').slideToggle();"), DIV(_id="w2p_query_panel", _style='position:absolute;z-index:1000', _class='hidden', From 3ba5e4520368bd3be458124c58390772be4b95bd Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sun, 29 Jan 2012 16:25:15 -0600 Subject: [PATCH 47/77] .webm, thanks Jonathan --- VERSION | 2 +- gluon/contenttype.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 7f279823..b68c9721 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-29 16:18:48) stable +Version 1.99.4 (2012-01-29 16:24:43) stable diff --git a/gluon/contenttype.py b/gluon/contenttype.py index 8110d6db..48b9721e 100644 --- a/gluon/contenttype.py +++ b/gluon/contenttype.py @@ -633,6 +633,7 @@ '.wbmp': 'image/vnd.wap.wbmp', '.wcm': 'application/vnd.ms-works', '.wdb': 'application/vnd.ms-works', + '.webm': 'video/webm', '.wk1': 'application/vnd.lotus-1-2-3', '.wk3': 'application/vnd.lotus-1-2-3', '.wk4': 'application/vnd.lotus-1-2-3', From eb259bd8be1848e80c6330c32dd15fbb4546e051 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sun, 29 Jan 2012 23:26:59 -0600 Subject: [PATCH 48/77] mygroups -> user_groups, thanks Bruno --- VERSION | 2 +- gluon/tools.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/VERSION b/VERSION index b68c9721..201af844 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-29 16:24:43) stable +Version 1.99.4 (2012-01-29 23:26:39) stable diff --git a/gluon/tools.py b/gluon/tools.py index 432adb13..78fa1a5b 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -872,7 +872,7 @@ def __init__(self, environment=None, db=None, mailer=True, request = current.request session = current.session auth = session.auth - self.mygroups = auth and auth.mygroups or {} + self.user_groups = auth and auth.user_groups or {} if auth and auth.last_visit and auth.last_visit + \ datetime.timedelta(days=0, seconds=auth.expiration) > request.now: self.user = auth.user @@ -1320,7 +1320,7 @@ def define_tables(self, username=False, migrate=True, fake_migrate=False): Field('role', length=512, default='', label=self.messages.label_role), Field('description', 'text', - label=self.messages.label_description), + label=self.messages.label_description), *settings.extra_fields.get(settings.table_group_name,[]), **dict( migrate=self.__get_migrate( @@ -2493,13 +2493,13 @@ def impersonate(self, user_id=DEFAULT): def update_groups(self): if not self.user: return - mygroups = self.mygroups = current.session.auth.mygroups = {} + user_groups = self.user_groups = current.session.auth.user_groups = {} memberships = self.db(self.settings.table_membership.user_id == self.user.id).select() for membership in memberships: group = self.settings.table_group(membership.group_id) if group: - mygroups[membership.group_id] = group.role + user_groups[membership.group_id] = group.role def groups(self): """ From 26b4d7b4251d7cab806e54920089bc2737d9e73e Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 30 Jan 2012 10:06:41 -0600 Subject: [PATCH 49/77] minor typo --- VERSION | 2 +- gluon/widget.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 201af844..276eb4a4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-29 23:26:39) stable +Version 1.99.4 (2012-01-30 10:04:58) stable diff --git a/gluon/widget.py b/gluon/widget.py index 492e5440..4baed549 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -1,4 +1,4 @@ -#!/-usr/bin/env python +#!/usr/bin/env python # -*- coding: utf-8 -*- """ From abc4109364416f59b5d5d0461bfd05cc1fc167ce Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 30 Jan 2012 11:15:38 -0600 Subject: [PATCH 50/77] gitignore --- .gitignore | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ VERSION | 2 +- 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0725e4c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +.* +*.pyc +*.pyo +*~ +#* +*.1 +*.bak +*.bak2 +*.svn +*.w2p +*.class +*.rej +*.orig +Thumbs.db +.DS_Store +index.yaml +routes.py +logging.conf +gluon/tests/VERSION +gluon/tests/sql.log +./httpserver.log +./httpserver.pid +./parameters*.py +./deposit +./benchmark +./build +./dist* +./dummy_tests +./optional_contrib +./ssl +./docs +./logs +./*.zip +./gluon/*.1 +./gluon/*.txt +./admin.w2p +./examples.w2p +applications/* +!applications/welcome +!applications/welcome/* +!applications/examples +!applications/examples/* +!applications/admin +!applications/admin/* +applications/*/databases/* +applications/*/sessions/* +applications/*/errors/* +applications/*/cache/* +applications/*/uploads/* +applications/examples/static/epydoc +applications/examples/static/sphinx +applications/admin/cron/cron.master + + diff --git a/VERSION b/VERSION index 276eb4a4..d6a709a5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-30 10:04:58) stable +Version 1.99.4 (2012-01-30 11:15:20) stable From 29f0171f03cb5e86ce9e5028673c81142a25b055 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 30 Jan 2012 11:26:43 -0600 Subject: [PATCH 51/77] https://groups.google.com/group/web2py/browse_thread/thread/ab8aa84d3baeeb3c/, thanks Jonathan --- VERSION | 2 +- gluon/newcron.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index d6a709a5..ea81fdba 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-30 11:15:20) stable +Version 1.99.4 (2012-01-30 11:26:36) stable diff --git a/gluon/newcron.py b/gluon/newcron.py index 8073e71f..1b40023d 100644 --- a/gluon/newcron.py +++ b/gluon/newcron.py @@ -290,6 +290,7 @@ def crondance(applications_parent, ctype='soft', startup=False): w2p_path = fileutils.abspath('web2py.py', gluon=True) if os.path.exists(w2p_path): commands.append(w2p_path) + commands.append('--') if global_settings.applications_parent != global_settings.gluon_parent: commands.extend(('-f', global_settings.applications_parent)) citems = [(k in task and not v in task[k]) for k,v in checks] From 76ff553fb5188be4b5671b3d3f79c7ced7a25e13 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 30 Jan 2012 12:57:18 -0600 Subject: [PATCH 52/77] new cron -- conditional to web2py.py, thanks Jonathan --- VERSION | 2 +- gluon/newcron.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index ea81fdba..703f1ce1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-30 11:26:36) stable +Version 1.99.4 (2012-01-30 12:57:03) stable diff --git a/gluon/newcron.py b/gluon/newcron.py index 1b40023d..5b74d773 100644 --- a/gluon/newcron.py +++ b/gluon/newcron.py @@ -290,7 +290,7 @@ def crondance(applications_parent, ctype='soft', startup=False): w2p_path = fileutils.abspath('web2py.py', gluon=True) if os.path.exists(w2p_path): commands.append(w2p_path) - commands.append('--') + commands.append('--') if global_settings.applications_parent != global_settings.gluon_parent: commands.extend(('-f', global_settings.applications_parent)) citems = [(k in task and not v in task[k]) for k,v in checks] From 89a796b7946c7b6d8d6fe50f88b0f31ac7a70601 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Tue, 31 Jan 2012 08:48:47 -0600 Subject: [PATCH 53/77] no more -- for now --- VERSION | 2 +- gluon/newcron.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 703f1ce1..7d06cae5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-30 12:57:03) stable +Version 1.99.4 (2012-01-31 08:48:24) stable diff --git a/gluon/newcron.py b/gluon/newcron.py index 5b74d773..8073e71f 100644 --- a/gluon/newcron.py +++ b/gluon/newcron.py @@ -290,7 +290,6 @@ def crondance(applications_parent, ctype='soft', startup=False): w2p_path = fileutils.abspath('web2py.py', gluon=True) if os.path.exists(w2p_path): commands.append(w2p_path) - commands.append('--') if global_settings.applications_parent != global_settings.gluon_parent: commands.extend(('-f', global_settings.applications_parent)) citems = [(k in task and not v in task[k]) for k,v in checks] From b832ca68ee22c852ffc869e736c0e44595ec290f Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Wed, 1 Feb 2012 11:36:26 -0600 Subject: [PATCH 54/77] fixed some issues in dal.py and sqlhtml.py thanks Michele --- VERSION | 2 +- gluon/dal.py | 2 +- gluon/sqlhtml.py | 10 +++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/VERSION b/VERSION index 7d06cae5..9a6896d1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-01-31 08:48:24) stable +Version 1.99.4 (2012-02-01 11:35:57) stable diff --git a/gluon/dal.py b/gluon/dal.py index 8896636e..09b9b4a5 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -1930,7 +1930,7 @@ def create_sequence_and_triggers(self, query, table, **args): def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, adapter_args={}): - if not self.drivers.get('psycopg2') and not self.drivers.get('psycopg2'): + if not self.drivers.get('psycopg2') and not self.drivers.get('pg8000'): raise RuntimeError, "Unable to import any drivers (psycopg2 or pg8000)" self.db = db self.dbengine = "postgres" diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index dd09d812..7c83bc5e 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1297,6 +1297,9 @@ def factory(*fields, **attributes): @staticmethod def build_query(fields,keywords): + if isinstance(keywords,(tuple,list)): + keywords = keywords[0] + request.vars.keywords = keywords key = keywords.strip() if key and not ' ' in key and not '"' in key and not "'" in key: SEARCHABLE_TYPES = ('string','text','list:string') @@ -1647,12 +1650,13 @@ def buttons(edit=False,view=False,record=None): console.append(form) keywords = request.vars.get('keywords','') try: - subquery = SQLFORM.build_query(sfields, keywords) + if callable(searchable): + subquery = searchable(sfields, keywords) + else: + subquery = SQLFORM.build_query(sfields, keywords) except RuntimeError: subquery = None error = T('Invalid query') - elif callable(searchable): - subquery = searchable(keywords,fields) else: subquery = None if subquery: From 6f19a5a5a143caec3a2401031706f9f9c8f3b7d1 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Wed, 1 Feb 2012 11:39:20 -0600 Subject: [PATCH 55/77] better gitignore, thanks Jonathan --- .gitignore | 8 +++++--- VERSION | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 0725e4c0..39a6f517 100644 --- a/.gitignore +++ b/.gitignore @@ -18,9 +18,9 @@ routes.py logging.conf gluon/tests/VERSION gluon/tests/sql.log -./httpserver.log -./httpserver.pid -./parameters*.py +httpserver.log +httpserver.pid +parameters*.py ./deposit ./benchmark ./build @@ -35,6 +35,7 @@ gluon/tests/sql.log ./gluon/*.txt ./admin.w2p ./examples.w2p +cron.master applications/* !applications/welcome !applications/welcome/* @@ -47,6 +48,7 @@ applications/*/sessions/* applications/*/errors/* applications/*/cache/* applications/*/uploads/* +applications/*/*.py[oc] applications/examples/static/epydoc applications/examples/static/sphinx applications/admin/cron/cron.master diff --git a/VERSION b/VERSION index 9a6896d1..88408871 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-01 11:35:57) stable +Version 1.99.4 (2012-02-01 11:39:16) stable From c502acf332f20f089f4604e4c0ccd9e3712228f7 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Wed, 1 Feb 2012 11:42:59 -0600 Subject: [PATCH 56/77] issue 622, white-space:normal in grid td, thanks mweissen --- VERSION | 2 +- applications/examples/static/css/web2py.css | 3 ++- applications/welcome/static/css/web2py.css | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 88408871..3fb93737 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-01 11:39:16) stable +Version 1.99.4 (2012-02-01 11:42:36) stable diff --git a/applications/examples/static/css/web2py.css b/applications/examples/static/css/web2py.css index 63a8fcfd..ab9beaf2 100644 --- a/applications/examples/static/css/web2py.css +++ b/applications/examples/static/css/web2py.css @@ -30,6 +30,7 @@ small { font-size: 0.8em; } textarea { width: 600px; } code { font-family: Courier;} input[type=text], input[type=password], select { width: 300px; } +ul { list-style-type: none; margin: 0px; padding: 0px; } /** end **/ /* Sticky footer begin */ @@ -193,7 +194,7 @@ div.error { .web2py_paginator { border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-bottom: 1px solid #ccc; } .web2py_grid a { text-decoration:none;} .web2py_grid table { width: 100% } -.web2py_grid td { white-space:nowrap; } +.web2py_grid td { white-space:normal; } .web2py_grid tbody td { padding: 2px 5px 2px 5px; line-height: 13.5px; diff --git a/applications/welcome/static/css/web2py.css b/applications/welcome/static/css/web2py.css index 934e623c..ab9beaf2 100644 --- a/applications/welcome/static/css/web2py.css +++ b/applications/welcome/static/css/web2py.css @@ -194,7 +194,7 @@ div.error { .web2py_paginator { border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-bottom: 1px solid #ccc; } .web2py_grid a { text-decoration:none;} .web2py_grid table { width: 100% } -.web2py_grid td { white-space:nowrap; } +.web2py_grid td { white-space:normal; } .web2py_grid tbody td { padding: 2px 5px 2px 5px; line-height: 13.5px; From bd3c21b2eb1cbc86e613463740923fc90360b8f6 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Thu, 2 Feb 2012 16:34:35 -0600 Subject: [PATCH 57/77] sync laguages does now saves a UTF8 file, thanks Guruyaya --- VERSION | 2 +- scripts/sync_languages.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 3fb93737..88e35a97 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-01 11:42:36) stable +Version 1.99.4 (2012-02-02 16:34:12) stable diff --git a/scripts/sync_languages.py b/scripts/sync_languages.py index 39dff978..ddea4ff8 100755 --- a/scripts/sync_languages.py +++ b/scripts/sync_languages.py @@ -7,7 +7,7 @@ import shutil import os -from gluon.languages import findT +from gluon.languages import findT, utf8_repr sys.path.insert(0, '.') @@ -30,11 +30,12 @@ f = open(file1, 'w') try: + f.write('# coding: utf8\n') f.write('{\n') keys = d.keys() keys.sort() for key in keys: - f.write('%s:%s,\n' % (repr(key), repr(str(d[key])))) + f.write('%s:%s,\n' % (utf8_repr(key), utf8_repr(str(d[key])))) f.write('}\n') finally: f.close() From b829109e330f6e83cd89d08445db6e401d446879 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Thu, 2 Feb 2012 16:35:40 -0600 Subject: [PATCH 58/77] socket timeout = 5 seconds until better solution --- VERSION | 2 +- gluon/widget.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 88e35a97..3fd96e9b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-02 16:34:12) stable +Version 1.99.4 (2012-02-02 16:35:38) stable diff --git a/gluon/widget.py b/gluon/widget.py index 4baed549..8858e40c 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -555,10 +555,10 @@ def console(): help='timeout on shutdown of server (5 seconds)') parser.add_option('--socket-timeout', - default=60, + default=5, type='int', dest='socket_timeout', - help='timeout for socket (60 second)') + help='timeout for socket (5 second)') parser.add_option('-f', '--folder', From 00b51227a07bafb4e7cbd28d1cf90312eddd2413 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Fri, 3 Feb 2012 11:29:43 -0600 Subject: [PATCH 59/77] mega patch, text corrections, improved web2py.js, thanks Anthony --- VERSION | 2 +- applications/admin/static/js/web2py.js | 31 ++++++++++++------- .../en/default/documentation/more.markmin | 6 ++-- .../en/default/documentation/official.markmin | 2 ++ .../content/en/default/what/whyweb2py.markmin | 2 +- applications/examples/static/css/examples.css | 2 +- applications/examples/static/js/web2py.js | 31 ++++++++++++------- .../examples/views/default/index.html | 2 +- applications/examples/views/layout.html | 2 +- applications/welcome/static/js/web2py.js | 31 ++++++++++++------- gluon/__init__.py | 11 +++++++ gluon/dal.py | 2 +- 12 files changed, 82 insertions(+), 42 deletions(-) diff --git a/VERSION b/VERSION index 3fd96e9b..4f0d04a4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-02 16:35:38) stable +Version 1.99.4 (2012-02-03 11:29:23) stable diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index ec9599fc..64b0f3a1 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -23,10 +23,6 @@ function ajax(u,s,t) { String.prototype.reverse = function () { return this.split('').reverse().join('');}; function web2py_ajax_fields(target) { - jQuery('input.integer', target).keyup(function(){this.value=this.value.reverse().replace(/[^0-9\-]|\-(?=.)/g,'').reverse();}); - jQuery('input.double,input.decimal', target).keyup(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?"; - jQuery("input[type='checkbox'].delete", target).live('click',function(){ if(this.checked) if(!confirm(confirm_message)) this.checked=false; }); var date_format = (typeof w2p_ajax_date_format != 'undefined') ? w2p_ajax_date_format : "%Y-%m-%d"; var datetime_format = (typeof w2p_ajax_datetime_format != 'undefined') ? w2p_ajax_datetime_format : "%Y-%m-%d %H:%M:%S"; jQuery("input.date",target).each(function() {Calendar.setup({inputField:this, ifFormat:date_format, showsTime:false });}); @@ -38,17 +34,26 @@ function web2py_ajax_fields(target) { function web2py_ajax_init(target) { jQuery('.hidden', target).hide(); jQuery('.error', target).hide().slideDown('slow'); - jQuery('.flash', target).click(function(e) { jQuery(this).fadeOut('slow'); e.preventDefault(); }); - // jQuery('input[type=submit]').click(function(){var t=jQuery(this);t.hide();t.after('')}); web2py_ajax_fields(target); }; +function web2py_event_handlers() { + var doc = jQuery(document) + doc.on('click', '.flash', function(e){jQuery(this).fadeOut('slow'); e.preventDefault();}); + doc.on('keyup', 'input.integer', function(){this.value=this.value.reverse().replace(/[^0-9\-]|\-(?=.)/g,'').reverse();}); + 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;}); +}; + jQuery(function() { var flash = jQuery('.flash'); flash.hide(); if(flash.html()) flash.slideDown(); web2py_ajax_init(document); + web2py_event_handlers(); }); + function web2py_trap_form(action,target) { jQuery('#'+target+' form').each(function(i){ var form=jQuery(this); @@ -60,6 +65,7 @@ function web2py_trap_form(action,target) { }); }); } + function web2py_trap_link(target) { jQuery('#'+target+' a.w2p_trap').each(function(i){ var link=jQuery(this); @@ -70,11 +76,12 @@ function web2py_trap_link(target) { }); }); } -function web2py_ajax_page(method,action,data,target) { - jQuery.ajax({'type':method,'url':action,'data':data, + +function web2py_ajax_page(method, action, data, target) { + jQuery.ajax({'type':method, 'url':action, 'data':data, 'beforeSend':function(xhr) { - xhr.setRequestHeader('web2py-component-location',document.location); - xhr.setRequestHeader('web2py-component-element',target);}, + xhr.setRequestHeader('web2py-component-location', document.location); + xhr.setRequestHeader('web2py-component-element', target);}, 'complete':function(xhr,text){ var html=xhr.responseText; var content=xhr.getResponseHeader('web2py-component-content'); @@ -94,9 +101,11 @@ function web2py_ajax_page(method,action,data,target) { } }); } + function web2py_component(action,target) { - jQuery(function(){ web2py_ajax_page('get',action,null,target); }); + jQuery(function(){web2py_ajax_page('get',action,null,target);}); } + function web2py_comet(url,onmessage,onopen,onclose) { if ("WebSocket" in window) { var ws = new WebSocket(url); diff --git a/applications/examples/private/content/en/default/documentation/more.markmin b/applications/examples/private/content/en/default/documentation/more.markmin index 81a55093..854e076d 100644 --- a/applications/examples/private/content/en/default/documentation/more.markmin +++ b/applications/examples/private/content/en/default/documentation/more.markmin @@ -6,9 +6,9 @@ - [[Twitter http://twitter.com/#!/web2py]] - [[User Voice http://web2py.uservoice.com/ popup]] -#### Learning -- [[Interactive Demo http://www.web2py.com/demo_admin popup]] -- [[Quick Examples http://www.web2py.com/examples/default/examples]] +#### Learning and Demos +- [[Admin Demo http://www.web2py.com/demo_admin popup]] (web-based IDE) +- [[Welcome App Demo http://www.web2py.com/welcome]] (scaffolding application) - [[Videos http://www.web2py.com/examples/default/videos/]] - [[FAQ http://www.web2py.com/AlterEgo popup]] diff --git a/applications/examples/private/content/en/default/documentation/official.markmin b/applications/examples/private/content/en/default/documentation/official.markmin index 45dd1dbe..76303290 100644 --- a/applications/examples/private/content/en/default/documentation/official.markmin +++ b/applications/examples/private/content/en/default/documentation/official.markmin @@ -3,5 +3,7 @@ - [[**web2py Online Book (english)** http://web2py.com/book popup]] - [[web2py Online Book (spanish) http://www.latinuxpress.com/books/drafts/web2py/ popup]] - [[Buy E-book/Printed Version http://stores.lulu.com/web2py popup]] +- [[web2py Application Development Cookbook http://www.packtpub.com/web2py-application-development-recipes-to-master-python-web-framework-cookbook/book?utm_source=web2py.com&utm_medium=link&utm_content=pod&utm_campaign=mdb_009617]] +- [[Quick Examples http://www.web2py.com/examples/default/examples]] - [[API http://web2py.com/book/default/chapter/04#API popup]] - [[Epydoc (source code documentation) http://www.web2py.com/examples/static/epydoc/index.html popup]] diff --git a/applications/examples/private/content/en/default/what/whyweb2py.markmin b/applications/examples/private/content/en/default/what/whyweb2py.markmin index bb323fed..7f68bc0c 100644 --- a/applications/examples/private/content/en/default/what/whyweb2py.markmin +++ b/applications/examples/private/content/en/default/what/whyweb2py.markmin @@ -9,7 +9,7 @@ - **Secure** [[It prevents the most common types of vulnerabilities http://web2py.com/examples/default/security]] including Cross Site Scripting, Injection Flaws, and Malicious File Execution. - **Enforces good Software Engineering practices** (Model-View-Controller design, Server-side form validation, postbacks) that make the code more readable, scalable, and maintainable. - **Speaks multiple protocols** HTML/XML, RSS/ATOM, RTF, PDF, JSON, AJAX, XML-RPC, CSV, REST, WIKI, Flash/AMF, and Linked Data (RDF). -- **Includes** a SSL-enabled and streaming-capable web server, a relational database, a web-based integrated development environment and web-based management interface, a Database Abstraction Layer that writes SQL for you in real time, internationalization support, multiple authentication methods, role based access control, an error logging and ticketing system, multiple caching methods for scalability, the jQuery library for AJAX and effects. +- **Includes** an SSL-enabled and streaming-capable web server, a relational database, a web-based integrated development environment and web-based management interface, a Database Abstraction Layer that writes SQL for you in real time, internationalization support, multiple authentication methods, role based access control, an error logging and ticketing system, multiple caching methods for scalability, the jQuery library for AJAX and effects, and a [[scaffolding application http://www.web2py.com/welcome]] to jumpstart development. The best way to understand web2py is to try it. You can try it online [[here http://www.web2py.com/demo_admin]] (this online version is identical to the actual web2py although some functions are disabled for security reasons). diff --git a/applications/examples/static/css/examples.css b/applications/examples/static/css/examples.css index 32d71724..707211b6 100644 --- a/applications/examples/static/css/examples.css +++ b/applications/examples/static/css/examples.css @@ -37,7 +37,7 @@ sup { line-height: 2em; vertical-align: top; } -ul { list-style: circle outside;} +ul { list-style: circle outside; padding-left: 30px;} .frame { border: 3px solid #959595; diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js index ec9599fc..64b0f3a1 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -23,10 +23,6 @@ function ajax(u,s,t) { String.prototype.reverse = function () { return this.split('').reverse().join('');}; function web2py_ajax_fields(target) { - jQuery('input.integer', target).keyup(function(){this.value=this.value.reverse().replace(/[^0-9\-]|\-(?=.)/g,'').reverse();}); - jQuery('input.double,input.decimal', target).keyup(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?"; - jQuery("input[type='checkbox'].delete", target).live('click',function(){ if(this.checked) if(!confirm(confirm_message)) this.checked=false; }); var date_format = (typeof w2p_ajax_date_format != 'undefined') ? w2p_ajax_date_format : "%Y-%m-%d"; var datetime_format = (typeof w2p_ajax_datetime_format != 'undefined') ? w2p_ajax_datetime_format : "%Y-%m-%d %H:%M:%S"; jQuery("input.date",target).each(function() {Calendar.setup({inputField:this, ifFormat:date_format, showsTime:false });}); @@ -38,17 +34,26 @@ function web2py_ajax_fields(target) { function web2py_ajax_init(target) { jQuery('.hidden', target).hide(); jQuery('.error', target).hide().slideDown('slow'); - jQuery('.flash', target).click(function(e) { jQuery(this).fadeOut('slow'); e.preventDefault(); }); - // jQuery('input[type=submit]').click(function(){var t=jQuery(this);t.hide();t.after('')}); web2py_ajax_fields(target); }; +function web2py_event_handlers() { + var doc = jQuery(document) + doc.on('click', '.flash', function(e){jQuery(this).fadeOut('slow'); e.preventDefault();}); + doc.on('keyup', 'input.integer', function(){this.value=this.value.reverse().replace(/[^0-9\-]|\-(?=.)/g,'').reverse();}); + 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;}); +}; + jQuery(function() { var flash = jQuery('.flash'); flash.hide(); if(flash.html()) flash.slideDown(); web2py_ajax_init(document); + web2py_event_handlers(); }); + function web2py_trap_form(action,target) { jQuery('#'+target+' form').each(function(i){ var form=jQuery(this); @@ -60,6 +65,7 @@ function web2py_trap_form(action,target) { }); }); } + function web2py_trap_link(target) { jQuery('#'+target+' a.w2p_trap').each(function(i){ var link=jQuery(this); @@ -70,11 +76,12 @@ function web2py_trap_link(target) { }); }); } -function web2py_ajax_page(method,action,data,target) { - jQuery.ajax({'type':method,'url':action,'data':data, + +function web2py_ajax_page(method, action, data, target) { + jQuery.ajax({'type':method, 'url':action, 'data':data, 'beforeSend':function(xhr) { - xhr.setRequestHeader('web2py-component-location',document.location); - xhr.setRequestHeader('web2py-component-element',target);}, + xhr.setRequestHeader('web2py-component-location', document.location); + xhr.setRequestHeader('web2py-component-element', target);}, 'complete':function(xhr,text){ var html=xhr.responseText; var content=xhr.getResponseHeader('web2py-component-content'); @@ -94,9 +101,11 @@ function web2py_ajax_page(method,action,data,target) { } }); } + function web2py_component(action,target) { - jQuery(function(){ web2py_ajax_page('get',action,null,target); }); + jQuery(function(){web2py_ajax_page('get',action,null,target);}); } + function web2py_comet(url,onmessage,onopen,onclose) { if ("WebSocket" in window) { var ws = new WebSocket(url); diff --git a/applications/examples/views/default/index.html b/applications/examples/views/default/index.html index e587719f..46783d29 100644 --- a/applications/examples/views/default/index.html +++ b/applications/examples/views/default/index.html @@ -39,7 +39,7 @@

        WEB-BASED IDE

        diff --git a/applications/examples/views/layout.html b/applications/examples/views/layout.html index bd813d77..0f5cc8d8 100644 --- a/applications/examples/views/layout.html +++ b/applications/examples/views/layout.html @@ -87,7 +87,7 @@
        {{=response.subtitle or ''}}
        diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index ec9599fc..64b0f3a1 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -23,10 +23,6 @@ function ajax(u,s,t) { String.prototype.reverse = function () { return this.split('').reverse().join('');}; function web2py_ajax_fields(target) { - jQuery('input.integer', target).keyup(function(){this.value=this.value.reverse().replace(/[^0-9\-]|\-(?=.)/g,'').reverse();}); - jQuery('input.double,input.decimal', target).keyup(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?"; - jQuery("input[type='checkbox'].delete", target).live('click',function(){ if(this.checked) if(!confirm(confirm_message)) this.checked=false; }); var date_format = (typeof w2p_ajax_date_format != 'undefined') ? w2p_ajax_date_format : "%Y-%m-%d"; var datetime_format = (typeof w2p_ajax_datetime_format != 'undefined') ? w2p_ajax_datetime_format : "%Y-%m-%d %H:%M:%S"; jQuery("input.date",target).each(function() {Calendar.setup({inputField:this, ifFormat:date_format, showsTime:false });}); @@ -38,17 +34,26 @@ function web2py_ajax_fields(target) { function web2py_ajax_init(target) { jQuery('.hidden', target).hide(); jQuery('.error', target).hide().slideDown('slow'); - jQuery('.flash', target).click(function(e) { jQuery(this).fadeOut('slow'); e.preventDefault(); }); - // jQuery('input[type=submit]').click(function(){var t=jQuery(this);t.hide();t.after('')}); web2py_ajax_fields(target); }; +function web2py_event_handlers() { + var doc = jQuery(document) + doc.on('click', '.flash', function(e){jQuery(this).fadeOut('slow'); e.preventDefault();}); + doc.on('keyup', 'input.integer', function(){this.value=this.value.reverse().replace(/[^0-9\-]|\-(?=.)/g,'').reverse();}); + 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;}); +}; + jQuery(function() { var flash = jQuery('.flash'); flash.hide(); if(flash.html()) flash.slideDown(); web2py_ajax_init(document); + web2py_event_handlers(); }); + function web2py_trap_form(action,target) { jQuery('#'+target+' form').each(function(i){ var form=jQuery(this); @@ -60,6 +65,7 @@ function web2py_trap_form(action,target) { }); }); } + function web2py_trap_link(target) { jQuery('#'+target+' a.w2p_trap').each(function(i){ var link=jQuery(this); @@ -70,11 +76,12 @@ function web2py_trap_link(target) { }); }); } -function web2py_ajax_page(method,action,data,target) { - jQuery.ajax({'type':method,'url':action,'data':data, + +function web2py_ajax_page(method, action, data, target) { + jQuery.ajax({'type':method, 'url':action, 'data':data, 'beforeSend':function(xhr) { - xhr.setRequestHeader('web2py-component-location',document.location); - xhr.setRequestHeader('web2py-component-element',target);}, + xhr.setRequestHeader('web2py-component-location', document.location); + xhr.setRequestHeader('web2py-component-element', target);}, 'complete':function(xhr,text){ var html=xhr.responseText; var content=xhr.getResponseHeader('web2py-component-content'); @@ -94,9 +101,11 @@ function web2py_ajax_page(method,action,data,target) { } }); } + function web2py_component(action,target) { - jQuery(function(){ web2py_ajax_page('get',action,null,target); }); + jQuery(function(){web2py_ajax_page('get',action,null,target);}); } + function web2py_comet(url,onmessage,onopen,onclose) { if ("WebSocket" in window) { var ws = new WebSocket(url); diff --git a/gluon/__init__.py b/gluon/__init__.py index d2906318..32ab0473 100644 --- a/gluon/__init__.py +++ b/gluon/__init__.py @@ -20,6 +20,17 @@ from sqlhtml import SQLFORM, SQLTABLE from compileapp import LOAD +# Dummy code to enable code completion in IDE's. +if 0: + from globals import Request, Response, Session + from cache import Cache + from languages import translator + request = Request() + response = Response() + session = Session() + cache = Cache(request) + T = translator(request) + diff --git a/gluon/dal.py b/gluon/dal.py index 09b9b4a5..cfe46e01 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -7769,7 +7769,7 @@ def Rows_unpickler(data): def Rows_pickler(data): return Rows_unpickler, \ - (cPickle.dumps(data.as_list(storage_to_dict=True, + (cPickle.dumps(data.as_list(storage_to_dict=False, datetime_to_str=False)),) copy_reg.pickle(Rows, Rows_pickler, Rows_unpickler) From 8fbae09fdf20b8cf758f920c247f7ac09cb05ddf Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Fri, 3 Feb 2012 11:31:44 -0600 Subject: [PATCH 60/77] fixed not x -> x is None, thanks Pai --- VERSION | 2 +- gluon/dal.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 4f0d04a4..46cdf1f3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-03 11:29:23) stable +Version 1.99.4 (2012-02-03 11:31:35) stable diff --git a/gluon/dal.py b/gluon/dal.py index cfe46e01..6af3be1e 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -6552,8 +6552,8 @@ def __setitem__(self, key, value): elif str(key).isdigit(): if key == 0: self.insert(**self._filter_fields(value)) - elif not self._db(self._id == key)\ - .update(**self._filter_fields(value)): + elif self._db(self._id == key)\ + .update(**self._filter_fields(value)) is None: raise SyntaxError, 'No such record: %s' % key else: if isinstance(key, dict): From f2c8c1fd2178f0eb1523edf2621657259136b8d4 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sat, 4 Feb 2012 09:50:11 -0600 Subject: [PATCH 61/77] solved some bugs in build_query, thanks Michele --- VERSION | 2 +- gluon/sqlhtml.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 46cdf1f3..ea953740 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-03 11:31:35) stable +Version 1.99.4 (2012-02-04 09:49:34) stable diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 7c83bc5e..2043d64e 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1297,6 +1297,8 @@ def factory(*fields, **attributes): @staticmethod def build_query(fields,keywords): + from gluon import current + request = current.request if isinstance(keywords,(tuple,list)): keywords = keywords[0] request.vars.keywords = keywords @@ -1781,7 +1783,13 @@ def self_link(name,p): classtr = 'odd' numrec+=1 id = row[field_id] - tr = TR(_class=classtr) + if row_id: + rid = row_id + if callable(rid): + rid = rid(row) + tr = TR(_id=rid, _class='%s %s' % (classtr, 'with_id')) + else: + tr = TR(_class=classtr) if selectable: tr.append(INPUT(_type="checkbox",_name="records",_value=id, value=request.vars.records)) From 3bccc8a8d60518d3db6c0c536c927019703c62a8 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sat, 4 Feb 2012 09:51:14 -0600 Subject: [PATCH 62/77] test_router and test_routes patch, thanks Jonathan --- VERSION | 2 +- gluon/tests/test_router.py | 6 ++++++ gluon/tests/test_routes.py | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index ea953740..cb32ad35 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-04 09:49:34) stable +Version 1.99.4 (2012-02-04 09:51:12) stable diff --git a/gluon/tests/test_router.py b/gluon/tests/test_router.py index d01b518c..e258e88f 100644 --- a/gluon/tests/test_router.py +++ b/gluon/tests/test_router.py @@ -770,6 +770,12 @@ def test_router_args(self): "/init/default/f ['', 'arg1']") self.assertEqual(filter_url('http://domain.com/init/default/f/arg1/arg2'), "/init/default/f ['arg1', 'arg2']") + self.assertEqual(filter_url('http://domain.com/init/default/f/arg1//arg2'), + "/init/default/f ['arg1', '', 'arg2']") + self.assertEqual(filter_url('http://domain.com/init/default/f/arg1//arg3/'), + "/init/default/f ['arg1', '', 'arg3']") + self.assertEqual(filter_url('http://domain.com/init/default/f/arg1//arg3//'), + "/init/default/f ['arg1', '', 'arg3', '']") self.assertEqual(filter_url('http://domain.com/init/default/f',%20out=True), "/f") self.assertEqual(map_url_out(None, None, 'init', 'default', 'f', None, None, None, None, None), "/f") diff --git a/gluon/tests/test_routes.py b/gluon/tests/test_routes.py index 9bc3cc46..7a0672db 100644 --- a/gluon/tests/test_routes.py +++ b/gluon/tests/test_routes.py @@ -237,6 +237,12 @@ def test_routes_args(self): "/welcome/default/f ['', 'arg1']") self.assertEqual(filter_url('http://domain.com/welcome/default/f/arg1/arg2'), "/welcome/default/f ['arg1', 'arg2']") + self.assertEqual(filter_url('http://domain.com/welcome/default/f/arg1//arg2'), + "/welcome/default/f ['arg1', '', 'arg2']") + self.assertEqual(filter_url('http://domain.com/welcome/default/f/arg1//arg3/'), + "/welcome/default/f ['arg1', '', 'arg3']") + self.assertEqual(filter_url('http://domain.com/welcome/default/f/arg1//arg3//'), + "/welcome/default/f ['arg1', '', 'arg3', '']") self.assertEqual(filter_url('http://domain.com/welcome/default/f',%20out=True), "/f") self.assertEqual(regex_filter_out('/welcome/default/f'), "/f") From 932e9abc74390ce8701e1801b5877624db6766c8 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sat, 4 Feb 2012 10:07:42 -0600 Subject: [PATCH 63/77] patch related to registation_id, thanks Nik --- VERSION | 2 +- gluon/tools.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index cb32ad35..087aea73 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-04 09:51:12) stable +Version 1.99.4 (2012-02-04 10:07:28) stable diff --git a/gluon/tools.py b/gluon/tools.py index 78fa1a5b..2216e0ff 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1470,7 +1470,7 @@ def get_or_create_user(self, keys): checks.append(fieldname) user = user or table_user(**{fieldname:keys[fieldname]}) # if we think we found the user but registration_id does not match, make new user - if user and user.registration_id and user.registration_id!=keys.get('registration_id',None): + if 'registration_id' in checks and user and user.registration_id and user.registration_id!=keys.get('registration_id',None): user = None # THINK MORE ABOUT THIS? DO WE TRUST OPENID PROVIDER? keys['registration_key']='' if user: From a646a755cf82722c6c2c21cc2ae8e24e53d78892 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sat, 4 Feb 2012 10:13:36 -0600 Subject: [PATCH 64/77] gluon/contrib/login_methods/browserid_account.py, thanks Pai --- VERSION | 2 +- .../login_methods/browserid_account.py | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 gluon/contrib/login_methods/browserid_account.py diff --git a/VERSION b/VERSION index 087aea73..fcddf5cf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-04 10:07:28) stable +Version 1.99.4 (2012-02-04 10:13:29) stable diff --git a/gluon/contrib/login_methods/browserid_account.py b/gluon/contrib/login_methods/browserid_account.py new file mode 100644 index 00000000..519119ba --- /dev/null +++ b/gluon/contrib/login_methods/browserid_account.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + BrowserID Authentication for web2py + developed by Madhukar R Pai (Copyright � 2011) + Email + License : LGPL + + thanks and credits to the web2py community + + This custom authenticator allows web2py to authenticate using browserid (https://browserid.org/) + BrowserID is a project by Mozilla Labs (http://mozillalabs.com/) + to Know how browserid works please visit http://identity.mozilla.com/post/7616727542/introducing-browserid-a-better-way-to-sign-in + + bottom line BrowserID provides a free, secure, de-centralized, easy to use(for users and developers) login solution. + You can use any email id as your login id. Browserid just verifys the email id and lets you login with that id. + + credits for the doPost jquery function - itsadok (http://stackoverflow.com/users/7581/itsadok) + +""" +import time +from gluon import * +from gluon.storage import Storage +from gluon.tools import fetch +import gluon.contrib.simplejson as json + +class BrowserID(object): + """ + from gluon.contrib.login_methods.browserid_account import BrowserID + auth.settings.login_form = BrowserID(request, + audience = "http://127.0.0.1:8000/" + assertion_post_url = "http://127.0.0.1:8000/%s/default/user/login" % request.application) + """ + + def __init__(self, + request, + audience = "", + assertion_post_url = "", + prompt = "BrowserID Login", + issuer = "browserid.org", + verify_url = "https://browserid.org/verify", + browserid_js = "https://browserid.org/include.js", + browserid_button = "http://browserid.org.cn/css/sign_in_red.png", + crypto_js = "https://crypto-js.googlecode.com/files/2.2.0-crypto-md5.js", + on_login_failure = None, + ): + + self.request = request + self.audience = audience + self.assertion_post_url = assertion_post_url + self.prompt = prompt + self.issuer = issuer + self.verify_url = verify_url + self.browserid_js = browserid_js + self.browserid_button = browserid_button + self.crypto_js = crypto_js + self.on_login_failure = on_login_failure + self.asertion_js = """ + (function($){$.extend({doPost:function(url,params){var $form=$("
        ").attr("action",url); + $.each(params,function(name,value){$("").attr("name",name).attr("value",value).appendTo($form)}); + $form.appendTo("body");$form.submit()}})})(jQuery); + function gotVerifiedEmail(assertion){if(assertion !== null){$.doPost('%s',{'assertion':assertion});}}""" % self.assertion_post_url + + def get_user(self): + request = self.request + if request.vars.assertion: + audience = self.audience + issuer = self.issuer + assertion = XML(request.vars.assertion,sanitize=True) + verify_data = {'assertion':assertion,'audience':audience} + auth_info_json = fetch(self.verify_url,data=verify_data) + j = json.loads(auth_info_json) + epoch_time = int(time.time()*1000) # we need 13 digit epoch time + if j["status"] == "okay" and j["audience"] == audience and j['issuer'] == issuer and j['expires'] >= epoch_time: + return dict(email = j['email']) + elif self.on_login_failure: + redirect('http://google.com/') + else: + redirect('http://google.com/') + return None + + def login_form(self): + request = self.request + onclick = "javascript:navigator.id.getVerifiedEmail(gotVerifiedEmail) ; return false" + form = DIV(SCRIPT(_src=self.browserid_js,_type="text/javascript"), + SCRIPT(_src=self.crypto_js,_type="text/javascript"), + A(IMG(_src=self.browserid_button,_alt=self.prompt),_href="#",_onclick=onclick,_class="browserid",_title="Login With BrowserID"), + SCRIPT(self.asertion_js)) + return form From 73c1dc03ebe0c120673381e6262f14424fa148f9 Mon Sep 17 00:00:00 2001 From: Massimo DiPierro Date: Tue, 7 Feb 2012 11:08:35 -0600 Subject: [PATCH 65/77] removed fieldset from skeleton --- VERSION | 2 +- applications/welcome/static/css/skeleton.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index fcddf5cf..2dedd8b1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-04 10:13:29) stable +Version 1.99.4 (2012-02-07 11:08:32) stable diff --git a/applications/welcome/static/css/skeleton.css b/applications/welcome/static/css/skeleton.css index 4f13a5be..13241d33 100755 --- a/applications/welcome/static/css/skeleton.css +++ b/applications/welcome/static/css/skeleton.css @@ -25,7 +25,7 @@ /* #Reset & Basics (Inspired by E. Meyers) ================================================== */ - html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { + html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; From 82a8b7ec2d26634b9e9ff86a38b9e2920e162a94 Mon Sep 17 00:00:00 2001 From: Massimo DiPierro Date: Tue, 7 Feb 2012 11:28:02 -0600 Subject: [PATCH 66/77] fixed issue 649 --- VERSION | 2 +- gluon/dal.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 2dedd8b1..48b5b6b9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-07 11:08:32) stable +Version 1.99.4 (2012-02-07 11:28:00) stable diff --git a/gluon/dal.py b/gluon/dal.py index 6af3be1e..4b6713b0 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -6130,6 +6130,7 @@ def define_table( t._format = format t._singular = singular t._plural = plural + t._actual = True return t def __iter__(self): @@ -6346,6 +6347,7 @@ def __init__( :raises SyntaxError: when a supplied field is of incorrect type. """ + self._actual = False # set to True by define_table() self._tablename = tablename self._sequence_name = args.get('sequence_name',None) or \ db and db._adapter.sequence_name(tablename) @@ -6380,7 +6382,10 @@ def __init__( table = field for field in table: if not field.name in fieldnames and not field.type=='id': - newfields.append(copy.copy(field)) + field = copy.copy(field) + if field.type == 'reference '+table._tablename: # correct self references + field.type = 'reference '+self._tablename + newfields.append(field) fieldnames.add(field.name) else: # let's ignore new fields with duplicated names!!! From a5bb47dce4c58159701a4f71f44e42ab3e67789c Mon Sep 17 00:00:00 2001 From: Massimo DiPierro Date: Tue, 7 Feb 2012 11:29:56 -0600 Subject: [PATCH 67/77] fixed issue 649 --- VERSION | 2 +- gluon/dal.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 48b5b6b9..73d724cf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-07 11:28:00) stable +Version 1.99.4 (2012-02-07 11:29:54) stable diff --git a/gluon/dal.py b/gluon/dal.py index 4b6713b0..4cedaa6e 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -6383,7 +6383,8 @@ def __init__( for field in table: if not field.name in fieldnames and not field.type=='id': field = copy.copy(field) - if field.type == 'reference '+table._tablename: # correct self references + # correct self references + if not table._actual and field.type == 'reference '+table._tablename: field.type = 'reference '+self._tablename newfields.append(field) fieldnames.add(field.name) From 3b6ebb054ebbf180784cc4a02d475f6396a047bc Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Thu, 9 Feb 2012 10:59:22 -0600 Subject: [PATCH 68/77] added import STYLE in sqlhtml, thanks Robert --- VERSION | 2 +- gluon/sqlhtml.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 73d724cf..c70fa80e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-07 11:29:54) stable +Version 1.99.4 (2012-02-09 10:59:05) stable diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 2043d64e..532784ca 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -17,7 +17,7 @@ from http import HTTP from html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG, SCRIPT from html import FORM, INPUT, LABEL, OPTION, SELECT, MENU -from html import TABLE, THEAD, TBODY, TR, TD, TH +from html import TABLE, THEAD, TBODY, TR, TD, TH, STYLE from html import URL, truncate_string from dal import DAL, Table, Row, CALLABLETYPES, smart_query from storage import Storage From c6380e56015bed424831e1350230f6204c35c96f Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Thu, 9 Feb 2012 11:11:24 -0600 Subject: [PATCH 69/77] fixed problem with logout redirect, thanks mbelletti --- VERSION | 2 +- gluon/tools.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index c70fa80e..fb270c45 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-09 10:59:05) stable +Version 1.99.4 (2012-02-09 11:11:08) stable diff --git a/gluon/tools.py b/gluon/tools.py index 2216e0ff..23ebfdf5 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1186,8 +1186,12 @@ def navbar(self, prefix='Welcome', action=None, separators=(' [ ',' | ',' ] ')): next = '' else: next = '?_next='+urllib.quote(URL(args=request.args,vars=request.vars)) + + li_next = '?_next='+urllib.quote(self.settings.login_next) + lo_next = '?_next='+urllib.quote(self.settings.logout_next) + if self.user_id: - logout=A(T('Logout'),_href=action+'/logout'+next) + logout=A(T('Logout'),_href=action+'/logout'+lo_next) profile=A(T('Profile'),_href=action+'/profile'+next) password=A(T('Password'),_href=action+'/change_password'+next) bar = SPAN(prefix,self.user.first_name,s1, logout,s3,_class='auth_navbar') @@ -1198,7 +1202,7 @@ def navbar(self, prefix='Welcome', action=None, separators=(' [ ',' | ',' ] ')): bar.insert(-1, s2) bar.insert(-1, password) else: - login=A(T('Login'),_href=action+'/login'+next) + login=A(T('Login'),_href=action+'/login'+li_next) register=A(T('Register'),_href=action+'/register'+next) retrieve_username=A(T('forgot username?'), _href=action+'/retrieve_username'+next) From 4228350d195a77f8fd6aaa914670d3ffc647b34c Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Thu, 9 Feb 2012 11:15:42 -0600 Subject: [PATCH 70/77] fixed issue 1786 with row_is in sqlhtml, thanks simonm3 --- VERSION | 2 +- gluon/sqlhtml.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index fb270c45..7afcbe9a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-09 11:11:08) stable +Version 1.99.4 (2012-02-09 11:15:33) stable diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 532784ca..31426028 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1783,9 +1783,9 @@ def self_link(name,p): classtr = 'odd' numrec+=1 id = row[field_id] - if row_id: - rid = row_id - if callable(rid): + if id: + rid = id + if callable(rid): ### can this ever be callable? rid = rid(row) tr = TR(_id=rid, _class='%s %s' % (classtr, 'with_id')) else: From d1e654ee23d720e51ebc296e275d30fb02fc01a9 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Thu, 9 Feb 2012 11:21:38 -0600 Subject: [PATCH 71/77] new syntax db(db.dog_id.belongs(db.dogs.owner=='james')).select(), thanks guruyaya --- VERSION | 2 +- gluon/dal.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 7afcbe9a..648bfd75 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-09 11:15:33) stable +Version 1.99.4 (2012-02-09 11:21:10) stable diff --git a/gluon/dal.py b/gluon/dal.py index 4cedaa6e..6ab6672f 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -6914,6 +6914,8 @@ def like(self, value, case_sensitive=False): return Query(self.db, op, self, value) def belongs(self, value): + if isinstance(value,Query): + value = self.db(value)._select(value.first._table._id) return Query(self.db, self.db._adapter.BELONGS, self, value) def startswith(self, value): From cda213f38c452d3862eb47889e6ec67dd0fc09b8 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sat, 11 Feb 2012 23:06:24 -0600 Subject: [PATCH 72/77] fixed issue 647, problem with is_in_db and multiple, thanks Manuele Presenti --- VERSION | 2 +- gluon/validators.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 648bfd75..6f1471a7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-09 11:21:10) stable +Version 1.99.4 (2012-02-11 23:04:52) stable diff --git a/gluon/validators.py b/gluon/validators.py index 276013da..79ba4d61 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -466,6 +466,7 @@ def options(self, zero=True): items.sort(options_sorter) if zero and not self.zero is None and not self.multiple: items.insert(0,('',self.zero)) + self.options() # return items def __call__(self, value): @@ -479,7 +480,7 @@ def __call__(self, value): if isinstance(self.multiple,(tuple,list)) and \ not self.multiple[0]<=len(values) Date: Sun, 12 Feb 2012 09:49:39 -0600 Subject: [PATCH 73/77] sync languages patch, helps merging language files, thanks Yair --- VERSION | 2 +- scripts/sync_languages.py | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 6f1471a7..e0c2fa02 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-11 23:04:52) stable +Version 1.99.4 (2012-02-12 09:49:15) stable diff --git a/scripts/sync_languages.py b/scripts/sync_languages.py index ddea4ff8..8f1f8d6d 100755 --- a/scripts/sync_languages.py +++ b/scripts/sync_languages.py @@ -14,6 +14,28 @@ file = sys.argv[1] apps = sys.argv[2:] +def sync_language(d, data): + ''' this function makes sure a translated string will be prefered over an untranslated + string when syncing languages between apps. when both are translated, it prefers the + latter app, as did the original script + ''' + + for key in data: + # if this string is not in the allready translated data, add it + if key not in d: + d[key] = data[key] + # see if there is a translated string in the original list, but not in the new list + elif ( + ((d[key] != '') or (d[key] != key)) and + ((data[key] == '') or (data[key] == key)) + ): + d[key] = d[key] + # any other case (wether there is or there isn't a translated string) + else: + d[key] = data[key] + + return d + d = {} for app in apps: path = 'applications/%s/' % app @@ -23,7 +45,8 @@ data = eval(langfile.read()) finally: langfile.close() - d.update(data) + + d = sync_language(d, data) path = 'applications/%s/' % apps[-1] file1 = os.path.join(path, 'languages', '%s.py' % file) From 196fdf6b0450beee6c730dad630404717903dfb0 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sun, 12 Feb 2012 20:34:41 -0600 Subject: [PATCH 74/77] admin/languages/ja.py, thanks Omi --- VERSION | 2 +- applications/admin/languages/ja.py | 186 +++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 applications/admin/languages/ja.py diff --git a/VERSION b/VERSION index e0c2fa02..75a248d8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-12 09:49:15) stable +Version 1.99.4 (2012-02-12 20:34:23) stable diff --git a/applications/admin/languages/ja.py b/applications/admin/languages/ja.py new file mode 100644 index 00000000..910e8a67 --- /dev/null +++ b/applications/admin/languages/ja.py @@ -0,0 +1,186 @@ +# coding: utf8 +{ +'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'%Y-%m-%d': '%Y-%m-%d', +'(requires internet access)': '(インターネットアクセスが必要)', +'(something like "it-it")': '(例: "it-it")', +'Abort': '中断', +'About application': 'アプリケーションについて', +'About': 'About', +'Additional code for your application': 'アプリケーションに必要な追加記述', +'Admin language': '管理画面の言語', +'administrative interface': '管理画面', +'Administrator Password:': '管理者パスワード:', +'and rename it:': 'ファイル名を変更:', +'appadmin': 'アプリ管理画面', +'application "%s" uninstalled': '"%s"アプリケーションが削除されました', +'application compiled': 'アプリケーションがコンパイルされました', +'Application name:': 'アプリケーション名:', +'Are you sure you want to delete plugin "%s"?': '"%s"プラグインを削除してもよろしいですか?', +'Are you sure you want to delete this object?': 'このオブジェクトを削除してもよろしいですか?', +'Are you sure you want to uninstall application "%s"?': '"%s"アプリケーションを削除してもよろしいですか?', +'arguments': '引数', +'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': '注意: 安全(HTTPS)な接続でログインするかlocalhostで実行されている必要があります。', +'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': '注意: テストはスレッドセーフではないので複数のテストを同時に実行しないでください。', +'ATTENTION: you cannot edit the running application!': '注意: 実行中のアプリケーションは編集できません!', +'Available databases and tables': '利用可能なデータベースとテーブル一覧', +'back': '戻る', +'Basics': '基本情報', +'Begin': '開始', +'cache': 'cache', +'cannot upload file "%(filename)s"': '"%(filename)s"ファイルをアップロードできません', +'Change admin password': '管理者パスワード変更', +'check all': '全てを選択', +'Check for upgrades': '更新チェック', +'Checking for upgrades...': '更新を確認中...', +'Clean': '一時データ削除', +'Click row to expand traceback': '列をクリックしてトレースバックを展開', +'code': 'コード', +'collapse/expand all': '全て開閉する', +'Compile': 'コンパイル', +'compiled application removed': 'コンパイル済みのアプリケーションが削除されました', +'Controllers': 'コントローラ', +'controllers': 'コントローラ', +'Count': '回数', +'create file with filename:': 'ファイル名:', +'Create': '作成', +'created by': '作成者', +'crontab': 'crontab', +'currently running': '現在実行中', +'currently saved or': '現在保存されているデータ または', +'database administration': 'データベース管理', +'db': 'db', +'defines tables': 'テーブル定義', +'delete all checked': '選択したデータを全て削除', +'delete plugin': 'プラグイン削除', +'Delete this file (you will be asked to confirm deletion)': 'ファイルの削除(確認画面が出ます)', +'Delete': '削除', +'Deploy on Google App Engine': 'Google App Engineにデプロイ', +'Deploy': 'デプロイ', +'design': 'デザイン', +'Detailed traceback description': '詳細なトレースバック内容', +'details': '詳細', +'direction: ltr': 'direction: ltr', +'Disable': '無効', +'docs': 'ドキュメント', +'download layouts': 'レイアウトのダウンロード', +'download plugins': 'プラグインのダウンロード', +'edit all': '全て編集', +'Edit application': 'アプリケーションを編集', +'edit views:': 'ビューの編集:', +'Edit': '編集', +'Editing file "%s"': '"%s"ファイルを編集中', +'Enable': '有効', +'Error logs for "%(app)s"': '"%(app)s"のエラーログ', +'Error snapshot': 'エラー発生箇所', +'Error ticket': 'エラーチケット', +'Error': 'エラー', +'Errors': 'エラー', +'Exception instance attributes': '例外インスタンス引数', +'exposes': '公開', +'exposes:': '公開:', +'extends': '継承', +'File': 'ファイル', +'files': 'ファイル', +'filter': 'フィルタ', +'Frames': 'フレーム', +'Functions with no doctests will result in [passed] tests.': 'doctestsのない関数は自動的にテストをパスします。', +'Generate': 'アプリ生成', +'Get from URL:': 'URLから取得:', +'go!': '実行!', +'Help': 'ヘルプ', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'もし上記のレポートにチケット番号が含まれる場合は、doctestを実行する前に、コントローラの実行で問題があったことを示します。これはインデントの問題やその関数の外部で問題があった場合に起きるが一般的です。\n緑色のタイトルは全てのテスト(もし定義されていれば)をパスしたことを示します。その場合、テスト結果は表示されません。', +'includes': 'インクルード', +'index': 'index', +'inspect attributes': '引数の検査', +'Install': 'インストール', +'Installed applications': 'アプリケーション一覧', +'Languages': '言語', +'languages': '言語', +'Last saved on:': '最終保存日時:', +'License for': 'License for', +'loading...': 'ロードしています...', +'locals': 'ローカル', +'Login to the Administrative Interface': '管理画面へログイン', +'Login': 'ログイン', +'Logout': 'ログアウト', +'Models': 'モデル', +'models': 'モデル', +'Modules': 'モジュール', +'modules': 'モジュール', +'New Application Wizard': '新規アプリケーション作成ウィザード', +'New application wizard': '新規アプリケーション作成ウィザード', +'new plugin installed': '新しいプラグインがインストールされました', +'New simple application': '新規アプリケーション', +'No databases in this application': 'このアプリケーションにはデータベースが存在しません', +'NO': 'いいえ', +'online designer': 'オンラインデザイナー', +'Overwrite installed app': 'アプリケーションを上書き', +'Pack all': 'パッケージ化', +'Pack compiled': 'コンパイルデータのパッケージ化', +'pack plugin': 'プラグインのパッケージ化', +'Peeking at file': 'ファイルを参照', +'plugin "%(plugin)s" deleted': '"%(plugin)s"プラグインは削除されました', +'Plugin "%s" in application': '"%s"プラグイン', +'Plugins': 'プラグイン', +'plugins': 'プラグイン', +'Powered by': 'Powered by', +'Reload routes': 'ルーティング再読み込み', +'Remove compiled': 'コンパイルデータの削除', +'request': 'リクエスト', +'response': 'レスポンス', +'restart': '最初からやり直し', +'restore': '復元', +'revert': '一つ前に戻す', +"Run tests in this file (to run all files, you may also use the button labelled 'test')": "このファイルのテストを実行(全てのファイルに対して実行する場合は、'テスト'というボタンを使用できます)", +'Save': '保存', +'Saved file hash:': '保存されたファイルハッシュ:', +'Searching:': '検索中:', +'session expired': 'セッションの有効期限が切れました', +'session': 'セッション', +'shell': 'shell', +'Site': 'サイト', +'skip to generate': 'スキップしてアプリ生成画面へ移動', +'Sorry, could not find mercurial installed': 'インストールされているmercurialが見つかりません', +'Start a new app': '新規アプリの作成', +'Start wizard': 'ウィザードの開始', +'state': 'state', +'Static files': '静的ファイル', +'static': '静的ファイル', +'Step': 'ステップ', +'test': 'テスト', +'Testing application': 'アプリケーションをテスト中', +'The application logic, each URL path is mapped in one exposed function in the controller': 'アプリケーションロジック、それぞれのURLパスはコントローラで公開されている各関数にマッピングされています', +'The data representation, define database tables and sets': 'データの表示方法, テーブルとセットの定義', +'The presentations layer, views are also known as templates': 'プレゼンテーション層、ビューはテンプレートとしても知られています', +'There are no controllers': 'コントローラがありません', +'There are no modules': 'モジュールがありません', +'There are no plugins': 'プラグインはありません', +'There are no translators, only default language is supported': '翻訳がないためデフォルト言語のみをサポートします', +'There are no views': 'ビューがありません', +'These files are served without processing, your images go here': 'これらのファイルは直接参照されます, ここに画像が入ります', +'Ticket ID': 'チケットID', +'to previous version.': '前のバージョンへ戻す。', +'To create a plugin, name a file/folder plugin_[name]': 'ファイル名/フォルダ名 plugin_[名称]としてプラグインを作成してください', +'Traceback': 'トレースバック', +'Translation strings for the application': 'アプリケーションの翻訳文字列', +'Unable to download because:': '以下の理由でダウンロードできません:', +'uncheck all': '全ての選択を解除', +'Uninstall': 'アプリ削除', +'update all languages': '全ての言語を更新', +'Upload a package:': 'パッケージをアップロード:', +'Upload and install packed application': 'パッケージのアップロードとインストール', +'upload file:': 'ファイルをアップロード:', +'upload plugin file:': 'プラグインファイルをアップロード:', +'upload': 'アップロード', +'user': 'ユーザー', +'variables': '変数', +'Version': 'バージョン', +'Versioning': 'バージョン管理', +'Views': 'ビュー', +'views': 'ビュー', +'Web Framework': 'Web Framework', +'web2py is up to date': 'web2pyは最新です', +'web2py Recent Tweets': '最近のweb2pyTweets', +'YES': 'はい', +} From 1c808ecda2c376c8744a2d0143afadfff87fc855 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Mon, 13 Feb 2012 10:12:28 -0600 Subject: [PATCH 75/77] accidental infinite recursion in gluon/validators.py --- VERSION | 2 +- gluon/validators.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 75a248d8..c563ea36 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-12 20:34:23) stable +Version 1.99.4 (2012-02-13 10:12:14) stable diff --git a/gluon/validators.py b/gluon/validators.py index 79ba4d61..c7281f24 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -466,7 +466,6 @@ def options(self, zero=True): items.sort(options_sorter) if zero and not self.zero is None and not self.multiple: items.insert(0,('',self.zero)) - self.options() # return items def __call__(self, value): From 01b09a450e7e6151c8afd95a4cbcd350765f29e3 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Tue, 14 Feb 2012 08:40:06 -0600 Subject: [PATCH 76/77] =?UTF-8?q?fixed=20many=20small=20errors,=20thanks?= =?UTF-8?q?=20Marin=20Pranji=C4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 2 +- gluon/contrib/DowCommerce.py | 3 +-- gluon/contrib/feedparser.py | 2 +- gluon/contrib/generics.py | 24 ++++++++++++------------ gluon/contrib/memdb.py | 5 +++-- gluon/dal.py | 36 ++++++++++++++++++------------------ gluon/scheduler.py | 4 +++- gluon/sqlhtml.py | 2 +- 8 files changed, 40 insertions(+), 38 deletions(-) diff --git a/VERSION b/VERSION index c563ea36..5c1822c2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-13 10:12:14) stable +Version 1.99.4 (2012-02-14 08:39:29) stable diff --git a/gluon/contrib/DowCommerce.py b/gluon/contrib/DowCommerce.py index ebc40f1f..8c6ea3c0 100644 --- a/gluon/contrib/DowCommerce.py +++ b/gluon/contrib/DowCommerce.py @@ -48,14 +48,13 @@ def __init__(self, username=None, password=None, demomode=False): def process(self): encoded_args = urllib.urlencode(self.parameters) - if self.proxy == None: results = str(urllib.urlopen(self.url, encoded_args).read()).split(self.delimiter) else: opener = urllib.FancyURLopener(self.proxy) opened = opener.open(self.url, encoded_args) try: - results += str(opened.read()).split(self.delimiter) + results = str(opened.read()).split(self.delimiter) finally: opened.close() diff --git a/gluon/contrib/feedparser.py b/gluon/contrib/feedparser.py index a002dce0..d0fc640b 100755 --- a/gluon/contrib/feedparser.py +++ b/gluon/contrib/feedparser.py @@ -2718,7 +2718,7 @@ def unknown_starttag(self, tag, attrs): # declare xlink namespace, if needed if self.mathmlOK or self.svgOK: - if filter(lambda (n,v): n.startswith('xlink:'),attrs): + if filter((lambda n,v: n.startswith('xlink:')),attrs): if not ('xmlns:xlink','http://www.w3.org/1999/xlink') in attrs: attrs.append(('xmlns:xlink','http://www.w3.org/1999/xlink')) diff --git a/gluon/contrib/generics.py b/gluon/contrib/generics.py index 0798a6e3..2f5e95cc 100644 --- a/gluon/contrib/generics.py +++ b/gluon/contrib/generics.py @@ -4,8 +4,8 @@ import os import cPickle import gluon.serializers -from gluon import current -from gluon.html import markmin_serializer, TAG, HTML, BODY, UL, XML +from gluon import current, HTTP +from gluon.html import markmin_serializer, TAG, HTML, BODY, UL, XML, H1 from gluon.contenttype import contenttype from gluon.contrib.pyfpdf import FPDF, HTMLMixin from gluon.sanitizer import sanitize @@ -16,13 +16,13 @@ def wrapper(f): def g(data): try: output = f(data) - except (TypeError, ValueError): - raise HTTP(405, '%s serialization error' % extension.upper()) - except ImportError: - raise HTTP(405, '%s not available' % extension.upper()) - except: - raise HTTP(405, '%s error' % extension.upper()) - return XML(ouput) + return XML(ouput) + except (TypeError, ValueError), e: + raise HTTP(405, '%s serialization error' % e) + except ImportError, e: + raise HTTP(405, '%s not available' % e) + except Exception, e: + raise HTTP(405, '%s error' % e) return g def latex_from_html(html): @@ -32,13 +32,13 @@ def latex_from_html(html): def pdflatex_from_html(html): if os.system('which pdflatex > /dev/null')==0: markmin=TAG(html).element('body').flatten(markmin_serializer) - out,warning,errors=markmin2pdf(markmin) + out,warnings,errors=markmin2pdf(markmin) if errors: current.response.headers['Content-Type']='text/html' raise HTTP(405,HTML(BODY(H1('errors'), - LU(*errors), + UL(*errors), H1('warnings'), - LU(*warnings))).xml()) + UL(*warnings))).xml()) else: return XML(out) diff --git a/gluon/contrib/memdb.py b/gluon/contrib/memdb.py index 5fd45ceb..5c1b5dd5 100644 --- a/gluon/contrib/memdb.py +++ b/gluon/contrib/memdb.py @@ -21,6 +21,7 @@ import copy import gluon.validators as validators from gluon.storage import Storage +from gluon import SQLTABLE import random SQL_DIALECTS = {'memcache': { @@ -766,10 +767,10 @@ def __str__(self): def xml(self): """ - serializes the table using sqlhtml.SQLTABLE (if present) + serializes the table using SQLTABLE (if present) """ - return sqlhtml.SQLTABLE(self).xml() + return SQLTABLE(self).xml() def test_all(): diff --git a/gluon/dal.py b/gluon/dal.py index 6ab6672f..39f6084c 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -1468,7 +1468,7 @@ def rowslice(self, rows, minimum=0, maximum=None): def parse_value(self, value, field_type): if field_type != 'blob' and isinstance(value, str): try: - value = value.decode(db._db_codec) + value = value.decode(self.db._db_codec) except Exception: pass if isinstance(value, unicode): @@ -3891,7 +3891,7 @@ def connect(uri=self.uri,m=m): 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(Mer("This is not an official Mongodb uri (http://www.mongodb.org/display/DOCS/Connections) Error : %s" % inst)) + raise SyntaxError("This is not an official Mongodb uri (http://www.mongodb.org/display/DOCS/Connections) Error : %s" % inst) self.pool_connection(connect,cursor=False) @@ -4182,7 +4182,7 @@ def BELONGS(self, first, second): return {self.expand(first) : {"$in" : [ second[:-1]]} } elif second==[] or second==(): return {1:0} - items.append(self.expand(item, first.type) for item in second) + items = [self.expand(item, first.type) for item in second] return {self.expand(first) : {"$in" : items} } def LIKE(self, first, second): @@ -4252,32 +4252,32 @@ def GE(self,first,second=None): return result def ADD(self, first, second): - raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" return '%s + %s' % (self.expand(first), self.expand(second, first.type)) def SUB(self, first, second): - raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" return '(%s - %s)' % (self.expand(first), self.expand(second, first.type)) def MUL(self, first, second): - raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" return '(%s * %s)' % (self.expand(first), self.expand(second, first.type)) def DIV(self, first, second): - raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" return '(%s / %s)' % (self.expand(first), self.expand(second, first.type)) def MOD(self, first, second): - raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" return '(%s %% %s)' % (self.expand(first), self.expand(second, first.type)) def AS(self, first, second): - raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" 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. def ON(self, first, second): - raise NotSupported, "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)) def COMMA(self, first, second): @@ -4311,7 +4311,7 @@ def BELONGS(self, first, second): return {self.expand(first) : {"$in" : [ second[:-1]]} } elif second==[] or second==(): return {1:0} - items.append(self.expand(item, first.type) for item in second) + items = [self.expand(item, first.type) for item in second] return {self.expand(first) : {"$in" : items} } #TODO verify full compatibilty with official SQL Like operator @@ -4390,36 +4390,36 @@ def GE(self,first,second=None): #TODO javascript has math def ADD(self, first, second): - raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" return '%s + %s' % (self.expand(first), self.expand(second, first.type)) #TODO javascript has math def SUB(self, first, second): - raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" return '(%s - %s)' % (self.expand(first), self.expand(second, first.type)) #TODO javascript has math def MUL(self, first, second): - raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" return '(%s * %s)' % (self.expand(first), self.expand(second, first.type)) #TODO javascript has math def DIV(self, first, second): - raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" return '(%s / %s)' % (self.expand(first), self.expand(second, first.type)) #TODO javascript has math def MOD(self, first, second): - raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" return '(%s %% %s)' % (self.expand(first), self.expand(second, first.type)) #TODO javascript can do this def AS(self, first, second): - raise NotSupported, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" 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. def ON(self, first, second): - raise NotSupported, "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)) #TODO is this used in mongodb? diff --git a/gluon/scheduler.py b/gluon/scheduler.py index 99797f9c..f103efb9 100644 --- a/gluon/scheduler.py +++ b/gluon/scheduler.py @@ -160,7 +160,9 @@ def executor(queue,task): result = dumps(_function(*args,**vars)) else: ### for testing purpose only - result = eval(task.function)(*loads(task.args, list_hook),**loads(task.vars, object_hook=_decode_dict)) + result = eval(task.function)( + *loads(task.args, object_hook=_decode_dict), + **loads(task.vars, object_hook=_decode_dict)) stdout, sys.stdout = sys.stdout, stdout queue.put(TaskReport(COMPLETED, result,stdout.getvalue())) except BaseException,e: diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 31426028..f4c353ae 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1606,7 +1606,7 @@ def buttons(edit=False,view=False,record=None): table = db[request.args[-2]] if ondelete: ondelete(table,request.args[-1]) - ret = db(table[self.id_field_name]==request.args[-1]).delete() + ret = db(table[table._id.name]==request.args[-1]).delete() return ret elif csv and len(request.args)>0 and request.args[-1]=='csv': if request.vars.keywords: From 5427c1b503fb887da69210f50818d8b564a46e0b Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Tue, 14 Feb 2012 08:44:25 -0600 Subject: [PATCH 77/77] http://groups.google.com/group/web2py/browse_thread/thread/54dde625a2a19d19#, thanks Roderick --- VERSION | 2 +- gluon/sqlhtml.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 5c1822c2..542b46bc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-14 08:39:29) stable +Version 1.99.4 (2012-02-14 08:44:22) stable diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index f4c353ae..06d51ef4 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1939,7 +1939,9 @@ def index(): previous_tablename,previous_fieldname,previous_id = \ tablename,fieldname,id try: - name = db[referee]._format % record + format = db[referee]._format + if callable(format): name = format(record) + else: name = format % record except TypeError: name = id breadcrumbs += [A(T(db[referee]._plural),