diff --git a/INSTALL b/INSTALL index 599a9d1..a480ad0 100644 --- a/INSTALL +++ b/INSTALL @@ -4,19 +4,17 @@ you shouldn't need if you only want to make a wiki. First, install the prerequisites: - * Python 2.5 + * Python 2.4 * CherryPy 2.2 * PostgreSQL 8.1 * psycopg 2.0 * simplejson 1.3 + * pytz 2006p In Debian GNU/Linux, you can issue the following command to install these packages: - apt-get install python2.5 python-cherrypy postgresql-8.1 python-psycopg2 python-simplejson - -If you're using Debian Etch, see the note below about "psycopg in Debian -Etch". + apt-get install python2.4 python-cherrypy postgresql-8.1 python-psycopg2 python-simplejson python-tz development mode @@ -40,14 +38,15 @@ and set the password to "dev". createuser -S -d -R -P -E luminotes -Initialize the database with the starting schema and basic data: +Initialize the database with the starting schema and default data: - psql -U luminotes postgres -f model/schema.sql - psql -U luminotes postgres -f model/data.sql + createdb -W -U luminotes luminotes + export PYTHONPATH=. + python2.4 tools/initdb.py To start the server in development mode, run: - python2.5 luminotes.py -d + python2.4 luminotes.py -d Connect to the following URL in a web browser running on the same machine: @@ -114,18 +113,20 @@ Restart postgresql so these changes take effect: /etc/init.d/postgresql restart As the PostgreSQL superuser (usually "postgres"), create a new database user -and set the password to "dev". +and set a new password, for instance, "mypassword". createuser -S -d -R -P -E luminotes -Initialize the database with the starting schema and basic data: +Initialize the database with the starting schema and default data: - psql -U luminotes postgres -f model/schema.sql - psql -U luminotes postgres -f model/data.sql + createdb -W -U luminotes luminotes + export PYTHONPATH=. + export PGPASSWORD=mypassword + python2.4 tools/initdb.py Then to actually start the production mode server, run: - python2.5 luminotes.py + python2.4 luminotes.py You should be able to connect to the site at whatever domain you've configured Apache to serve. @@ -137,44 +138,28 @@ Python unit tests If you're interested in running unit tests of the server, install: * nose 0.9.0 + * pysqlite 2.3 In Debian GNU/Linux, you can issue the following command to install this package: - apt-get install python-nose + apt-get install python-nose python-pysqlite2 Then you can run unit tests by running: nosetests -JavaScript unit tests ---------------------- +JavaScript "unit" tests +----------------------- JsUnit is included with Luminotes, so to kick off tests of the client-side JavaScript code, simply run: - python2.5 static/js/test/run_tests.py + python2.4 static/js/test/run_tests.py -The run_tests.py script runs the tests inside browser windows and presumes you -have both Firefox and Internet Explorer 6 installed. Edit run_tests.py if you +The run_tests.py script runs the tests inside browser windows and presumes +that you have both Firefox and Internet Explorer 6 installed, and also that +the Luminotes server is running on the local machine. Edit run_tests.py if you need to specify different paths to the browser binaries or want to test with additional browsers. - - -psycopg in Debian Etch ----------------------- - -As of this writing, Debian Etch does not contain a version of psycopg with -support for Python 2.5. However, the version of psycopg in Debian testing does -support Python 2.5. So you can grab the source for python-psycopg2 from Debian -testing, install the build dependencies (including python2.5-dev), and build -the package yourself on an Etch machine. - -Then, edit /usr/share/python/debian_defaults and move "python2.5" from -"unsupported-versions" to "supported-versions". Finally, install the -python-psycopg2 package you've just built, and it should fully support Python -2.5. - -See Debian bug #404355 for more information. Note that it was fixed in -unstable, but not in Etch. diff --git a/controller/Database.py b/controller/Database.py index 04de62a..185e94e 100644 --- a/controller/Database.py +++ b/controller/Database.py @@ -1,219 +1,158 @@ import re -import bsddb +import os +import psycopg2 as psycopg +from psycopg2.pool import PersistentConnectionPool import random -import cPickle -from cStringIO import StringIO -from copy import copy -from model.Persistent import Persistent -from Async import async class Database( object ): ID_BITS = 128 # number of bits within an id ID_DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz" - def __init__( self, scheduler, database_path = None ): + def __init__( self, connection = None ): """ Create a new database and return it. - @type scheduler: Scheduler - @param scheduler: scheduler to use - @type database_path: unicode - @param database_path: path to the database file + @type connection: existing connection object with cursor()/close()/commit() methods, or NoneType + @param connection: database connection to use (optional, defaults to making a connection pool) @rtype: Database - @return: database at the given path + @return: newly constructed Database """ - self.__scheduler = scheduler - self.__env = bsddb.db.DBEnv() - self.__env.open( None, bsddb.db.DB_CREATE | bsddb.db.DB_PRIVATE | bsddb.db.DB_INIT_MPOOL ) - self.__db = bsddb.db.DB( self.__env ) - self.__db.open( database_path, "database", bsddb.db.DB_HASH, bsddb.db.DB_CREATE ) - self.__cache = {} + # This tells PostgreSQL to give us timestamps in UTC. I'd use "set timezone" instead, but that + # makes SQLite angry. + os.putenv( "PGTZ", "UTC" ) - def __persistent_id( self, obj, skip = None ): - # save the object and return its persistent id - if obj != skip and isinstance( obj, Persistent ): - self.__save( obj ) - return obj.object_id + if connection: + self.__connection = connection + self.__pool = None + else: + self.__connection = None + self.__pool = PersistentConnectionPool( + 1, # minimum connections + 50, # maximum connections + "dbname=luminotes user=luminotes password=%s" % os.getenv( "PGPASSWORD", "dev" ), + ) - # returning None indicates that the object should be pickled normally without using a persistent id - return None + def __get_connection( self ): + if self.__connection: + return self.__connection + else: + return self.__pool.getconn() - @async - def save( self, obj, callback = None ): + def save( self, obj, commit = True ): """ - Save the given object to the database, including any objects that it references. + Save the given object to the database. @type obj: Persistent @param obj: object to save - @type callback: generator or NoneType - @param callback: generator to wakeup when the save is complete (optional) + @type commit: bool + @param commit: True to automatically commit after the save """ - self.__save( obj ) - yield callback + connection = self.__get_connection() + cursor = connection.cursor() - def __save( self, obj ): - # if this object's current revision is already saved, bail - revision_id = obj.revision_id() - if revision_id in self.__cache: - return + cursor.execute( obj.sql_exists() ) + if cursor.fetchone(): + cursor.execute( obj.sql_update() ) + else: + cursor.execute( obj.sql_create() ) - object_id = unicode( obj.object_id ).encode( "utf8" ) - revision_id = unicode( obj.revision_id() ).encode( "utf8" ) - secondary_id = obj.secondary_id and unicode( obj.full_secondary_id() ).encode( "utf8" ) or None + if commit: + connection.commit() - # update the cache with this saved object - self.__cache[ object_id ] = obj - self.__cache[ revision_id ] = copy( obj ) - if secondary_id: - self.__cache[ secondary_id ] = obj + def commit( self ): + self.__get_connection().commit() - # set the pickler up to save persistent ids for every object except for the obj passed in, which - # will be pickled normally - buffer = StringIO() - pickler = cPickle.Pickler( buffer, protocol = -1 ) - pickler.persistent_id = lambda o: self.__persistent_id( o, skip = obj ) - - # pickle the object and write it to the database under both its id key and its revision id key - pickler.dump( obj ) - pickled = buffer.getvalue() - self.__db.put( object_id, pickled ) - self.__db.put( revision_id, pickled ) - - # write the pickled object id (only) to the database under its secondary id - if secondary_id: - buffer = StringIO() - pickler = cPickle.Pickler( buffer, protocol = -1 ) - pickler.persistent_id = lambda o: self.__persistent_id( o ) - pickler.dump( obj ) - self.__db.put( secondary_id, buffer.getvalue() ) - - self.__db.sync() - - @async - def load( self, object_id, callback, revision = None ): + def load( self, Object_type, object_id, revision = None ): """ - Load the object corresponding to the given object id from the database, and yield the provided - callback generator with the loaded object as its argument, or None if the object_id is unknown. - If a revision is provided, a specific revision of the object will be loaded. - - @type object_id: unicode - @param object_id: id of the object to load - @type callback: generator - @param callback: generator to send the loaded object to - @type revision: int or NoneType - @param revision: revision of the object to load (optional) - """ - obj = self.__load( object_id, revision ) - yield callback, obj - - def __load( self, object_id, revision = None ): - if revision is not None: - object_id = Persistent.make_revision_id( object_id, revision ) - - object_id = unicode( object_id ).encode( "utf8" ) - - # if the object corresponding to the given id has already been loaded, simply return it without - # loading it again - obj = self.__cache.get( object_id ) - if obj is not None: - return obj - - # grab the object for the given id from the database - buffer = StringIO() - unpickler = cPickle.Unpickler( buffer ) - unpickler.persistent_load = self.__load - - pickled = self.__db.get( object_id ) - if pickled is None or pickled == "": - return None - - buffer.write( pickled ) - buffer.flush() - buffer.seek( 0 ) - - # unpickle the object and update the cache with this saved object - obj = unpickler.load() - if obj is None: - print "error unpickling %s: %s" % ( object_id, pickled ) - return None - self.__cache[ unicode( obj.object_id ).encode( "utf8" ) ] = obj - self.__cache[ unicode( obj.revision_id() ).encode( "utf8" ) ] = copy( obj ) - - return obj - - @async - def reload( self, object_id, callback = None ): - """ - Load and immediately save the object corresponding to the given object id or database key. This - is useful when the object has a __setstate__() method that performs some sort of schema - evolution operation. - - @type object_id: unicode - @param object_id: id or key of the object to reload - @type callback: generator or NoneType - @param callback: generator to wakeup when the save is complete (optional) - """ - self.__reload( object_id ) - yield callback - - def __reload( self, object_id, revision = None ): - object_id = unicode( object_id ).encode( "utf8" ) - - # grab the object for the given id from the database - buffer = StringIO() - unpickler = cPickle.Unpickler( buffer ) - unpickler.persistent_load = self.__load - - pickled = self.__db.get( object_id ) - if pickled is None or pickled == "": - return - - buffer.write( pickled ) - buffer.flush() - buffer.seek( 0 ) - - # unpickle the object. this should trigger __setstate__() if the object has such a method - obj = unpickler.load() - if obj is None: - print "error unpickling %s: %s" % ( object_id, pickled ) - return - self.__cache[ object_id ] = obj - - # set the pickler up to save persistent ids for every object except for the obj passed in, which - # will be pickled normally - buffer = StringIO() - pickler = cPickle.Pickler( buffer, protocol = -1 ) - pickler.persistent_id = lambda o: self.__persistent_id( o, skip = obj ) - - # pickle the object and write it to the database under its id key - pickler.dump( obj ) - pickled = buffer.getvalue() - self.__db.put( object_id, pickled ) - - self.__db.sync() - - def size( self, object_id, revision = None ): - """ - Load the object corresponding to the given object id from the database, and return the size of - its pickled data in bytes. If a revision is provided, a specific revision of the object will be + Load the object corresponding to the given object id from the database and return it, or None if + the object_id is unknown. If a revision is provided, a specific revision of the object will be loaded. + @type Object_type: type + @param Object_type: class of the object to load @type object_id: unicode - @param object_id: id of the object whose size should be returned + @param object_id: id of the object to load @type revision: int or NoneType @param revision: revision of the object to load (optional) + @rtype: Object_type or NoneType + @return: loaded object, or None if no match """ - if revision is not None: - object_id = Persistent.make_revision_id( object_id, revision ) + return self.select_one( Object_type, Object_type.sql_load( object_id, revision ) ) - object_id = unicode( object_id ).encode( "utf8" ) + def select_one( self, Object_type, sql_command ): + """ + Execute the given sql_command and return its results in the form of an object of Object_type, + or None if there was no match. - pickled = self.__db.get( object_id ) - if pickled is None or pickled == "": + @type Object_type: type + @param Object_type: class of the object to load + @type sql_command: unicode + @param sql_command: SQL command to execute + @rtype: Object_type or NoneType + @return: loaded object, or None if no match + """ + connection = self.__get_connection() + cursor = connection.cursor() + + cursor.execute( sql_command ) + + row = cursor.fetchone() + if not row: return None - return len( pickled ) + if Object_type in ( tuple, list ): + return Object_type( row ) + else: + return Object_type( *row ) + + def select_many( self, Object_type, sql_command ): + """ + Execute the given sql_command and return its results in the form of a list of objects of + Object_type. + + @type Object_type: type + @param Object_type: class of the object to load + @type sql_command: unicode + @param sql_command: SQL command to execute + @rtype: list of Object_type + @return: loaded objects + """ + connection = self.__get_connection() + cursor = connection.cursor() + + cursor.execute( sql_command ) + + objects = [] + row = cursor.fetchone() + + while row: + if Object_type in ( tuple, list ): + obj = Object_type( row ) + else: + obj = Object_type( *row ) + + objects.append( obj ) + row = cursor.fetchone() + + return objects + + def execute( self, sql_command, commit = True ): + """ + Execute the given sql_command. + + @type sql_command: unicode + @param sql_command: SQL command to execute + @type commit: bool + @param commit: True to automatically commit after the command + """ + connection = self.__get_connection() + cursor = connection.cursor() + + cursor.execute( sql_command ) + + if commit: + connection.commit() @staticmethod def generate_id(): @@ -231,44 +170,45 @@ class Database( object ): return "".join( digits ) - @async - def next_id( self, callback ): + def next_id( self, Object_type, commit = True ): """ - Generate the next available object id, and yield the provided callback generator with the - object id as its argument. + Generate the next available object id and return it. - @type callback: generator - @param callback: generator to send the next available object id to + @type Object_type: type + @param Object_type: class of the object that the id is for + @type commit: bool + @param commit: True to automatically commit after storing the next id """ + connection = self.__get_connection() + cursor = connection.cursor() + # generate a random id, but on the off-chance that it collides with something else already in # the database, try again next_id = Database.generate_id() - while self.__db.get( next_id, default = None ) is not None: + cursor.execute( Object_type.sql_id_exists( next_id ) ) + + while cursor.fetchone() is not None: next_id = Database.generate_id() + cursor.execute( Object_type.sql_id_exists( next_id ) ) - # save the next_id as a key in the database so that it's not handed out again to another client - self.__db[ next_id ] = "" + # save a new object with the next_id to the database + obj = Object_type( next_id ) + cursor.execute( obj.sql_create() ) - yield callback, next_id + if commit: + connection.commit() + + return next_id - @async def close( self ): """ Shutdown the database. """ - self.__db.close() - self.__env.close() - yield None + if self.__connection: + self.__connection.close() - @async - def clear_cache( self ): - """ - Clear the memory object cache. - """ - self.__cache.clear() - yield None - - scheduler = property( lambda self: self.__scheduler ) + if self.__pool: + self.__pool.closeall() class Valid_id( object ): @@ -289,9 +229,9 @@ class Valid_id( object ): class Valid_revision( object ): """ - Validator for an object id. + Validator for an object revision timestamp. """ - REVISION_PATTERN = re.compile( "^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d+$" ) + REVISION_PATTERN = re.compile( "^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d+[+-]\d\d(:)?\d\d$" ) def __init__( self, none_okay = False ): self.__none_okay = none_okay diff --git a/controller/Expose.py b/controller/Expose.py index 067cebf..7b83051 100644 --- a/controller/Expose.py +++ b/controller/Expose.py @@ -1,18 +1,10 @@ import cherrypy -from Validate import Validation_error - # module-level variable that, when set to a view, overrides the view for all exposed methods. used # by unit tests view_override = None -class Expose_error( Exception ): - def __init__( self, message ): - Exception.__init__( self, message ) - self.__message = message - - def expose( view = None, rss = None ): """ expose() can be used to tag a method as available for publishing to the web via CherryPy. In @@ -57,8 +49,16 @@ def expose( view = None, rss = None ): # try executing the exposed function try: result = function( *args, **kwargs ) - except Validation_error, error: - result = dict( name = error.name, value = error.value, error = error.message ) + except cherrypy.NotFound: + raise + except Exception, error: + if hasattr( error, "to_dict" ): + result = error.to_dict() + else: + # TODO: it'd be nice to send an email to myself with the traceback + import traceback + traceback.print_exc() + result = dict( error = u"An error occurred when processing your request. Please try again or contact support." ) redirect = result.get( u"redirect", None ) @@ -74,7 +74,7 @@ def expose( view = None, rss = None ): return unicode( view_override( **result ) ) except: if redirect is None: - raise Expose_error( result.get( u"error" ) or result ) + raise # if that doesn't work, and there's a redirect, then redirect del( result[ u"redirect" ] ) diff --git a/controller/Notebooks.py b/controller/Notebooks.py index a825e8a..40cdaf9 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -1,15 +1,13 @@ import cherrypy -from Scheduler import Scheduler +from datetime import datetime from Expose import expose from Validate import validate, Valid_string, Validation_error, Valid_bool from Database import Valid_id, Valid_revision from Users import grab_user_id -from Updater import wait_for_update, update_client from Expire import strongly_expire from Html_nuker import Html_nuker -from Async import async -from model.Notebook import Notebook -from model.Note import Note +from new_model.Notebook import Notebook +from new_model.Note import Note from view.Main_page import Main_page from view.Json import Json from view.Html_file import Html_file @@ -18,7 +16,7 @@ from view.Html_file import Html_file class Access_error( Exception ): def __init__( self, message = None ): if message is None: - message = u"You don't have access to this notebook." + message = u"Sorry, you don't have access to do that." Exception.__init__( self, message ) self.__message = message @@ -33,12 +31,10 @@ class Notebooks( object ): """ Controller for dealing with notebooks and their notes, corresponding to the "/notebooks" URL. """ - def __init__( self, scheduler, database, users ): + def __init__( self, database, users ): """ Create a new Notebooks object. - @type scheduler: controller.Scheduler - @param scheduler: scheduler to use for asynchronous calls @type database: controller.Database @param database: database that notebooks are stored in @type users: controller.Users @@ -46,7 +42,6 @@ class Notebooks( object ): @rtype: Notebooks @return: newly constructed Notebooks """ - self.__scheduler = scheduler self.__database = database self.__users = users @@ -83,14 +78,11 @@ class Notebooks( object ): @expose( view = Json ) @strongly_expire - @wait_for_update @grab_user_id - @async - @update_client @validate( notebook_id = Valid_id(), note_id = Valid_id( none_okay = True ), - revision = Valid_string( min = 0, max = 30 ), + revision = Valid_revision( none_okay = True ), user_id = Valid_id( none_okay = True ), ) def contents( self, notebook_id, note_id = None, revision = None, user_id = None ): @@ -108,39 +100,37 @@ class Notebooks( object ): @param user_id: id of current logged-in user (if any), determined by @grab_user_id @rtype: json dict @return: { 'notebook': notebookdict, 'note': notedict or None } - @raise Access_error: the current user doesn't have access to the given notebook + @raise Access_error: the current user doesn't have access to the given notebook or note @raise Validation_error: one of the arguments is invalid """ - self.check_access( notebook_id, user_id, self.__scheduler.thread ) - if not ( yield Scheduler.SLEEP ): + if not self.__users.check_access( user_id, notebook_id ): raise Access_error() - self.__database.load( notebook_id, self.__scheduler.thread ) - notebook = ( yield Scheduler.SLEEP ) + notebook = self.__database.load( Notebook, notebook_id ) + + if not self.__users.check_access( user_id, notebook_id, read_write = True ): + notebook.read_write = False if notebook is None: note = None elif note_id == u"blank": - note = Note( note_id ) + note = Note.create( note_id ) else: - note = notebook.lookup_note( note_id ) + note = self.__database.load( Note, note_id, revision ) + if note and note.notebook_id != notebook_id: + raise Access_error() - if revision: - self.__database.load( note_id, self.__scheduler.thread, revision ) - note = ( yield Scheduler.SLEEP ) + startup_notes = self.__database.select_many( Note, notebook.sql_load_startup_notes() ) - yield dict( + return dict( notebook = notebook, - startup_notes = notebook.startup_notes, + startup_notes = startup_notes, note = note, ) @expose( view = Json ) @strongly_expire - @wait_for_update @grab_user_id - @async - @update_client @validate( notebook_id = Valid_id(), note_id = Valid_id(), @@ -161,35 +151,24 @@ class Notebooks( object ): @param user_id: id of current logged-in user (if any), determined by @grab_user_id @rtype: json dict @return: { 'note': notedict or None } - @raise Access_error: the current user doesn't have access to the given notebook + @raise Access_error: the current user doesn't have access to the given notebook or note @raise Validation_error: one of the arguments is invalid """ - self.check_access( notebook_id, user_id, self.__scheduler.thread ) - if not ( yield Scheduler.SLEEP ): + if not self.__users.check_access( user_id, notebook_id ): raise Access_error() - self.__database.load( notebook_id, self.__scheduler.thread ) - notebook = ( yield Scheduler.SLEEP ) + note = self.__database.load( Note, note_id, revision ) - if notebook is None: - note = None - else: - note = notebook.lookup_note( note_id ) + if note and note.notebook_id != notebook_id: + raise Access_error() - if revision: - self.__database.load( note_id, self.__scheduler.thread, revision ) - note = ( yield Scheduler.SLEEP ) - - yield dict( + return dict( note = note, ) @expose( view = Json ) @strongly_expire - @wait_for_update @grab_user_id - @async - @update_client @validate( notebook_id = Valid_id(), note_title = Valid_string( min = 1, max = 500 ), @@ -210,28 +189,23 @@ class Notebooks( object ): @raise Access_error: the current user doesn't have access to the given notebook @raise Validation_error: one of the arguments is invalid """ - self.check_access( notebook_id, user_id, self.__scheduler.thread ) - if not ( yield Scheduler.SLEEP ): + if not self.__users.check_access( user_id, notebook_id ): raise Access_error() - self.__database.load( notebook_id, self.__scheduler.thread ) - notebook = ( yield Scheduler.SLEEP ) + notebook = self.__database.load( Notebook, notebook_id ) if notebook is None: note = None else: - note = notebook.lookup_note_by_title( note_title ) + note = self.__database.select_one( Notebook, notebook.sql_load_note_by_title( note_title ) ) - yield dict( + return dict( note = note, ) @expose( view = Json ) @strongly_expire - @wait_for_update @grab_user_id - @async - @update_client @validate( notebook_id = Valid_id(), note_title = Valid_string( min = 1, max = 500 ), @@ -252,27 +226,61 @@ class Notebooks( object ): @raise Access_error: the current user doesn't have access to the given notebook @raise Validation_error: one of the arguments is invalid """ - self.check_access( notebook_id, user_id, self.__scheduler.thread ) - if not ( yield Scheduler.SLEEP ): + if not self.__users.check_access( user_id, notebook_id ): raise Access_error() - self.__database.load( notebook_id, self.__scheduler.thread ) - notebook = ( yield Scheduler.SLEEP ) + notebook = self.__database.load( Notebook, notebook_id ) if notebook is None: note = None else: - note = notebook.lookup_note_by_title( note_title ) + note = self.__database.select_one( Notebook, notebook.sql_load_note_by_title( note_title ) ) - yield dict( + return dict( note_id = note and note.object_id or None, ) @expose( view = Json ) - @wait_for_update + @strongly_expire + @grab_user_id + @validate( + notebook_id = Valid_id(), + note_id = Valid_id(), + user_id = Valid_id( none_okay = True ), + ) + def load_note_revisions( self, notebook_id, note_id, user_id = None ): + """ + Return the full list of revision timestamps for this note in chronological order. + + @type notebook_id: unicode + @param notebook_id: id of notebook the note is in + @type note_id: unicode + @param note_id: id of note in question + @type user_id: unicode or NoneType + @param user_id: id of current logged-in user (if any), determined by @grab_user_id + @rtype: json dict + @return: { 'revisions': revisionslist or None } + @raise Access_error: the current user doesn't have access to the given notebook or note + @raise Validation_error: one of the arguments is invalid + """ + if not self.__users.check_access( user_id, notebook_id ): + raise Access_error() + + note = self.__database.load( Note, note_id ) + + if note: + if note.notebook_id != notebook_id: + raise Access_error() + revisions = self.__database.select_many( unicode, note.sql_load_revisions() ) + else: + revisions = None + + return dict( + revisions = revisions, + ) + + @expose( view = Json ) @grab_user_id - @async - @update_client @validate( notebook_id = Valid_id(), note_id = Valid_id(), @@ -310,186 +318,78 @@ class Notebooks( object ): @raise Access_error: the current user doesn't have access to the given notebook @raise Validation_error: one of the arguments is invalid """ - self.check_access( notebook_id, user_id, self.__scheduler.thread ) - if not ( yield Scheduler.SLEEP ): + if not self.__users.check_access( user_id, notebook_id, read_write = True ): raise Access_error() - self.__database.load( notebook_id, self.__scheduler.thread ) - notebook = ( yield Scheduler.SLEEP ) + notebook = self.__database.load( Notebook, notebook_id ) if not notebook: raise Access_error() - self.__database.load( note_id, self.__scheduler.thread ) - note = ( yield Scheduler.SLEEP ) + note = self.__database.load( Note, note_id ) # check whether the provided note contents have been changed since the previous revision - def update_note( current_notebook, old_note ): + def update_note( current_notebook, old_note, startup ): # the note hasn't been changed, so bail without updating it - if contents == old_note.contents: + if contents == old_note.contents and startup == old_note.startup: new_revision = None # the note has changed, so update it else: - notebook.update_note( note, contents ) + note.contents = contents + note.startup = startup + if startup: + if note.rank is None: + note.rank = self.__database.select_one( float, notebook.sql_highest_rank() ) + 1 + else: + note.rank = None + new_revision = note.revision return new_revision # if the note is already in the given notebook, load it and update it - if note and note in notebook.notes: - self.__database.load( note_id, self.__scheduler.thread, previous_revision ) - old_note = ( yield Scheduler.SLEEP ) + if note and note.notebook_id == notebook.object_id: + old_note = self.__database.load( Note, note_id, previous_revision ) previous_revision = note.revision - new_revision = update_note( notebook, old_note ) + new_revision = update_note( notebook, old_note, startup ) # the note is not already in the given notebook, so look for it in the trash - elif note and notebook.trash and note in notebook.trash.notes: - self.__database.load( note_id, self.__scheduler.thread, previous_revision ) - old_note = ( yield Scheduler.SLEEP ) + elif note and notebook.trash_id and note.notebook_id == notebook.trash_id: + old_note = self.__database.load( Note, note_id, previous_revision ) # undelete the note, putting it back in the given notebook previous_revision = note.revision - notebook.trash.remove_note( note ) - note.deleted_from = None - notebook.add_note( note ) - - new_revision = update_note( notebook, old_note ) + note.notebook_id = notebook.object_id + note.deleted_from_id = None + new_revision = update_note( notebook, old_note, startup ) # otherwise, create a new note else: + if startup: + rank = self.__database.select_one( float, notebook.sql_highest_rank() ) + 1 + else: + rank = None + previous_revision = None - note = Note( note_id, contents ) - notebook.add_note( note ) + note = Note.create( note_id, contents, notebook_id = notebook.object_id, startup = startup, rank = rank ) new_revision = note.revision - if startup: - startup_changed = notebook.add_startup_note( note ) - else: - startup_changed = notebook.remove_startup_note( note ) - - if new_revision or startup_changed: - self.__database.save( notebook, self.__scheduler.thread ) - yield Scheduler.SLEEP - self.__users.update_storage( user_id, self.__scheduler.thread ) - user = ( yield Scheduler.SLEEP ) - self.__database.save( user ) + if new_revision: + self.__database.save( note, commit = False ) + user = self.__users.update_storage( user_id, commit = False ) + self.__database.commit() else: user = None - yield dict( + return dict( new_revision = new_revision, previous_revision = previous_revision, storage_bytes = user and user.storage_bytes or 0, ) @expose( view = Json ) - @wait_for_update @grab_user_id - @async - @update_client - @validate( - notebook_id = Valid_id(), - note_id = Valid_id(), - user_id = Valid_id( none_okay = True ), - ) - def add_startup_note( self, notebook_id, note_id, user_id ): - """ - Designate a particular note to be shown upon startup, e.g. whenever its notebook is displayed. - The given note must already be within this notebook. - - @type notebook_id: unicode - @param notebook_id: id of notebook the note is in - @type note_id: unicode - @param note_id: id of note to show on startup - @type user_id: unicode or NoneType - @param user_id: id of current logged-in user (if any), determined by @grab_user_id - @rtype: json dict - @return: { 'storage_bytes': current storage usage by user } - @raise Access_error: the current user doesn't have access to the given notebook - @raise Validation_error: one of the arguments is invalid - """ - self.check_access( notebook_id, user_id, self.__scheduler.thread ) - if not ( yield Scheduler.SLEEP ): - raise Access_error() - - self.__database.load( notebook_id, self.__scheduler.thread ) - notebook = ( yield Scheduler.SLEEP ) - - if not notebook: - raise Access_error() - - self.__database.load( note_id, self.__scheduler.thread ) - note = ( yield Scheduler.SLEEP ) - - if note: - notebook.add_startup_note( note ) - self.__database.save( notebook, self.__scheduler.thread ) - yield Scheduler.SLEEP - self.__users.update_storage( user_id, self.__scheduler.thread ) - user = ( yield Scheduler.SLEEP ) - self.__database.save( user ) - - yield dict( storage_bytes = user.storage_bytes ) - else: - yield dict( storage_bytes = 0 ) - - @expose( view = Json ) - @wait_for_update - @grab_user_id - @async - @update_client - @validate( - notebook_id = Valid_id(), - note_id = Valid_id(), - user_id = Valid_id( none_okay = True ), - ) - def remove_startup_note( self, notebook_id, note_id, user_id ): - """ - Prevent a particular note from being shown on startup, e.g. whenever its notebook is displayed. - The given note must already be within this notebook. - - @type notebook_id: unicode - @param notebook_id: id of notebook the note is in - @type note_id: unicode - @param note_id: id of note to no longer show on startup - @type user_id: unicode or NoneType - @param user_id: id of current logged-in user (if any), determined by @grab_user_id - @rtype: json dict - @return: { 'storage_bytes': current storage usage by user } - @raise Access_error: the current user doesn't have access to the given notebook - @raise Validation_error: one of the arguments is invalid - """ - self.check_access( notebook_id, user_id, self.__scheduler.thread ) - if not ( yield Scheduler.SLEEP ): - raise Access_error() - - self.__database.load( notebook_id, self.__scheduler.thread ) - notebook = ( yield Scheduler.SLEEP ) - - if not notebook: - raise Access_error() - - self.__database.load( note_id, self.__scheduler.thread ) - note = ( yield Scheduler.SLEEP ) - - if note: - notebook.remove_startup_note( note ) - self.__database.save( notebook, self.__scheduler.thread ) - yield Scheduler.SLEEP - self.__users.update_storage( user_id, self.__scheduler.thread ) - user = ( yield Scheduler.SLEEP ) - self.__database.save( user ) - - yield dict( storage_bytes = user.storage_bytes ) - else: - yield dict( storage_bytes = 0 ) - - @expose( view = Json ) - @wait_for_update - @grab_user_id - @async - @update_client @validate( notebook_id = Valid_id(), note_id = Valid_id(), @@ -512,42 +412,34 @@ class Notebooks( object ): @raise Access_error: the current user doesn't have access to the given notebook @raise Validation_error: one of the arguments is invalid """ - self.check_access( notebook_id, user_id, self.__scheduler.thread ) - if not ( yield Scheduler.SLEEP ): + if not self.__users.check_access( user_id, notebook_id, read_write = True ): raise Access_error() - self.__database.load( notebook_id, self.__scheduler.thread ) - notebook = ( yield Scheduler.SLEEP ) + notebook = self.__database.load( Notebook, notebook_id ) if not notebook: raise Access_error() - self.__database.load( note_id, self.__scheduler.thread ) - note = ( yield Scheduler.SLEEP ) + note = self.__database.load( Note, note_id ) - if note: - notebook.remove_note( note ) + if note and note.notebook_id == notebook_id: + if notebook.trash_id: + note.deleted_from_id = notebook_id + note.notebook_id = notebook.trash_id + note.startup = True + else: + note.notebook_id = None - if notebook.trash: - note.deleted_from = notebook.object_id - notebook.trash.add_note( note ) - notebook.trash.add_startup_note( note ) + self.__database.save( note, commit = False ) + user = self.__users.update_storage( user_id, commit = False ) + self.__database.commit() - self.__database.save( notebook, self.__scheduler.thread ) - yield Scheduler.SLEEP - self.__users.update_storage( user_id, self.__scheduler.thread ) - user = ( yield Scheduler.SLEEP ) - self.__database.save( user ) - - yield dict( storage_bytes = user.storage_bytes ) + return dict( storage_bytes = user.storage_bytes ) else: - yield dict( storage_bytes = 0 ) + return dict( storage_bytes = 0 ) @expose( view = Json ) - @wait_for_update @grab_user_id - @async - @update_client @validate( notebook_id = Valid_id(), note_id = Valid_id(), @@ -569,50 +461,39 @@ class Notebooks( object ): @raise Access_error: the current user doesn't have access to the given notebook @raise Validation_error: one of the arguments is invalid """ - self.check_access( notebook_id, user_id, self.__scheduler.thread ) - if not ( yield Scheduler.SLEEP ): + if not self.__users.check_access( user_id, notebook_id, read_write = True ): raise Access_error() - self.__database.load( notebook_id, self.__scheduler.thread ) - notebook = ( yield Scheduler.SLEEP ) + notebook = self.__database.load( Notebook, notebook_id ) if not notebook: raise Access_error() - self.__database.load( note_id, self.__scheduler.thread ) - note = ( yield Scheduler.SLEEP ) + note = self.__database.load( Note, note_id ) - if note and notebook.trash: + if note and notebook.trash_id: # if the note isn't deleted, and it's already in this notebook, just return - if note.deleted_from is None and notebook.lookup_note( note.object_id ): - yield dict( storage_bytes = 0 ) - return + if note.deleted_from_id is None and note.notebook_id == notebook_id: + return dict( storage_bytes = 0 ) # if the note was deleted from a different notebook than the notebook given, raise - if note.deleted_from != notebook_id: + if note.deleted_from_id != notebook_id: raise Access_error() - notebook.trash.remove_note( note ) + note.notebook_id = note.deleted_from_id + note.deleted_from_id = None + note.startup = True - note.deleted_from = None - notebook.add_note( note ) - notebook.add_startup_note( note ) + self.__database.save( note, commit = False ) + user = self.__users.update_storage( user_id, commit = False ) + self.__database.commit() - self.__database.save( notebook, self.__scheduler.thread ) - yield Scheduler.SLEEP - self.__users.update_storage( user_id, self.__scheduler.thread ) - user = ( yield Scheduler.SLEEP ) - self.__database.save( user ) - - yield dict( storage_bytes = user.storage_bytes ) + return dict( storage_bytes = user.storage_bytes ) else: - yield dict( storage_bytes = 0 ) + return dict( storage_bytes = 0 ) @expose( view = Json ) - @wait_for_update @grab_user_id - @async - @update_client @validate( notebook_id = Valid_id(), user_id = Valid_id( none_okay = True ), @@ -632,40 +513,35 @@ class Notebooks( object ): @raise Access_error: the current user doesn't have access to the given notebook @raise Validation_error: one of the arguments is invalid """ - self.check_access( notebook_id, user_id, self.__scheduler.thread ) - if not ( yield Scheduler.SLEEP ): + if not self.__users.check_access( user_id, notebook_id, read_write = True ): raise Access_error() - self.__database.load( notebook_id, self.__scheduler.thread ) - notebook = ( yield Scheduler.SLEEP ) + notebook = self.__database.load( Notebook, notebook_id ) if not notebook: raise Access_error() - for note in notebook.notes: - notebook.remove_note( note ) + notes = self.__database.select_many( Note, notebook.sql_load_notes() ) - if notebook.trash: - note.deleted_from = notebook.object_id - notebook.trash.add_note( note ) - notebook.trash.add_startup_note( note ) + for note in notes: + if notebook.trash_id: + note.deleted_from_id = notebook_id + note.notebook_id = notebook.trash_id + note.startup = True + else: + note.notebook_id = None + self.__database.save( note, commit = False ) - self.__database.save( notebook, self.__scheduler.thread ) - yield Scheduler.SLEEP - self.__users.update_storage( user_id, self.__scheduler.thread ) - user = ( yield Scheduler.SLEEP ) - self.__database.save( user ) + user = self.__users.update_storage( user_id, commit = False ) + self.__database.commit() - yield dict( + return dict( storage_bytes = user.storage_bytes, ) @expose( view = Json ) @strongly_expire - @wait_for_update @grab_user_id - @async - @update_client @validate( notebook_id = Valid_id(), search_text = Valid_string( min = 0, max = 100 ), @@ -688,41 +564,39 @@ class Notebooks( object ): @raise Access_error: the current user doesn't have access to the given notebook @raise Validation_error: one of the arguments is invalid """ - self.check_access( notebook_id, user_id, self.__scheduler.thread ) - if not ( yield Scheduler.SLEEP ): + if not self.__users.check_access( user_id, notebook_id ): raise Access_error() - self.__database.load( notebook_id, self.__scheduler.thread ) - notebook = ( yield Scheduler.SLEEP ) + notebook = self.__database.load( Notebook, notebook_id ) if not notebook: raise Access_error() search_text = search_text.lower() + if len( search_text ) == 0: + return dict( notes = [] ) + title_matches = [] content_matches = [] nuker = Html_nuker() - if len( search_text ) > 0: - for note in notebook.notes: - if note is None: continue - if search_text in nuker.nuke( note.title ).lower(): - title_matches.append( note ) - elif search_text in nuker.nuke( note.contents ).lower(): - content_matches.append( note ) + notes = self.__database.select_many( Note, notebook.sql_search_notes( search_text ) ) - notes = title_matches + content_matches + # further narrow the search results by making sure notes still match after all HTML tags are + # stripped out + for note in notes: + if search_text in nuker.nuke( note.title ).lower(): + title_matches.append( note ) + elif search_text in nuker.nuke( note.contents ).lower(): + content_matches.append( note ) - yield dict( - notes = notes, + return dict( + notes = title_matches + content_matches, ) @expose( view = Json ) @strongly_expire - @wait_for_update @grab_user_id - @async - @update_client @validate( notebook_id = Valid_id(), user_id = Valid_id( none_okay = True ), @@ -740,29 +614,23 @@ class Notebooks( object ): @raise Access_error: the current user doesn't have access to the given notebook @raise Validation_error: one of the arguments is invalid """ - self.check_access( notebook_id, user_id, self.__scheduler.thread ) - if not ( yield Scheduler.SLEEP ): + if not self.__users.check_access( user_id, notebook_id ): raise Access_error() - self.__database.load( notebook_id, self.__scheduler.thread ) - notebook = ( yield Scheduler.SLEEP ) + notebook = self.__database.load( Notebook, notebook_id ) if not notebook: raise Access_error() - notes = [ note for note in notebook.notes if note is not None and note.title is not None ] - notes.sort( lambda a, b: cmp( b.revision, a.revision ) ) + notes = self.__database.select_many( Note, notebook.sql_load_notes() ) - yield dict( + return dict( notes = [ ( note.object_id, note.title ) for note in notes ] ) @expose( view = Html_file ) @strongly_expire - @wait_for_update @grab_user_id - @async - @update_client @validate( notebook_id = Valid_id(), user_id = Valid_id( none_okay = True ), @@ -780,42 +648,18 @@ class Notebooks( object ): @raise Access_error: the current user doesn't have access to the given notebook @raise Validation_error: one of the arguments is invalid """ - self.check_access( notebook_id, user_id, self.__scheduler.thread ) - if not ( yield Scheduler.SLEEP ): + if not self.__users.check_access( user_id, notebook_id ): raise Access_error() - self.__database.load( notebook_id, self.__scheduler.thread ) - notebook = ( yield Scheduler.SLEEP ) + notebook = self.__database.load( Notebook, notebook_id ) if not notebook: raise Access_error() - normal_notes = list( set( notebook.notes ) - set( notebook.startup_notes ) ) - normal_notes.sort( lambda a, b: -cmp( a.revision, b.revision ) ) - - yield dict( + startup_notes = self.__database.select_many( Note, notebook.sql_load_startup_notes() ) + other_notes = self.__database.select_many( Note, notebook.sql_load_non_startup_notes() ) + + return dict( notebook_name = notebook.name, - notes = [ note for note in notebook.startup_notes + normal_notes if note is not None ], + notes = startup_notes + other_notes, ) - - @async - def check_access( self, notebook_id, user_id, callback ): - # check if the anonymous user has access to this notebook - self.__database.load( u"User anonymous", self.__scheduler.thread ) - anonymous = ( yield Scheduler.SLEEP ) - - access = False - if anonymous.has_access( notebook_id ): - access = True - - if user_id: - # check if the currently logged in user has access to this notebook - self.__database.load( user_id, self.__scheduler.thread ) - user = ( yield Scheduler.SLEEP ) - - if user and user.has_access( notebook_id ): - access = True - - yield callback, access - - scheduler = property( lambda self: self.__scheduler ) diff --git a/controller/Old_database.py b/controller/Old_database.py new file mode 100644 index 0000000..e56dd1f --- /dev/null +++ b/controller/Old_database.py @@ -0,0 +1,303 @@ +import re +import bsddb +import random +import cPickle +from cStringIO import StringIO +from copy import copy +from model.Persistent import Persistent +from Async import async + + +class Old_database( object ): + ID_BITS = 128 # number of bits within an id + ID_DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz" + + def __init__( self, scheduler, database_path = None ): + """ + Create a new database and return it. + + @type scheduler: Scheduler + @param scheduler: scheduler to use + @type database_path: unicode + @param database_path: path to the database file + @rtype: Old_database + @return: database at the given path + """ + self.__scheduler = scheduler + self.__env = bsddb.db.DBEnv() + self.__env.open( None, bsddb.db.DB_CREATE | bsddb.db.DB_PRIVATE | bsddb.db.DB_INIT_MPOOL ) + self.__db = bsddb.db.DB( self.__env ) + self.__db.open( database_path, "database", bsddb.db.DB_HASH, bsddb.db.DB_CREATE ) + self.__cache = {} + + def __persistent_id( self, obj, skip = None ): + # save the object and return its persistent id + if obj != skip and isinstance( obj, Persistent ): + self.__save( obj ) + return obj.object_id + + # returning None indicates that the object should be pickled normally without using a persistent id + return None + + @async + def save( self, obj, callback = None ): + """ + Save the given object to the database, including any objects that it references. + + @type obj: Persistent + @param obj: object to save + @type callback: generator or NoneType + @param callback: generator to wakeup when the save is complete (optional) + """ + self.__save( obj ) + yield callback + + def __save( self, obj ): + # if this object's current revision is already saved, bail + revision_id = obj.revision_id() + if revision_id in self.__cache: + return + + object_id = unicode( obj.object_id ).encode( "utf8" ) + revision_id = unicode( obj.revision_id() ).encode( "utf8" ) + secondary_id = obj.secondary_id and unicode( obj.full_secondary_id() ).encode( "utf8" ) or None + + # update the cache with this saved object + self.__cache[ object_id ] = obj + self.__cache[ revision_id ] = copy( obj ) + if secondary_id: + self.__cache[ secondary_id ] = obj + + # set the pickler up to save persistent ids for every object except for the obj passed in, which + # will be pickled normally + buffer = StringIO() + pickler = cPickle.Pickler( buffer, protocol = -1 ) + pickler.persistent_id = lambda o: self.__persistent_id( o, skip = obj ) + + # pickle the object and write it to the database under both its id key and its revision id key + pickler.dump( obj ) + pickled = buffer.getvalue() + self.__db.put( object_id, pickled ) + self.__db.put( revision_id, pickled ) + + # write the pickled object id (only) to the database under its secondary id + if secondary_id: + buffer = StringIO() + pickler = cPickle.Pickler( buffer, protocol = -1 ) + pickler.persistent_id = lambda o: self.__persistent_id( o ) + pickler.dump( obj ) + self.__db.put( secondary_id, buffer.getvalue() ) + + self.__db.sync() + + @async + def load( self, object_id, callback, revision = None ): + """ + Load the object corresponding to the given object id from the database, and yield the provided + callback generator with the loaded object as its argument, or None if the object_id is unknown. + If a revision is provided, a specific revision of the object will be loaded. + + @type object_id: unicode + @param object_id: id of the object to load + @type callback: generator + @param callback: generator to send the loaded object to + @type revision: int or NoneType + @param revision: revision of the object to load (optional) + """ + obj = self.__load( object_id, revision ) + yield callback, obj + + def __load( self, object_id, revision = None ): + if revision is not None: + object_id = Persistent.make_revision_id( object_id, revision ) + + object_id = unicode( object_id ).encode( "utf8" ) + + # if the object corresponding to the given id has already been loaded, simply return it without + # loading it again + obj = self.__cache.get( object_id ) + if obj is not None: + return obj + + # grab the object for the given id from the database + buffer = StringIO() + unpickler = cPickle.Unpickler( buffer ) + unpickler.persistent_load = self.__load + + pickled = self.__db.get( object_id ) + if pickled is None or pickled == "": + return None + + buffer.write( pickled ) + buffer.flush() + buffer.seek( 0 ) + + # unpickle the object and update the cache with this saved object + obj = unpickler.load() + if obj is None: + print "error unpickling %s: %s" % ( object_id, pickled ) + return None + self.__cache[ unicode( obj.object_id ).encode( "utf8" ) ] = obj + self.__cache[ unicode( obj.revision_id() ).encode( "utf8" ) ] = copy( obj ) + + return obj + + @async + def reload( self, object_id, callback = None ): + """ + Load and immediately save the object corresponding to the given object id or database key. This + is useful when the object has a __setstate__() method that performs some sort of schema + evolution operation. + + @type object_id: unicode + @param object_id: id or key of the object to reload + @type callback: generator or NoneType + @param callback: generator to wakeup when the save is complete (optional) + """ + self.__reload( object_id ) + yield callback + + def __reload( self, object_id, revision = None ): + object_id = unicode( object_id ).encode( "utf8" ) + + # grab the object for the given id from the database + buffer = StringIO() + unpickler = cPickle.Unpickler( buffer ) + unpickler.persistent_load = self.__load + + pickled = self.__db.get( object_id ) + if pickled is None or pickled == "": + return + + buffer.write( pickled ) + buffer.flush() + buffer.seek( 0 ) + + # unpickle the object. this should trigger __setstate__() if the object has such a method + obj = unpickler.load() + if obj is None: + print "error unpickling %s: %s" % ( object_id, pickled ) + return + self.__cache[ object_id ] = obj + + # set the pickler up to save persistent ids for every object except for the obj passed in, which + # will be pickled normally + buffer = StringIO() + pickler = cPickle.Pickler( buffer, protocol = -1 ) + pickler.persistent_id = lambda o: self.__persistent_id( o, skip = obj ) + + # pickle the object and write it to the database under its id key + pickler.dump( obj ) + pickled = buffer.getvalue() + self.__db.put( object_id, pickled ) + + self.__db.sync() + + def size( self, object_id, revision = None ): + """ + Load the object corresponding to the given object id from the database, and return the size of + its pickled data in bytes. If a revision is provided, a specific revision of the object will be + loaded. + + @type object_id: unicode + @param object_id: id of the object whose size should be returned + @type revision: int or NoneType + @param revision: revision of the object to load (optional) + """ + if revision is not None: + object_id = Persistent.make_revision_id( object_id, revision ) + + object_id = unicode( object_id ).encode( "utf8" ) + + pickled = self.__db.get( object_id ) + if pickled is None or pickled == "": + return None + + return len( pickled ) + + @staticmethod + def generate_id(): + int_id = random.getrandbits( Old_database.ID_BITS ) + + base = len( Old_database.ID_DIGITS ) + digits = [] + + while True: + index = int_id % base + digits.insert( 0, Old_database.ID_DIGITS[ index ] ) + int_id = int_id / base + if int_id == 0: + break + + return "".join( digits ) + + @async + def next_id( self, callback ): + """ + Generate the next available object id, and yield the provided callback generator with the + object id as its argument. + + @type callback: generator + @param callback: generator to send the next available object id to + """ + # generate a random id, but on the off-chance that it collides with something else already in + # the database, try again + next_id = Old_database.generate_id() + while self.__db.get( next_id, default = None ) is not None: + next_id = Old_database.generate_id() + + # save the next_id as a key in the database so that it's not handed out again to another client + self.__db[ next_id ] = "" + + yield callback, next_id + + @async + def close( self ): + """ + Shutdown the database. + """ + self.__db.close() + self.__env.close() + yield None + + @async + def clear_cache( self ): + """ + Clear the memory object cache. + """ + self.__cache.clear() + yield None + + scheduler = property( lambda self: self.__scheduler ) + + +class Valid_id( object ): + """ + Validator for an object id. + """ + ID_PATTERN = re.compile( "^[%s]+$" % Old_database.ID_DIGITS ) + + def __init__( self, none_okay = False ): + self.__none_okay = none_okay + + def __call__( self, value ): + if self.__none_okay and value in ( None, "None", "" ): return None + if self.ID_PATTERN.search( value ): return str( value ) + + raise ValueError() + + +class Valid_revision( object ): + """ + Validator for an object id. + """ + REVISION_PATTERN = re.compile( "^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d+$" ) + + def __init__( self, none_okay = False ): + self.__none_okay = none_okay + + def __call__( self, value ): + if self.__none_okay and value in ( None, "None", "" ): return None + if self.REVISION_PATTERN.search( value ): return str( value ) + + raise ValueError() diff --git a/controller/Root.py b/controller/Root.py index 87b21f5..a3f0b5f 100644 --- a/controller/Root.py +++ b/controller/Root.py @@ -1,13 +1,11 @@ import cherrypy -from Scheduler import Scheduler from Expose import expose from Validate import validate -from Async import async from Notebooks import Notebooks from Users import Users -from Updater import update_client, wait_for_update from Database import Valid_id +from new_model.Note import Note from view.Main_page import Main_page from view.Json import Json from view.Error_page import Error_page @@ -18,12 +16,10 @@ class Root( object ): """ The root of the controller hierarchy, corresponding to the "/" URL. """ - def __init__( self, scheduler, database, settings ): + def __init__( self, database, settings ): """ Create a new Root object with the given settings. - @type scheduler: controller.Scheduler - @param scheduler: scheduler to use for asynchronous calls @type database: controller.Database @param database: database to use for all controllers @type settings: dict @@ -31,18 +27,16 @@ class Root( object ): @rtype: Root @return: newly constructed Root """ - self.__scheduler = scheduler self.__database = database self.__settings = settings self.__users = Users( - scheduler, database, settings[ u"global" ].get( u"luminotes.http_url", u"" ), settings[ u"global" ].get( u"luminotes.https_url", u"" ), settings[ u"global" ].get( u"luminotes.support_email", u"" ), settings[ u"global" ].get( u"luminotes.rate_plans", [] ), ) - self.__notebooks = Notebooks( scheduler, database, self.__users ) + self.__notebooks = Notebooks( database, self.__users ) @expose() def default( self, password_reset_id ): @@ -72,22 +66,19 @@ class Root( object ): return dict() + # TODO: move this method to controller.Notebooks, and maybe give it a more sensible name @expose( view = Json ) - @wait_for_update - @async - @update_client def next_id( self ): """ - Return the next available database object id. This id is guaranteed to be unique to the - database. + Return the next available database object id for a new note. This id is guaranteed to be unique + among all existing notes. @rtype: json dict @return: { 'next_id': nextid } """ - self.__database.next_id( self.__scheduler.thread ) - next_id = ( yield Scheduler.SLEEP ) + next_id = self.__database.next_id( Note ) - yield dict( + return dict( next_id = next_id, ) @@ -95,28 +86,20 @@ class Root( object ): """ CherryPy HTTP error handler, used to display page not found and generic error pages. """ + support_email = self.__settings[ u"global" ].get( u"luminotes.support_email" ) + if status == 404: cherrypy.response.headerMap[ u"Status" ] = u"404 Not Found" cherrypy.response.status = status - cherrypy.response.body = [ unicode( Not_found_page( self.__settings[ u"global" ].get( u"luminotes.support_email" ) ) ) ] + cherrypy.response.body = [ unicode( Not_found_page( support_email ) ) ] return - import sys + # TODO: it'd be nice to send an email to myself with the traceback import traceback traceback.print_exc() - exc_info = sys.exc_info() - if exc_info: - message = exc_info[ 1 ].message - else: - message = None + cherrypy.response.body = [ unicode( Error_page( support_email ) ) ] - cherrypy.response.body = [ unicode( Error_page( - self.__settings[ u"global" ].get( u"luminotes.support_email" ), - message, - ) ) ] - - scheduler = property( lambda self: self.__scheduler ) database = property( lambda self: self.__database ) notebooks = property( lambda self: self.__notebooks ) users = property( lambda self: self.__users ) diff --git a/controller/Updater.py b/controller/Updater.py deleted file mode 100644 index 2a6927c..0000000 --- a/controller/Updater.py +++ /dev/null @@ -1,72 +0,0 @@ -from Queue import Queue, Empty - - -TIMEOUT_SECONDS = 10.0 - - -def wait_for_update( function ): - """ - A decorator that passes a "queue" keyword arugment to its decorated function, calls the function, - and then blocks until an asynchronous response comes back via the Queue. When a response is - received, wait_for_update() returns it. - - For this decorator to be useful, you should use it to decorate a function that fires off some - asynchronous action and then returns immediately. A typical way to accomplish this is by using - the @async decorator after the @wait_for_update decorator. - """ - def get_message( *args, **kwargs ): - queue = Queue() - - kwargs[ "queue" ] = queue - function( *args, **kwargs ) - - # wait until a response is available in the queue, and then return that response - try: - return queue.get( block = True, timeout = TIMEOUT_SECONDS ) - except Empty: - return { "error": u"A timeout occurred when processing your request. Please try again or contact support." } - - return get_message - - -def update_client( function ): - """ - A decorator used to wrap a generator function so that its yielded values can be issued as - updates to the client. For this to work, the generator function must be invoked with a keyword - argument "queue" containing a Queue where the result can be put(). - - Also supports catching Validation_error exceptions and sending appropriate errors to the client. - - Note that this decorator itself is a generator function and works by passing along next()/send() - calls to its decorated generator. Only yielded values that are dictionaries are sent to the - client via the provided queue. All other types of yielded values are in turn yielded by this - decorator itself. - """ - def put_message( *args, **kwargs ): - # look in the called function's kwargs for the queue where results should be sent - queue = kwargs.pop( "queue" ) - - try: - generator = function( *args, **kwargs ) - message = None - - while True: - result = generator.send( message ) - - if isinstance( result, dict ): - queue.put( result ) - message = ( yield None ) - else: - message = ( yield result ) - except StopIteration: - return - except Exception, error: - # TODO: might be better to use view.Json instead of calling to_dict() manually - if hasattr( error, "to_dict" ): - result = error.to_dict() - queue.put( result ) - else: - queue.put( { "error": u"An error occurred when processing your request. Please try again or contact support." } ) - raise - - return put_message diff --git a/controller/Users.py b/controller/Users.py index 4654575..c440542 100644 --- a/controller/Users.py +++ b/controller/Users.py @@ -1,17 +1,15 @@ import re import cherrypy +from pytz import utc from datetime import datetime, timedelta -from model.User import User -from model.Notebook import Notebook -from model.Note import Note -from model.Password_reset import Password_reset -from Scheduler import Scheduler +from new_model.User import User +from new_model.Notebook import Notebook +from new_model.Note import Note +from new_model.Password_reset import Password_reset from Expose import expose from Validate import validate, Valid_string, Valid_bool, Validation_error from Database import Valid_id -from Updater import update_client, wait_for_update from Expire import strongly_expire -from Async import async from view.Json import Json from view.Main_page import Main_page from view.Redeem_reset_note import Redeem_reset_note @@ -123,12 +121,10 @@ class Users( object ): """ Controller for dealing with users, corresponding to the "/users" URL. """ - def __init__( self, scheduler, database, http_url, https_url, support_email, rate_plans ): + def __init__( self, database, http_url, https_url, support_email, rate_plans ): """ Create a new Users object. - @type scheduler: controller.Scheduler - @param scheduler: scheduler to use for asynchronous calls @type database: controller.Database @param database: database that users are stored in @type http_url: unicode @@ -142,7 +138,6 @@ class Users( object ): @rtype: Users @return: newly constructed Users """ - self.__scheduler = scheduler self.__database = database self.__http_url = http_url self.__https_url = https_url @@ -151,9 +146,6 @@ class Users( object ): @expose( view = Json ) @update_auth - @wait_for_update - @async - @update_client @validate( username = ( Valid_string( min = 1, max = 30 ), valid_username ), password = Valid_string( min = 1, max = 30 ), @@ -184,45 +176,39 @@ class Users( object ): if password != password_repeat: raise Signup_error( u"The passwords you entered do not match. Please try again." ) - self.__database.load( "User %s" % username, self.__scheduler.thread ) - user = ( yield Scheduler.SLEEP ) + user = self.__database.select_one( User, User.sql_load_by_username( username ) ) if user is not None: raise Signup_error( u"Sorry, that username is not available. Please try something else." ) # create a notebook for this user, along with a trash for that notebook - self.__database.next_id( self.__scheduler.thread ) - trash_id = ( yield Scheduler.SLEEP ) - trash = Notebook( trash_id, u"trash" ) + trash_id = self.__database.next_id( Notebook, commit = False ) + trash = Notebook.create( trash_id, u"trash" ) + self.__database.save( trash, commit = False ) - self.__database.next_id( self.__scheduler.thread ) - notebook_id = ( yield Scheduler.SLEEP ) - notebook = Notebook( notebook_id, u"my notebook", trash ) + notebook_id = self.__database.next_id( Notebook, commit = False ) + notebook = Notebook.create( notebook_id, u"my notebook", trash_id ) + self.__database.save( notebook, commit = False ) # create a startup note for this user's notebook - self.__database.next_id( self.__scheduler.thread ) - note_id = ( yield Scheduler.SLEEP ) - note = Note( note_id, file( u"static/html/welcome to your wiki.html" ).read() ) - notebook.add_note( note ) - notebook.add_startup_note( note ) + note_id = self.__database.next_id( Note, commit = False ) + note_contents = file( u"static/html/welcome to your wiki.html" ).read() + note = Note.create( note_id, note_contents, notebook_id, startup = True, rank = 0 ) + self.__database.save( note, commit = False ) # actually create the new user - self.__database.next_id( self.__scheduler.thread ) - user_id = ( yield Scheduler.SLEEP ) + user_id = self.__database.next_id( User, commit = False ) + user = User.create( user_id, username, password, email_address ) + self.__database.save( user, commit = False ) - user = User( user_id, username, password, email_address, notebooks = [ notebook ] ) - self.__database.save( user ) - - # add the new user to the user list - self.__database.load( u"User_list all", self.scheduler.thread ) - user_list = ( yield Scheduler.SLEEP ) - if user_list: - user_list.add_user( user ) - self.__database.save( user_list ) + # record the fact that the new user has access to their new notebook + self.__database.execute( user.sql_save_notebook( notebook_id, read_write = True ), commit = False ) + self.__database.execute( user.sql_save_notebook( trash_id, read_write = True ), commit = False ) + self.__database.commit() redirect = u"/notebooks/%s" % notebook.object_id - yield dict( + return dict( redirect = redirect, authenticated = user, ) @@ -230,9 +216,6 @@ class Users( object ): @expose() @grab_user_id @update_auth - @wait_for_update - @async - @update_client def demo( self, user_id = None ): """ Create a new guest User for purposes of the demo. Start that user with their own Notebook and @@ -250,54 +233,51 @@ class Users( object ): # if the user is already logged in as a guest, then just redirect to their existing demo # notebook if user_id: - self.__database.load( user_id, self.__scheduler.thread ) - user = ( yield Scheduler.SLEEP ) - if user.username is None and len( user.notebooks ) > 0: - redirect = u"/notebooks/%s" % user.notebooks[ 0 ].object_id - yield dict( redirect = redirect ) - return + user = self.__database.load( User, user_id ) + first_notebook = self.__database.select_one( Notebook, user.sql_load_notebooks( parents_only = True ) ) + if user.username is None and first_notebook: + redirect = u"/notebooks/%s" % first_notebook.object_id + return dict( redirect = redirect ) # create a demo notebook for this user, along with a trash for that notebook - self.__database.next_id( self.__scheduler.thread ) - trash_id = ( yield Scheduler.SLEEP ) - trash = Notebook( trash_id, u"trash" ) + trash_id = self.__database.next_id( Notebook, commit = False ) + trash = Notebook.create( trash_id, u"trash" ) + self.__database.save( trash, commit = False ) - self.__database.next_id( self.__scheduler.thread ) - notebook_id = ( yield Scheduler.SLEEP ) - notebook = Notebook( notebook_id, u"my notebook", trash ) + notebook_id = self.__database.next_id( Notebook, commit = False ) + notebook = Notebook.create( notebook_id, u"my notebook", trash_id ) + self.__database.save( notebook, commit = False ) # create startup notes for this user's notebook - self.__database.next_id( self.__scheduler.thread ) - note_id = ( yield Scheduler.SLEEP ) - note = Note( note_id, file( u"static/html/this is a demo.html" ).read() ) - notebook.add_note( note ) - notebook.add_startup_note( note ) + note_id = self.__database.next_id( Note, commit = False ) + note_contents = file( u"static/html/this is a demo.html" ).read() + note = Note.create( note_id, note_contents, notebook_id, startup = True, rank = 0 ) + self.__database.save( note, commit = False ) - self.__database.next_id( self.__scheduler.thread ) - note_id = ( yield Scheduler.SLEEP ) - note = Note( note_id, file( u"static/html/welcome to your wiki.html" ).read() ) - notebook.add_note( note ) - notebook.add_startup_note( note ) + note_id = self.__database.next_id( Note, commit = False ) + note_contents = file( u"static/html/welcome to your wiki.html" ).read() + note = Note.create( note_id, note_contents, notebook_id, startup = True, rank = 1 ) + self.__database.save( note, commit = False ) - # actually create the new user. because this is just a demo user, we're not adding it to the User_list - self.__database.next_id( self.__scheduler.thread ) - user_id = ( yield Scheduler.SLEEP ) + # actually create the new user + user_id = self.__database.next_id( User, commit = False ) + user = User.create( user_id, username = None, password = None, email_address = None ) + self.__database.save( user, commit = False ) - user = User( user_id, username = None, password = None, email_address = None, notebooks = [ notebook ] ) - self.__database.save( user ) + # record the fact that the new user has access to their new notebook + self.__database.execute( user.sql_save_notebook( notebook_id, read_write = True ), commit = False ) + self.__database.execute( user.sql_save_notebook( trash_id, read_write = True ), commit = False ) + self.__database.commit() redirect = u"/notebooks/%s" % notebook.object_id - yield dict( + return dict( redirect = redirect, authenticated = user, ) @expose( view = Json ) @update_auth - @wait_for_update - @async - @update_client @validate( username = ( Valid_string( min = 1, max = 30 ), valid_username ), password = Valid_string( min = 1, max = 30 ), @@ -317,28 +297,26 @@ class Users( object ): @raise Authentication_error: invalid username or password @raise Validation_error: one of the arguments is invalid """ - self.__database.load( "User %s" % username, self.__scheduler.thread ) - user = ( yield Scheduler.SLEEP ) + user = self.__database.select_one( User, User.sql_load_by_username( username ) ) if user is None or user.check_password( password ) is False: raise Authentication_error( u"Invalid username or password." ) + first_notebook = self.__database.select_one( Notebook, user.sql_load_notebooks( parents_only = True ) ) + # redirect to the user's first notebook (if any) - if len( user.notebooks ) > 0: - redirect = u"/notebooks/%s" % user.notebooks[ 0 ].object_id + if first_notebook: + redirect = u"/notebooks/%s" % first_notebook.object_id else: redirect = u"/" - yield dict( + return dict( redirect = redirect, authenticated = user, ) @expose( view = Json ) @update_auth - @wait_for_update - @async - @update_client def logout( self ): """ Deauthenticate the user and log them out of their current session. @@ -346,7 +324,7 @@ class Users( object ): @rtype: json dict @return: { 'redirect': url, 'deauthenticated': True } """ - yield dict( + return dict( redirect = self.__http_url + u"/", deauthenticated = True, ) @@ -354,9 +332,6 @@ class Users( object ): @expose( view = Json ) @strongly_expire @grab_user_id - @wait_for_update - @async - @update_client @validate( include_startup_notes = Valid_bool(), user_id = Valid_id( none_okay = True ), @@ -382,38 +357,42 @@ class Users( object ): @raise Validation_error: one of the arguments is invalid """ # if there's no logged-in user, default to the anonymous user - self.__database.load( user_id or u"User anonymous", self.__scheduler.thread ) - user = ( yield Scheduler.SLEEP ) + anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ) ) + if user_id: + user = self.__database.load( User, user_id ) + else: + user = anonymous - if not user: - yield dict( + if not user or not anonymous: + return dict( user = None, notebooks = None, http_url = u"", ) - return # in addition to this user's own notebooks, add to that list the anonymous user's notebooks - self.__database.load( u"User anonymous", self.__scheduler.thread ) - anonymous = ( yield Scheduler.SLEEP ) login_url = None + notebooks = self.__database.select_many( Notebook, anonymous.sql_load_notebooks() ) if user_id: - notebooks = anonymous.notebooks + notebooks += self.__database.select_many( Notebook, user.sql_load_notebooks() ) + # if the user is not logged in, return a login URL else: - notebooks = [] - if len( anonymous.notebooks ) > 0: - anon_notebook = anonymous.notebooks[ 0 ] - login_note = anon_notebook.lookup_note_by_title( u"login" ) + if len( notebooks ) > 0: + main_notebook = notebooks[ 0 ] + login_note = self.__database.select_one( Note, main_notebook.sql_load_note_by_title( u"login" ) ) if login_note: - login_url = "%s/notebooks/%s?note_id=%s" % ( self.__https_url, anon_notebook.object_id, login_note.object_id ) + login_url = "%s/notebooks/%s?note_id=%s" % ( self.__https_url, main_notebook.object_id, login_note.object_id ) - notebooks += user.notebooks + if include_startup_notes and len( notebooks ) > 0: + startup_notes = self.__database.select_many( Note, notebooks[ 0 ].sql_load_startup_notes() ) + else: + startup_notes = [] - yield dict( + return dict( user = user, notebooks = notebooks, - startup_notes = include_startup_notes and len( notebooks ) > 0 and notebooks[ 0 ].startup_notes or [], + startup_notes = startup_notes, http_url = self.__http_url, login_url = login_url, rate_plan = ( user.rate_plan < len( self.__rate_plans ) ) and self.__rate_plans[ user.rate_plan ] or {}, @@ -421,54 +400,64 @@ class Users( object ): def calculate_storage( self, user ): """ - Calculate total storage utilization for all notebooks and all notes of the given user, - including storage for all past revisions. + Calculate total storage utilization for all notes of the given user, including storage for all + past revisions. + @type user: User @param user: user for which to calculate storage utilization @rtype: int @return: total bytes used for storage """ - total_bytes = 0 + return sum( self.__database.select_one( tuple, user.sql_calculate_storage() ), 0 ) - def sum_revisions( obj ): - return \ - self.__database.size( obj.object_id ) + \ - sum( [ self.__database.size( obj.object_id, revision ) or 0 for revision in obj.revisions_list ], 0 ) - - def sum_notebook( notebook ): - return \ - self.__database.size( notebook.object_id ) + \ - sum( [ sum_revisions( note ) for note in notebook.notes ], 0 ) - - for notebook in user.notebooks: - total_bytes += sum_notebook( notebook ) - - if notebook.trash: - total_bytes += sum_notebook( notebook.trash ) - - return total_bytes - - @async - def update_storage( self, user_id, callback = None ): + def update_storage( self, user_id, commit = True ): """ Calculate and record total storage utilization for the given user. - @type user_id: unicode or NoneType + + @type user_id: unicode @param user_id: id of user for which to calculate storage utilization - @type callback: generator or NoneType - @param callback: generator to wakeup when the update is complete (optional) + @type commit: bool + @param commit: True to automatically commit after the update + @rtype: model.User + @return: object of the user corresponding to user_id """ - self.__database.load( user_id, self.__scheduler.thread ) - user = ( yield Scheduler.SLEEP ) + user = self.__database.load( User, user_id ) if user: user.storage_bytes = self.calculate_storage( user ) + self.__database.save( user, commit ) - yield callback, user + return user + + def check_access( self, user_id, notebook_id, read_write = False ): + """ + Determine whether the given user has access to the given notebook. + + @type user_id: unicode + @param user_id: id of user whose access to check + @type notebook_id: unicode + @param notebook_id: id of notebook to check access for + @type read_write: bool + @param read_write: True if read-write access is being checked, False if read-only access (defaults to False) + @rtype: bool + @return: True if the user has access + """ + # check if the anonymous user has access to this notebook + anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ) ) + + if self.__database.select_one( bool, anonymous.sql_has_access( notebook_id, read_write ) ): + return True + + if user_id: + # check if the given user has access to this notebook + user = self.__database.load( User, user_id ) + + if user and self.__database.select_one( bool, user.sql_has_access( notebook_id ) ): + return True + + return False @expose( view = Json ) - @wait_for_update - @async - @update_client @validate( email_address = ( Valid_string( min = 1, max = 60 ), valid_email_address ), send_reset_button = unicode, @@ -491,19 +480,13 @@ class Users( object ): from email import Message # check whether there are actually any users with the given email address - self.__database.load( u"User_list all", self.scheduler.thread ) - user_list = ( yield Scheduler.SLEEP ) + users = self.__database.select_many( User, User.sql_load_by_email_address( email_address ) ) - if not user_list: - raise Password_reset_error( "There was an error when sending your password reset email. Please contact %s." % self.__support_email ) - - users = [ user for user in user_list.users if user.email_address == email_address ] if len( users ) == 0: raise Password_reset_error( u"There are no Luminotes users with the email address %s" % email_address ) # record the sending of this reset email - self.__database.next_id( self.__scheduler.thread ) - password_reset_id = ( yield Scheduler.SLEEP ) + password_reset_id = self.__database.next_id( Password_reset, commit = False ) password_reset = Password_reset( password_reset_id, email_address ) self.__database.save( password_reset ) @@ -527,15 +510,12 @@ class Users( object ): server.sendmail( message[ u"from" ], [ email_address ], message.as_string() ) server.quit() - yield dict( + return dict( message = u"Please check your inbox. A password reset email has been sent to %s" % email_address, ) @expose( view = Main_page ) @strongly_expire - @wait_for_update - @async - @update_client @validate( password_reset_id = Valid_id(), ) @@ -550,43 +530,34 @@ class Users( object ): @raise Password_reset_error: an error occured when redeeming the password reset, such as an expired link @raise Validation_error: one of the arguments is invalid """ - self.__database.load( u"User anonymous", self.__scheduler.thread ) - anonymous = ( yield Scheduler.SLEEP ) + anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ) ) + if anonymous: + main_notebook = self.__database.select_one( Notebook, anonymous.sql_load_notebooks() ) - if not anonymous or len( anonymous.notebooks ) == 0: + if not anonymous or not main_notebook: raise Password_reset_error( "There was an error when completing your password reset. Please contact %s." % self.__support_email ) - self.__database.load( password_reset_id, self.__scheduler.thread ) - password_reset = ( yield Scheduler.SLEEP ) + password_reset = self.__database.load( Password_reset, password_reset_id ) - if not password_reset or datetime.now() - password_reset.revision > timedelta( hours = 25 ): + if not password_reset or datetime.now( tz = utc ) - password_reset.revision > timedelta( hours = 25 ): raise Password_reset_error( "Your password reset link has expired. Please request a new password reset email." ) if password_reset.redeemed: raise Password_reset_error( "Your password has already been reset. Please request a new password reset email." ) - self.__database.load( u"User_list all", self.__scheduler.thread ) - user_list = ( yield Scheduler.SLEEP ) - - if not user_list: - raise Password_reset_error( u"There are no Luminotes users with the email address %s" % password_reset.email_address ) - # find the user(s) with the email address from the password reset request - matching_users = [ user for user in user_list.users if user.email_address == password_reset.email_address ] + matching_users = self.__database.select_many( User, User.sql_load_by_email_address( password_reset.email_address ) ) if len( matching_users ) == 0: raise Password_reset_error( u"There are no Luminotes users with the email address %s" % password_reset.email_address ) - yield dict( - notebook_id = anonymous.notebooks[ 0 ].object_id, + return dict( + notebook_id = main_notebook.object_id, note_id = u"blank", note_contents = unicode( Redeem_reset_note( password_reset_id, matching_users ) ), ) @expose( view = Json ) - @wait_for_update - @async - @update_client def reset_password( self, password_reset_id, reset_button, **new_passwords ): """ Reset all the users with the provided passwords. @@ -606,27 +577,19 @@ class Users( object ): except ValueError: raise Validation_error( "password_reset_id", password_reset_id, id_validator, "is not a valid id" ) - self.__database.load( password_reset_id, self.__scheduler.thread ) - password_reset = ( yield Scheduler.SLEEP ) + password_reset = self.__database.load( Password_reset, password_reset_id ) - if not password_reset or datetime.now() - password_reset.revision > timedelta( hours = 25 ): + if not password_reset or datetime.now( tz = utc ) - password_reset.revision > timedelta( hours = 25 ): raise Password_reset_error( "Your password reset link has expired. Please request a new password reset email." ) if password_reset.redeemed: raise Password_reset_error( "Your password has already been reset. Please request a new password reset email." ) - self.__database.load( u"User_list all", self.__scheduler.thread ) - user_list = ( yield Scheduler.SLEEP ) - - if not user_list: - raise Password_reset_error( "There was an error when resetting your password. Please contact %s." % self.__support_email ) - - # find the user(s) with the email address from the password reset request - matching_users = [ user for user in user_list.users if user.email_address == password_reset.email_address ] + matching_users = self.__database.select_many( User, User.sql_load_by_email_address( password_reset.email_address ) ) allowed_user_ids = [ user.object_id for user in matching_users ] # reset any passwords that are non-blank - users_to_reset = [] + at_least_one_reset = False for ( user_id, ( new_password, new_password_repeat ) ) in new_passwords.items(): if user_id not in allowed_user_ids: raise Password_reset_error( "There was an error when resetting your password. Please contact %s." % self.__support_email ) @@ -635,8 +598,7 @@ class Users( object ): if new_password == u"" and new_password_repeat == u"": continue - self.__database.load( user_id, self.__scheduler.thread ) - user = ( yield Scheduler.SLEEP ) + user = self.__database.load( User, user_id ) if not user: raise Password_reset_error( "There was an error when resetting your password. Please contact %s." % self.__support_email ) @@ -649,19 +611,16 @@ class Users( object ): if len( new_password ) > 30: raise Password_reset_error( u"Your password can be no longer than 30 characters." ) - users_to_reset.append( ( user, new_password ) ) - - for ( user, new_password ) in users_to_reset: + at_least_one_reset = True user.password = new_password - self.__database.save( user ) + self.__database.save( user, commit = False ) # if all the new passwords provided are blank, bail - if not users_to_reset: + if not at_least_one_reset: raise Password_reset_error( u"Please enter a new password. Or, if you already know your password, just click the login link above." ) password_reset.redeemed = True - self.__database.save( password_reset ) + self.__database.save( password_reset, commit = False ) + self.__database.commit() - yield dict( redirect = u"/" ) - - scheduler = property( lambda self: self.__scheduler ) + return dict( redirect = u"/" ) diff --git a/controller/Validate.py b/controller/Validate.py index 131eb62..5f0963b 100644 --- a/controller/Validate.py +++ b/controller/Validate.py @@ -32,7 +32,7 @@ class Validation_error( Exception ): def to_dict( self ): return dict( - error = u"The %s %s." % ( self.__name, self.__message ), + error = u"The %s %s." % ( self.__name.replace( u"_", " " ), self.__message ), name = self.__name, value = self.__value, ) diff --git a/controller/test/Stub_database.py b/controller/test/Stub_database.py new file mode 100644 index 0000000..52b0d30 --- /dev/null +++ b/controller/test/Stub_database.py @@ -0,0 +1,72 @@ +from copy import copy + + +class Stub_database( object ): + def __init__( self, connection = None ): + # map of object id to list of saved objects (presumably in increasing order of revisions) + self.objects = {} + self.user_notebook = {} # map of user_id to ( notebook_id, read_write ) + self.__next_id = 0 + + def save( self, obj, commit = False ): + if obj.object_id in self.objects: + self.objects[ obj.object_id ].append( copy( obj ) ) + else: + self.objects[ obj.object_id ] = [ copy( obj ) ] + + def load( self, Object_type, object_id, revision = None ): + obj_list = self.objects.get( object_id ) + + if not obj_list: + return None + + # if a particular revision wasn't requested, just return the most recently saved object + # matching the given object_id + if revision is None: + if not isinstance( obj_list[ -1 ], Object_type ): + return None + return copy( obj_list[ -1 ] ) + + # a particular revision was requested, so pick it out of the objects matching the given id + matching_objs = [ obj for obj in obj_list if str( obj.revision ) == str( revision ) ] + if len( matching_objs ) > 0: + if not isinstance( matching_objs[ -1 ], Object_type ): + return None + return copy( matching_objs[ -1 ] ) + + return None + + def select_one( self, Object_type, sql_command ): + if callable( sql_command ): + result = sql_command( self ) + if isinstance( result, list ): + if len( result ) == 0: return None + return result[ 0 ] + return result + + raise NotImplementedError( sql_command ) + + def select_many( self, Object_type, sql_command ): + if callable( sql_command ): + result = sql_command( self ) + if isinstance( result, list ): + return result + return [ result ] + + raise NotImplementedError( sql_command ) + + def execute( self, sql_command, commit = False ): + if callable( sql_command ): + return sql_command( self ) + + raise NotImplementedError( sql_command ) + + def next_id( self, Object_type, commit = True ): + self.__next_id += 1 + return unicode( self.__next_id ) + + def commit( self ): + pass + + def close( self ): + pass diff --git a/controller/test/Stub_object.py b/controller/test/Stub_object.py new file mode 100644 index 0000000..6daef90 --- /dev/null +++ b/controller/test/Stub_object.py @@ -0,0 +1,79 @@ +from datetime import datetime +from new_model.Persistent import Persistent, quote + + +def notz_quote( value ): + """ + Apparently, pysqlite2 chokes on timestamps that have a timezone when reading them out of the + database, so for purposes of the unit tests, strip off the timezone on all datetime objects. + """ + if isinstance( value, datetime ): + value = value.replace( tzinfo = None ) + + return quote( value ) + + +class Stub_object( Persistent ): + def __init__( self, object_id, revision = None, value = None, value2 = None ): + Persistent.__init__( self, object_id, revision ) + self.__value = value + self.__value2 = value2 + + @staticmethod + def sql_load( object_id, revision = None ): + if revision: + return "select * from stub_object where id = %s and revision = %s;" % ( quote( object_id ), notz_quote( revision ) ) + + return "select * from stub_object where id = %s order by revision desc limit 1;" % quote( object_id ) + + @staticmethod + def sql_id_exists( object_id, revision = None ): + if revision: + return "select id from stub_object where id = %s and revision = %s;" % ( quote( object_id ), notz_quote( revision ) ) + + return "select id from stub_object where id = %s order by revision desc limit 1;" % quote( object_id ) + + def sql_exists( self ): + return Stub_object.sql_id_exists( self.object_id, self.revision ) + + def sql_create( self ): + return \ + "insert into stub_object ( id, revision, value, value2 ) " + \ + "values ( %s, %s, %s, %s );" % \ + ( quote( self.object_id ), notz_quote( self.revision ), quote( self.__value ), + quote( self.__value2 ) ) + + def sql_update( self ): + return self.sql_create() + + @staticmethod + def sql_load_em_all(): + return "select * from stub_object;" + + @staticmethod + def sql_create_table(): + return \ + """ + create table stub_object ( + id text not null, + revision timestamp with time zone not null, + value integer, + value2 integer + ); + """ + + @staticmethod + def sql_tuple(): + return "select 1, 2;" + + def __set_value( self, value ): + self.update_revision() + self.__value = value + + def __set_value2( self, value2 ): + self.update_revision() + self.__value2 = value2 + + value = property( lambda self: self.__value, __set_value ) + value2 = property( lambda self: self.__value2, __set_value2 ) + diff --git a/controller/test/Test_controller.py b/controller/test/Test_controller.py index 8ad62a6..a1b3ed2 100644 --- a/controller/test/Test_controller.py +++ b/controller/test/Test_controller.py @@ -1,18 +1,178 @@ import cherrypy -from controller.Scheduler import Scheduler -from controller.Database import Database -from controller.test.Stub_view import Stub_view +from Stub_database import Stub_database +from Stub_view import Stub_view from config import Common from datetime import datetime from StringIO import StringIO class Test_controller( object ): + def __init__( self ): + from new_model.User import User + from new_model.Notebook import Notebook + from new_model.Note import Note + + # Since Stub_database isn't a real database and doesn't know SQL, replace some of the + # SQL-returning methods in User, Note, and Notebook to return functions that manipulate data in + # Stub_database directly instead. This is all a little fragile, but it's better than relying on + # the presence of a real database for unit tests. + def sql_save_notebook( self, notebook_id, read_write, database ): + if self.object_id in database.user_notebook: + database.user_notebook[ self.object_id ].append( ( notebook_id, read_write ) ) + else: + database.user_notebook[ self.object_id ] = [ ( notebook_id, read_write ) ] + + User.sql_save_notebook = lambda self, notebook_id, read_write = False: \ + lambda database: sql_save_notebook( self, notebook_id, read_write, database ) + + def sql_load_notebooks( self, parents_only, database ): + notebooks = [] + notebook_tuples = database.user_notebook.get( self.object_id ) + + if not notebook_tuples: return None + + for notebook_tuple in notebook_tuples: + ( notebook_id, read_write ) = notebook_tuple + notebook = database.objects.get( notebook_id )[ -1 ] + notebook._Notebook__read_write = read_write + if parents_only and notebook.trash_id is None: + continue + notebooks.append( notebook ) + + return notebooks + + User.sql_load_notebooks = lambda self, parents_only = False: \ + lambda database: sql_load_notebooks( self, parents_only, database ) + + def sql_load_by_username( username, database ): + users = [] + + for ( object_id, obj_list ) in database.objects.items(): + obj = obj_list[ -1 ] + if isinstance( obj, User ) and obj.username == username: + users.append( obj ) + + return users + + User.sql_load_by_username = staticmethod( lambda username: \ + lambda database: sql_load_by_username( username, database ) ) + + def sql_load_by_email_address( email_address, database ): + users = [] + + for ( object_id, obj_list ) in database.objects.items(): + obj = obj_list[ -1 ] + if isinstance( obj, User ) and obj.email_address == email_address: + users.append( obj ) + + return users + + User.sql_load_by_email_address = staticmethod( lambda email_address: \ + lambda database: sql_load_by_email_address( email_address, database ) ) + + def sql_calculate_storage( self, database ): + return ( 17, 3, 4, 22 ) # rather than actually calculating anything, return arbitrary numbers + + User.sql_calculate_storage = lambda self: \ + lambda database: sql_calculate_storage( self, database ) + + def sql_has_access( self, notebook_id, read_write, database ): + for ( user_id, notebook_tuples ) in database.user_notebook.items(): + for notebook_tuple in notebook_tuples: + ( db_notebook_id, db_read_write ) = notebook_tuple + + if self.object_id == user_id and notebook_id == db_notebook_id: + if read_write is True and db_read_write is False: + return False + return True + + return False + + User.sql_has_access = lambda self, notebook_id, read_write = False: \ + lambda database: sql_has_access( self, notebook_id, read_write, database ) + + def sql_load_revisions( self, database ): + note_list = database.objects.get( self.object_id ) + if not note_list: return None + + revisions = [ note.revision for note in note_list ] + return revisions + + Note.sql_load_revisions = lambda self: \ + lambda database: sql_load_revisions( self, database ) + + def sql_load_notes( self, database ): + notes = [] + + for ( object_id, obj_list ) in database.objects.items(): + obj = obj_list[ -1 ] + if isinstance( obj, Note ) and obj.notebook_id == self.object_id: + notes.append( obj ) + + notes.sort( lambda a, b: -cmp( a.revision, b.revision ) ) + return notes + + Notebook.sql_load_notes = lambda self: \ + lambda database: sql_load_notes( self, database ) + + def sql_load_startup_notes( self, database ): + notes = [] + + for ( object_id, obj_list ) in database.objects.items(): + obj = obj_list[ -1 ] + if isinstance( obj, Note ) and obj.notebook_id == self.object_id and obj.startup: + notes.append( obj ) + + return notes + + Notebook.sql_load_startup_notes = lambda self: \ + lambda database: sql_load_startup_notes( self, database ) + + def sql_load_note_by_title( self, title, database ): + notes = [] + + for ( object_id, obj_list ) in database.objects.items(): + obj = obj_list[ -1 ] + if isinstance( obj, Note ) and obj.notebook_id == self.object_id and obj.title == title: + notes.append( obj ) + + return notes + + Notebook.sql_load_note_by_title = lambda self, title: \ + lambda database: sql_load_note_by_title( self, title, database ) + + def sql_search_notes( self, search_text, database ): + notes = [] + search_text = search_text.lower() + + for ( object_id, obj_list ) in database.objects.items(): + obj = obj_list[ -1 ] + if isinstance( obj, Note ) and obj.notebook_id == self.object_id and \ + search_text in obj.contents.lower(): + notes.append( obj ) + + return notes + + Notebook.sql_search_notes = lambda self, search_text: \ + lambda database: sql_search_notes( self, search_text, database ) + + def sql_highest_rank( self, database ): + max_rank = -1 + + for ( object_id, obj_list ) in database.objects.items(): + obj = obj_list[ -1 ] + if isinstance( obj, Note ) and obj.notebook_id == self.object_id and obj.rank > max_rank: + max_rank = obj.rank + + return max_rank + + Notebook.sql_highest_rank = lambda self: \ + lambda database: sql_highest_rank( self, database ) + def setUp( self ): from controller.Root import Root cherrypy.lowercase_api = True - self.scheduler = Scheduler() - self.database = Database( self.scheduler, database_path = None ) + self.database = Stub_database() self.settings = { u"global": { u"luminotes.http_url" : u"http://luminotes.com", @@ -33,7 +193,7 @@ class Test_controller( object ): }, } - cherrypy.root = Root( self.scheduler, self.database, self.settings ) + cherrypy.root = Root( self.database, self.settings ) cherrypy.config.update( Common.settings ) cherrypy.config.update( { u"server.log_to_screen": False } ) cherrypy.server.start( init_only = True, server_class = None ) @@ -45,7 +205,6 @@ class Test_controller( object ): def tearDown( self ): cherrypy.server.stop() - self.scheduler.shutdown() def http_get( self, http_path, headers = None, session_id = None, pretend_https = False ): """ @@ -64,7 +223,7 @@ class Test_controller( object ): proxy_ip = self.settings[ "global" ].get( u"luminotes.http_proxy_ip" ) request = cherrypy.server.request( ( proxy_ip, 1234 ), u"127.0.0.5" ) - response = request.run( "GET %s HTTP/1.0" % http_path, headers = headers, rfile = StringIO() ) + response = request.run( "GET %s HTTP/1.0" % str( http_path ), headers = headers, rfile = StringIO() ) session_id = response.simple_cookie.get( u"session_id" ) if session_id: session_id = session_id.value @@ -103,7 +262,7 @@ class Test_controller( object ): headers.append( ( u"Cookie", "session_id=%s" % session_id ) ) # will break if unicode is used for the value request = cherrypy.server.request( ( u"127.0.0.1", 1234 ), u"127.0.0.5" ) - response = request.run( "POST %s HTTP/1.0" % http_path, headers = headers, rfile = StringIO( post_data ) ) + response = request.run( "POST %s HTTP/1.0" % str( http_path ), headers = headers, rfile = StringIO( post_data ) ) session_id = response.simple_cookie.get( u"session_id" ) if session_id: session_id = session_id.value diff --git a/controller/test/Test_database.py b/controller/test/Test_database.py index 1cb7e28..8cf085e 100644 --- a/controller/test/Test_database.py +++ b/controller/test/Test_database.py @@ -1,323 +1,188 @@ +from pytz import utc +from pysqlite2 import dbapi2 as sqlite +from datetime import datetime +from Stub_object import Stub_object from controller.Database import Database -from controller.Scheduler import Scheduler -from model.Persistent import Persistent - - -class Some_object( Persistent ): - def __init__( self, object_id, value, value2 = None, secondary_id = None ): - Persistent.__init__( self, object_id, secondary_id ) - self.__value = value - self.__value2 = value2 - - def __set_value( self, value ): - self.update_revision() - self.__value = value - - def __set_value2( self, value2 ): - self.update_revision() - self.__value2 = value2 - - value = property( lambda self: self.__value, __set_value ) - value2 = property( lambda self: self.__value2, __set_value2 ) class Test_database( object ): - def __init__( self, clear_cache = True ): - self.clear_cache = clear_cache - def setUp( self ): - self.scheduler = Scheduler() - self.database = Database( self.scheduler ) - next_id = None + # make an in-memory sqlite database to use in place of PostgreSQL during testing + self.connection = sqlite.connect( ":memory:", detect_types = sqlite.PARSE_DECLTYPES | sqlite.PARSE_COLNAMES ) + cursor = self.connection.cursor() + cursor.execute( Stub_object.sql_create_table() ) + + self.database = Database( self.connection ) def tearDown( self ): self.database.close() - self.scheduler.shutdown() def test_save_and_load( self ): - def gen(): - basic_obj = Some_object( object_id = "5", value = 1 ) - original_revision = basic_obj.revision + basic_obj = Stub_object( object_id = "5", value = 1 ) + original_revision = basic_obj.revision - self.database.save( basic_obj, self.scheduler.thread ) - yield Scheduler.SLEEP - if self.clear_cache: self.database.clear_cache() - self.database.load( basic_obj.object_id, self.scheduler.thread ) - obj = ( yield Scheduler.SLEEP ) + self.database.save( basic_obj ) + obj = self.database.load( Stub_object, basic_obj.object_id ) - assert obj.object_id == basic_obj.object_id - assert obj.revision == original_revision - assert obj.revisions_list == [ original_revision ] - assert obj.value == basic_obj.value + assert obj.object_id == basic_obj.object_id + assert obj.revision.replace( tzinfo = utc ) == original_revision + assert obj.value == basic_obj.value - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) + def test_save_and_load_without_commit( self ): + basic_obj = Stub_object( object_id = "5", value = 1 ) + original_revision = basic_obj.revision - def test_complex_save_and_load( self ): - def gen(): - basic_obj = Some_object( object_id = "7", value = 2 ) - basic_original_revision = basic_obj.revision - complex_obj = Some_object( object_id = "6", value = basic_obj ) - complex_original_revision = complex_obj.revision + self.database.save( basic_obj, commit = False ) + self.connection.rollback() # if commit wasn't called, this should back out the save + obj = self.database.load( Stub_object, basic_obj.object_id ) - self.database.save( complex_obj, self.scheduler.thread ) - yield Scheduler.SLEEP - if self.clear_cache: self.database.clear_cache() - self.database.load( complex_obj.object_id, self.scheduler.thread ) - obj = ( yield Scheduler.SLEEP ) - if self.clear_cache: self.database.clear_cache() + assert obj == None - assert obj.object_id == complex_obj.object_id - assert obj.revision == complex_original_revision - assert obj.revisions_list == [ complex_original_revision ] - assert obj.value.object_id == basic_obj.object_id - assert obj.value.value == basic_obj.value - assert obj.value.revision == basic_original_revision - assert obj.value.revisions_list == [ basic_original_revision ] + def test_save_and_load_with_explicit_commit( self ): + basic_obj = Stub_object( object_id = "5", value = 1 ) + original_revision = basic_obj.revision - self.database.load( basic_obj.object_id, self.scheduler.thread ) - obj = ( yield Scheduler.SLEEP ) + self.database.save( basic_obj, commit = False ) + self.database.commit() + self.connection.rollback() # should have no effect because of the call to commit + obj = self.database.load( Stub_object, basic_obj.object_id ) - assert obj.object_id == basic_obj.object_id - assert obj.value == basic_obj.value - assert obj.revision == basic_original_revision - assert obj.revisions_list == [ basic_original_revision ] + assert obj.object_id == basic_obj.object_id + assert obj.revision.replace( tzinfo = utc ) == original_revision + assert obj.value == basic_obj.value - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) + def test_select_one( self ): + basic_obj = Stub_object( object_id = "5", value = 1 ) + original_revision = basic_obj.revision - def test_save_and_load_by_secondary( self ): - def gen(): - basic_obj = Some_object( object_id = "5", value = 1, secondary_id = u"foo" ) - original_revision = basic_obj.revision + self.database.save( basic_obj ) + obj = self.database.select_one( Stub_object, Stub_object.sql_load( basic_obj.object_id ) ) - self.database.save( basic_obj, self.scheduler.thread ) - yield Scheduler.SLEEP - if self.clear_cache: self.database.clear_cache() - self.database.load( u"Some_object foo", self.scheduler.thread ) - obj = ( yield Scheduler.SLEEP ) + assert obj.object_id == basic_obj.object_id + assert obj.revision.replace( tzinfo = utc ) == original_revision + assert obj.value == basic_obj.value - assert obj.object_id == basic_obj.object_id - assert obj.value == basic_obj.value - assert obj.revision == original_revision - assert obj.revisions_list == [ original_revision ] + def test_select_one_tuple( self ): + obj = self.database.select_one( tuple, Stub_object.sql_tuple() ) - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) + assert len( obj ) == 2 + assert obj[ 0 ] == 1 + assert obj[ 1 ] == 2 - def test_duplicate_save_and_load( self ): - def gen(): - basic_obj = Some_object( object_id = "9", value = 3 ) - basic_original_revision = basic_obj.revision - complex_obj = Some_object( object_id = "8", value = basic_obj, value2 = basic_obj ) - complex_original_revision = complex_obj.revision + def test_select_many( self ): + basic_obj = Stub_object( object_id = "5", value = 1 ) + original_revision = basic_obj.revision + basic_obj2 = Stub_object( object_id = "6", value = 2 ) + original_revision2 = basic_obj2.revision - self.database.save( complex_obj, self.scheduler.thread ) - yield Scheduler.SLEEP - if self.clear_cache: self.database.clear_cache() - self.database.load( complex_obj.object_id, self.scheduler.thread ) - obj = ( yield Scheduler.SLEEP ) - if self.clear_cache: self.database.clear_cache() + self.database.save( basic_obj ) + self.database.save( basic_obj2 ) + objs = self.database.select_many( Stub_object, Stub_object.sql_load_em_all() ) - assert obj.object_id == complex_obj.object_id - assert obj.revision == complex_original_revision - assert obj.revisions_list == [ complex_original_revision ] + assert len( objs ) == 2 + assert objs[ 0 ].object_id == basic_obj.object_id + assert objs[ 0 ].revision.replace( tzinfo = utc ) == original_revision + assert objs[ 0 ].value == basic_obj.value + assert objs[ 1 ].object_id == basic_obj2.object_id + assert objs[ 1 ].revision.replace( tzinfo = utc ) == original_revision2 + assert objs[ 1 ].value == basic_obj2.value - assert obj.value.object_id == basic_obj.object_id - assert obj.value.value == basic_obj.value - assert obj.value.revision == basic_original_revision - assert obj.value.revisions_list == [ basic_original_revision ] + def test_select_many_tuples( self ): + objs = self.database.select_many( tuple, Stub_object.sql_tuple() ) - assert obj.value2.object_id == basic_obj.object_id - assert obj.value2.value == basic_obj.value - assert obj.value2.revision == basic_original_revision - assert obj.value2.revisions_list == [ basic_original_revision ] + assert len( objs ) == 1 + assert len( objs[ 0 ] ) == 2 + assert objs[ 0 ][ 0 ] == 1 + assert objs[ 0 ][ 1 ] == 2 - assert obj.value == obj.value2 + def test_select_many_with_no_matches( self ): + objs = self.database.select_many( Stub_object, Stub_object.sql_load_em_all() ) - self.database.load( basic_obj.object_id, self.scheduler.thread ) - obj = ( yield Scheduler.SLEEP ) - - assert obj.object_id == basic_obj.object_id - assert obj.value == basic_obj.value - assert obj.revision == basic_original_revision - assert obj.revisions_list == [ basic_original_revision ] - - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) + assert len( objs ) == 0 def test_save_and_load_revision( self ): - def gen(): - basic_obj = Some_object( object_id = "5", value = 1 ) - original_revision = basic_obj.revision + basic_obj = Stub_object( object_id = "5", value = 1 ) + original_revision = basic_obj.revision - self.database.save( basic_obj, self.scheduler.thread ) - yield Scheduler.SLEEP - if self.clear_cache: self.database.clear_cache() + self.database.save( basic_obj ) + basic_obj.value = 2 - basic_obj.value = 2 + self.database.save( basic_obj ) + obj = self.database.load( Stub_object, basic_obj.object_id ) - self.database.save( basic_obj, self.scheduler.thread ) - yield Scheduler.SLEEP - if self.clear_cache: self.database.clear_cache() - self.database.load( basic_obj.object_id, self.scheduler.thread ) - obj = ( yield Scheduler.SLEEP ) - if self.clear_cache: self.database.clear_cache() + assert obj.object_id == basic_obj.object_id + assert obj.revision.replace( tzinfo = utc ) == basic_obj.revision + assert obj.value == basic_obj.value - assert obj.object_id == basic_obj.object_id - assert obj.revision == basic_obj.revision - assert obj.revisions_list == [ original_revision, basic_obj.revision ] - assert obj.value == basic_obj.value + revised = self.database.load( Stub_object, basic_obj.object_id, revision = original_revision ) - self.database.load( basic_obj.object_id, self.scheduler.thread, revision = original_revision ) - revised = ( yield Scheduler.SLEEP ) + assert revised.object_id == basic_obj.object_id + assert revised.value == 1 + assert revised.revision.replace( tzinfo = utc ) == original_revision - assert revised.object_id == basic_obj.object_id - assert revised.value == 1 - assert revised.revision == original_revision - assert id( obj.revisions_list ) != id( revised.revisions_list ) - assert revised.revisions_list == [ original_revision ] + def test_execute( self ): + basic_obj = Stub_object( object_id = "5", value = 1 ) + original_revision = basic_obj.revision - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) + self.database.execute( basic_obj.sql_create() ) + obj = self.database.load( Stub_object, basic_obj.object_id ) + + assert obj.object_id == basic_obj.object_id + assert obj.revision.replace( tzinfo = utc ) == original_revision + assert obj.value == basic_obj.value + + def test_execute_without_commit( self ): + basic_obj = Stub_object( object_id = "5", value = 1 ) + original_revision = basic_obj.revision + + self.database.execute( basic_obj.sql_create(), commit = False ) + self.connection.rollback() + obj = self.database.load( Stub_object, basic_obj.object_id ) + + assert obj == None + + def test_execute_with_explicit_commit( self ): + basic_obj = Stub_object( object_id = "5", value = 1 ) + original_revision = basic_obj.revision + + self.database.execute( basic_obj.sql_create(), commit = False ) + self.database.commit() + obj = self.database.load( Stub_object, basic_obj.object_id ) + + assert obj.object_id == basic_obj.object_id + assert obj.revision.replace( tzinfo = utc ) == original_revision + assert obj.value == basic_obj.value def test_load_unknown( self ): - def gen(): - basic_obj = Some_object( object_id = "5", value = 1 ) - self.database.load( basic_obj.object_id, self.scheduler.thread ) - obj = ( yield Scheduler.SLEEP ) - - assert obj == None - - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) - - def test_reload( self ): - def gen(): - basic_obj = Some_object( object_id = "5", value = 1 ) - original_revision = basic_obj.revision - - self.database.save( basic_obj, self.scheduler.thread ) - yield Scheduler.SLEEP - if self.clear_cache: self.database.clear_cache() - - def setstate( self, state ): - state[ "_Some_object__value" ] = 55 - self.__dict__.update( state ) - - Some_object.__setstate__ = setstate - - self.database.reload( basic_obj.object_id, self.scheduler.thread ) - yield Scheduler.SLEEP - delattr( Some_object, "__setstate__" ) - if self.clear_cache: self.database.clear_cache() - - self.database.load( basic_obj.object_id, self.scheduler.thread ) - obj = ( yield Scheduler.SLEEP ) - - assert obj.object_id == basic_obj.object_id - assert obj.value == 55 - assert obj.revision == original_revision - assert obj.revisions_list == [ original_revision ] - - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) - - def test_reload_revision( self ): - def gen(): - basic_obj = Some_object( object_id = "5", value = 1 ) - original_revision = basic_obj.revision - original_revision_id = basic_obj.revision_id() - - self.database.save( basic_obj, self.scheduler.thread ) - yield Scheduler.SLEEP - if self.clear_cache: self.database.clear_cache() - - basic_obj.value = 2 - - self.database.save( basic_obj, self.scheduler.thread ) - yield Scheduler.SLEEP - if self.clear_cache: self.database.clear_cache() - - def setstate( self, state ): - state[ "_Some_object__value" ] = 55 - self.__dict__.update( state ) - - Some_object.__setstate__ = setstate - - self.database.reload( original_revision_id, self.scheduler.thread ) - yield Scheduler.SLEEP - delattr( Some_object, "__setstate__" ) - if self.clear_cache: self.database.clear_cache() - - self.database.load( basic_obj.object_id, self.scheduler.thread, revision = original_revision ) - obj = ( yield Scheduler.SLEEP ) - - assert obj.object_id == basic_obj.object_id - assert obj.revision == original_revision - assert obj.revisions_list == [ original_revision ] - assert obj.value == 55 - - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) - - def test_size( self ): - def gen(): - basic_obj = Some_object( object_id = "5", value = 1 ) - original_revision = basic_obj.revision - - self.database.save( basic_obj, self.scheduler.thread ) - yield Scheduler.SLEEP - if self.clear_cache: self.database.clear_cache() - - size = self.database.size( basic_obj.object_id ) - - from cPickle import Pickler - from StringIO import StringIO - buffer = StringIO() - pickler = Pickler( buffer, protocol = -1 ) - pickler.dump( basic_obj ) - expected_size = len( buffer.getvalue() ) - - # as long as the size is close to the expected size, that's fine - assert abs( size - expected_size ) < 10 - - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) + basic_obj = Stub_object( object_id = "5", value = 1 ) + obj = self.database.load( Stub_object, basic_obj.object_id ) + assert obj == None def test_next_id( self ): - def gen(): - self.database.next_id( self.scheduler.thread ) - next_id = ( yield Scheduler.SLEEP ) - assert next_id - prev_ids = [ next_id ] + next_id = self.database.next_id( Stub_object ) + assert next_id + assert self.database.load( Stub_object, next_id ) + prev_ids = [ next_id ] - self.database.next_id( self.scheduler.thread ) - next_id = ( yield Scheduler.SLEEP ) - assert next_id - assert next_id not in prev_ids - prev_ids.append( next_id ) + next_id = self.database.next_id( Stub_object ) + assert next_id + assert next_id not in prev_ids + assert self.database.load( Stub_object, next_id ) + prev_ids.append( next_id ) - self.database.next_id( self.scheduler.thread ) - next_id = ( yield Scheduler.SLEEP ) - assert next_id - assert next_id not in prev_ids + next_id = self.database.next_id( Stub_object ) + assert next_id + assert next_id not in prev_ids + assert self.database.load( Stub_object, next_id ) - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) + def test_next_id_without_commit( self ): + next_id = self.database.next_id( Stub_object, commit = False ) + self.connection.rollback() + assert self.database.load( Stub_object, next_id ) == None - -class Test_database_without_clearing_cache( Test_database ): - def __init__( self ): - Test_database.__init__( self, clear_cache = False ) + def test_next_id_with_explit_commit( self ): + next_id = self.database.next_id( Stub_object, commit = False ) + self.database.commit() + assert next_id + assert self.database.load( Stub_object, next_id ) diff --git a/controller/test/Test_notebooks.py b/controller/test/Test_notebooks.py index 64a7efc..8f41209 100644 --- a/controller/test/Test_notebooks.py +++ b/controller/test/Test_notebooks.py @@ -2,10 +2,9 @@ import cherrypy import cgi from urllib import quote from Test_controller import Test_controller -from controller.Scheduler import Scheduler -from model.Notebook import Notebook -from model.Note import Note -from model.User import User +from new_model.Notebook import Notebook +from new_model.Note import Note +from new_model.User import User class Test_notebooks( Test_controller ): @@ -23,57 +22,51 @@ class Test_notebooks( Test_controller ): self.anonymous = None self.session_id = None - thread = self.make_notebooks() - self.scheduler.add( thread ) - self.scheduler.wait_for( thread ) - - thread = self.make_users() - self.scheduler.add( thread ) - self.scheduler.wait_for( thread ) + self.make_notebooks() + self.make_users() + self.database.commit() def make_notebooks( self ): - self.database.next_id( self.scheduler.thread ) - self.trash = Notebook( ( yield Scheduler.SLEEP ), u"trash", ) - self.database.next_id( self.scheduler.thread ) - self.notebook = Notebook( ( yield Scheduler.SLEEP ), u"notebook", self.trash ) + self.trash = Notebook.create( self.database.next_id( Notebook ), u"trash" ) + self.database.save( self.trash, commit = False ) + self.notebook = Notebook.create( self.database.next_id( Notebook ), u"notebook", self.trash.object_id ) + self.database.save( self.notebook, commit = False ) - self.database.next_id( self.scheduler.thread ) - note_id = ( yield Scheduler.SLEEP ) - self.note = Note( note_id, u"

my title

blah" ) - self.note_duplicate = Note( note_id, u"

my title

blah" ) - self.notebook.add_note( self.note ) - self.notebook.add_startup_note( self.note ) + note_id = self.database.next_id( Note ) + self.note = Note.create( note_id, u"

my title

blah", notebook_id = self.notebook.object_id, startup = True ) + self.database.save( self.note, commit = False ) - self.database.next_id( self.scheduler.thread ) - self.note2 = Note( ( yield Scheduler.SLEEP ), u"

other title

whee" ) - self.notebook.add_note( self.note2 ) - self.database.save( self.notebook ) + note_id = self.database.next_id( Note ) + self.note2 = Note.create( note_id, u"

other title

whee", notebook_id = self.notebook.object_id ) + self.database.save( self.note2, commit = False ) - self.database.next_id( self.scheduler.thread ) - self.anon_notebook = Notebook( ( yield Scheduler.SLEEP ), u"anon_notebook" ) - self.database.save( self.anon_notebook ) + self.anon_notebook = Notebook.create( self.database.next_id( Notebook ), u"anon_notebook" ) + self.database.save( self.anon_notebook, commit = False ) def make_users( self ): - self.database.next_id( self.scheduler.thread ) - self.user = User( ( yield Scheduler.SLEEP ), self.username, self.password, self.email_address, [ self.notebook ] ) - self.database.next_id( self.scheduler.thread ) - self.anonymous = User( ( yield Scheduler.SLEEP ), u"anonymous", None, None, [ self.anon_notebook ] ) + self.user = User.create( self.database.next_id( User ), self.username, self.password, self.email_address ) + self.database.save( self.user, commit = False ) + self.database.execute( self.user.sql_save_notebook( self.notebook.object_id, read_write = True ) ) + self.database.execute( self.user.sql_save_notebook( self.notebook.trash_id, read_write = True ) ) - self.database.save( self.user ) - self.database.save( self.anonymous ) + self.anonymous = User.create( self.database.next_id( User ), u"anonymous" ) + self.database.save( self.anonymous, commit = False ) + self.database.execute( self.user.sql_save_notebook( self.anon_notebook.object_id, read_write = False ) ) def test_default( self ): result = self.http_get( "/notebooks/%s" % self.notebook.object_id ) assert result.get( u"notebook_id" ) == self.notebook.object_id - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_default_with_note( self ): result = self.http_get( "/notebooks/%s?note_id=%s" % ( self.notebook.object_id, self.note.object_id ) ) assert result.get( u"notebook_id" ) == self.notebook.object_id assert result.get( u"note_id" ) == self.note.object_id - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_default_with_note_and_revision( self ): result = self.http_get( "/notebooks/%s?note_id=%s&revision=%s" % ( @@ -85,7 +78,8 @@ class Test_notebooks( Test_controller ): assert result.get( u"notebook_id" ) == self.notebook.object_id assert result.get( u"note_id" ) == self.note.object_id assert result.get( u"revision" ) == unicode( self.note.revision ) - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_default_with_parent( self ): parent_id = "foo" @@ -93,7 +87,8 @@ class Test_notebooks( Test_controller ): assert result.get( u"notebook_id" ) == self.notebook.object_id assert result.get( u"parent_id" ) == parent_id - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_contents( self ): self.login() @@ -107,9 +102,11 @@ class Test_notebooks( Test_controller ): startup_notes = result[ "startup_notes" ] assert notebook.object_id == self.notebook.object_id + assert notebook.read_write == True assert len( startup_notes ) == 1 - assert startup_notes[ 0 ] == self.note - assert self.user.storage_bytes == 0 + assert startup_notes[ 0 ].object_id == self.note.object_id + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_contents_with_note( self ): self.login() @@ -123,13 +120,15 @@ class Test_notebooks( Test_controller ): startup_notes = result[ "startup_notes" ] assert notebook.object_id == self.notebook.object_id + assert notebook.read_write == True assert len( startup_notes ) == 1 - assert startup_notes[ 0 ] == self.note + assert startup_notes[ 0 ].object_id == self.note.object_id note = result[ "note" ] assert note.object_id == self.note.object_id - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_contents_with_note_and_revision( self ): self.login() @@ -147,13 +146,15 @@ class Test_notebooks( Test_controller ): startup_notes = result[ "startup_notes" ] assert notebook.object_id == self.notebook.object_id + assert notebook.read_write == True assert len( startup_notes ) == 1 - assert startup_notes[ 0 ] == self.note + assert startup_notes[ 0 ].object_id == self.note.object_id note = result[ "note" ] assert note.object_id == self.note.object_id - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_contents_with_blank_note( self ): self.login() @@ -167,16 +168,18 @@ class Test_notebooks( Test_controller ): startup_notes = result[ "startup_notes" ] assert notebook.object_id == self.notebook.object_id + assert notebook.read_write == True assert len( startup_notes ) == 1 - assert startup_notes[ 0 ] == self.note + assert startup_notes[ 0 ].object_id == self.note.object_id note = result[ "note" ] assert note.object_id == u"blank" assert note.contents == None assert note.title == None - assert note.deleted_from == None - assert self.user.storage_bytes == 0 + assert note.deleted_from_id == None + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_contents_without_login( self ): result = self.http_get( @@ -185,7 +188,25 @@ class Test_notebooks( Test_controller ): ) assert result.get( "error" ) - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 + + def test_contents_with_read_only_notebook( self ): + self.login() + + result = self.http_get( + "/notebooks/contents?notebook_id=%s" % self.anon_notebook.object_id, + session_id = self.session_id, + ) + + notebook = result[ "notebook" ] + startup_notes = result[ "startup_notes" ] + + assert notebook.object_id == self.anon_notebook.object_id + assert notebook.read_write == False + assert len( startup_notes ) == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_load_note( self ): self.login() @@ -200,7 +221,8 @@ class Test_notebooks( Test_controller ): assert note.object_id == self.note.object_id assert note.title == self.note.title assert note.contents == self.note.contents - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_load_note_with_revision( self ): self.login() @@ -230,7 +252,8 @@ class Test_notebooks( Test_controller ): assert note.revision == previous_revision assert note.title == previous_title assert note.contents == previous_contents - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_load_note_without_login( self ): result = self.http_post( "/notebooks/load_note/", dict( @@ -249,7 +272,8 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_load_unknown_note( self ): self.login() @@ -261,7 +285,8 @@ class Test_notebooks( Test_controller ): note = result[ "note" ] assert note == None - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_load_note_by_title( self ): self.login() @@ -276,7 +301,8 @@ class Test_notebooks( Test_controller ): assert note.object_id == self.note.object_id assert note.title == self.note.title assert note.contents == self.note.contents - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_load_note_by_title_without_login( self ): result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -285,7 +311,8 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_load_note_by_title_with_unknown_notebook( self ): self.login() @@ -296,7 +323,8 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_load_unknown_note_by_title( self ): self.login() @@ -308,7 +336,8 @@ class Test_notebooks( Test_controller ): note = result[ "note" ] assert note == None - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_lookup_note_id( self ): self.login() @@ -319,7 +348,8 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "note_id" ) == self.note.object_id - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_lookup_note_id_without_login( self ): result = self.http_post( "/notebooks/lookup_note_id/", dict( @@ -328,7 +358,8 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_lookup_note_id_with_unknown_notebook( self ): self.login() @@ -339,7 +370,8 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_lookup_unknown_note_id( self ): self.login() @@ -350,7 +382,21 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "note_id" ) == None - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 + + def test_load_note_revisions( self ): + self.login() + + result = self.http_post( "/notebooks/load_note_revisions/", dict( + notebook_id = self.notebook.object_id, + note_id = self.note.object_id, + ), session_id = self.session_id ) + + revisions = result[ "revisions" ] + assert revisions != None + assert len( revisions ) == 1 + assert revisions[ 0 ] == self.note.revision def test_save_note( self, startup = False ): self.login() @@ -367,9 +413,11 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision + current_revision = result[ "new_revision" ] assert result[ "previous_revision" ] == previous_revision - assert self.user.storage_bytes > 0 - assert result[ "storage_bytes" ] == self.user.storage_bytes + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes > 0 + assert result[ "storage_bytes" ] == user.storage_bytes # make sure the old title can no longer be loaded result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -389,14 +437,26 @@ class Test_notebooks( Test_controller ): note = result[ "note" ] assert note.object_id == self.note.object_id - assert note.title == self.note.title - assert note.contents == self.note.contents + assert note.title == "new title" + assert note.contents == new_note_contents + assert note.startup == startup - # check that the note is / is not a startup note if startup: - assert note in self.notebook.startup_notes + assert note.rank == 0 else: - assert not note in self.notebook.startup_notes + assert note.rank is None + + # make sure that the correct revisions are returned and are in chronological order + result = self.http_post( "/notebooks/load_note_revisions/", dict( + notebook_id = self.notebook.object_id, + note_id = self.note.object_id, + ), session_id = self.session_id ) + + revisions = result[ "revisions" ] + assert revisions != None + assert len( revisions ) == 2 + assert revisions[ 0 ] == previous_revision + assert revisions[ 1 ] == current_revision def test_save_startup_note( self ): self.test_save_note( startup = True ) @@ -412,7 +472,8 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_save_startup_note_without_login( self ): self.test_save_note_without_login( startup = True ) @@ -427,7 +488,8 @@ class Test_notebooks( Test_controller ): # save over a deleted note, supplying new contents and a new title. this should cause the note # to be automatically undeleted - previous_revision = self.note.revision + deleted_note = self.database.load( Note, self.note.object_id ) + previous_revision = deleted_note.revision new_note_contents = u"

new title

new blah" result = self.http_post( "/notebooks/save_note/", dict( notebook_id = self.notebook.object_id, @@ -439,8 +501,9 @@ class Test_notebooks( Test_controller ): assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision assert result[ "previous_revision" ] == previous_revision - assert self.user.storage_bytes > 0 - assert result[ "storage_bytes" ] == self.user.storage_bytes + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes > 0 + assert result[ "storage_bytes" ] == user.storage_bytes # make sure the old title can no longer be loaded result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -457,15 +520,15 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) note = result[ "note" ] - + assert note assert note.object_id == self.note.object_id - assert note.title == self.note.title - assert note.contents == self.note.contents - assert note.deleted_from == None + assert note.title == "new title" + assert note.contents == new_note_contents + assert note.deleted_from_id == None # make sure the old title can no longer be loaded from the trash result = self.http_post( "/notebooks/load_note_by_title/", dict( - notebook_id = self.notebook.trash.object_id, + notebook_id = self.notebook.trash_id, note_title = "my title", ), session_id = self.session_id ) @@ -473,7 +536,7 @@ class Test_notebooks( Test_controller ): # make sure the new title is not loadable from the trash either result = self.http_post( "/notebooks/load_note_by_title/", dict( - notebook_id = self.notebook.trash.object_id, + notebook_id = self.notebook.trash_id, note_title = "new title", ), session_id = self.session_id ) @@ -485,7 +548,7 @@ class Test_notebooks( Test_controller ): # save over an existing note, supplying new contents and a new title previous_revision = self.note.revision new_note_contents = u"

new title

new blah" - self.http_post( "/notebooks/save_note/", dict( + result = self.http_post( "/notebooks/save_note/", dict( notebook_id = self.notebook.object_id, note_id = self.note.object_id, contents = new_note_contents, @@ -494,8 +557,9 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) # now attempt to save over that note again without changing the contents - previous_storage_bytes = self.user.storage_bytes - previous_revision = self.note.revision + user = self.database.load( User, self.user.object_id ) + previous_storage_bytes = user.storage_bytes + previous_revision = result[ "new_revision" ] result = self.http_post( "/notebooks/save_note/", dict( notebook_id = self.notebook.object_id, note_id = self.note.object_id, @@ -507,7 +571,8 @@ class Test_notebooks( Test_controller ): # assert that the note wasn't actually updated the second time assert result[ "new_revision" ] == None assert result[ "previous_revision" ] == previous_revision - assert self.user.storage_bytes == previous_storage_bytes + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == previous_storage_bytes assert result[ "storage_bytes" ] == 0 result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -516,10 +581,10 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) note = result[ "note" ] - + assert note assert note.object_id == self.note.object_id - assert note.title == self.note.title - assert note.contents == self.note.contents + assert note.title == "new title" + assert note.contents == new_note_contents assert note.revision == previous_revision def test_save_unchanged_deleted_note( self, startup = False ): @@ -533,7 +598,7 @@ class Test_notebooks( Test_controller ): # save over an existing deleted note, supplying new contents and a new title previous_revision = self.note.revision new_note_contents = u"

new title

new blah" - self.http_post( "/notebooks/save_note/", dict( + result = self.http_post( "/notebooks/save_note/", dict( notebook_id = self.notebook.object_id, note_id = self.note.object_id, contents = new_note_contents, @@ -542,8 +607,9 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) # now attempt to save over that note again without changing the contents - previous_storage_bytes = self.user.storage_bytes - previous_revision = self.note.revision + user = self.database.load( User, self.user.object_id ) + previous_storage_bytes = user.storage_bytes + previous_revision = result[ "new_revision" ] result = self.http_post( "/notebooks/save_note/", dict( notebook_id = self.notebook.object_id, note_id = self.note.object_id, @@ -555,7 +621,8 @@ class Test_notebooks( Test_controller ): # assert that the note wasn't actually updated the second time assert result[ "new_revision" ] == None assert result[ "previous_revision" ] == previous_revision - assert self.user.storage_bytes == previous_storage_bytes + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == previous_storage_bytes assert result[ "storage_bytes" ] == 0 result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -564,16 +631,16 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) note = result[ "note" ] - + assert note assert note.object_id == self.note.object_id - assert note.title == self.note.title - assert note.contents == self.note.contents + assert note.title == "new title" + assert note.contents == new_note_contents assert note.revision == previous_revision - assert note.deleted_from == None + assert note.deleted_from_id == None # make sure the note is not loadable from the trash result = self.http_post( "/notebooks/load_note_by_title/", dict( - notebook_id = self.notebook.trash.object_id, + notebook_id = self.notebook.trash_id, note_title = "new title", ), session_id = self.session_id ) @@ -585,7 +652,7 @@ class Test_notebooks( Test_controller ): # save over an existing note supplying new contents and a new title previous_revision = self.note.revision new_note_contents = u"

new title

new blah" - self.http_post( "/notebooks/save_note/", dict( + result = self.http_post( "/notebooks/save_note/", dict( notebook_id = self.notebook.object_id, note_id = self.note.object_id, contents = new_note_contents, @@ -595,7 +662,7 @@ class Test_notebooks( Test_controller ): # now attempt to save over that note again without changing the contents, but with a change # to its startup flag - previous_revision = self.note.revision + previous_revision = result[ "new_revision" ] result = self.http_post( "/notebooks/save_note/", dict( notebook_id = self.notebook.object_id, note_id = self.note.object_id, @@ -604,11 +671,12 @@ class Test_notebooks( Test_controller ): previous_revision = previous_revision, ), session_id = self.session_id ) - # assert that the note wasn't actually updated the second time - assert result[ "new_revision" ] == None + # assert that the note was updated the second time + assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision assert result[ "previous_revision" ] == previous_revision - assert self.user.storage_bytes > 0 - assert result[ "storage_bytes" ] == self.user.storage_bytes + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes > 0 + assert result[ "storage_bytes" ] == user.storage_bytes result = self.http_post( "/notebooks/load_note_by_title/", dict( notebook_id = self.notebook.object_id, @@ -616,17 +684,17 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) note = result[ "note" ] - + assert note assert note.object_id == self.note.object_id - assert note.title == self.note.title - assert note.contents == self.note.contents - assert note.revision == previous_revision + assert note.title == "new title" + assert note.contents == new_note_contents + assert note.revision > previous_revision + assert note.startup == ( not startup ) - # assert that the notebook now has the proper startup status for the note - if startup: - assert note not in self.notebook.startup_notes + if note.startup: + assert note.rank == 0 else: - assert note in self.notebook.startup_notes + assert note.rank is None def test_save_note_from_an_older_revision( self ): self.login() @@ -644,7 +712,7 @@ class Test_notebooks( Test_controller ): # save over that note again with new contents, providing the original # revision as the previous known revision - second_revision = self.note.revision + second_revision = result[ "new_revision" ] new_note_contents = u"

new new title

new new blah" result = self.http_post( "/notebooks/save_note/", dict( notebook_id = self.notebook.object_id, @@ -658,8 +726,9 @@ class Test_notebooks( Test_controller ): assert result[ "new_revision" ] assert result[ "new_revision" ] not in ( first_revision, second_revision ) assert result[ "previous_revision" ] == second_revision - assert self.user.storage_bytes > 0 - assert result[ "storage_bytes" ] == self.user.storage_bytes + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes > 0 + assert result[ "storage_bytes" ] == user.storage_bytes # make sure the first title can no longer be loaded result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -688,8 +757,8 @@ class Test_notebooks( Test_controller ): note = result[ "note" ] assert note.object_id == self.note.object_id - assert note.title == self.note.title - assert note.contents == self.note.contents + assert note.title == "new new title" + assert note.contents == new_note_contents def test_save_note_with_unknown_notebook( self ): self.login() @@ -704,13 +773,14 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) - assert self.user.storage_bytes == 0 + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 def test_save_new_note( self, startup = False ): self.login() # save a completely new note - new_note = Note( "55", u"

newest title

foo" ) + new_note = Note.create( "55", u"

newest title

foo" ) previous_revision = new_note.revision result = self.http_post( "/notebooks/save_note/", dict( notebook_id = self.notebook.object_id, @@ -722,8 +792,9 @@ class Test_notebooks( Test_controller ): assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision assert result[ "previous_revision" ] == None - assert self.user.storage_bytes > 0 - assert result[ "storage_bytes" ] == self.user.storage_bytes + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes > 0 + assert result[ "storage_bytes" ] == user.storage_bytes # make sure the new title is now loadable result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -736,12 +807,12 @@ class Test_notebooks( Test_controller ): assert note.object_id == new_note.object_id assert note.title == new_note.title assert note.contents == new_note.contents + assert note.startup == startup - # check that the note is / is not a startup note if startup: - assert note in self.notebook.startup_notes + assert note.rank == 0 else: - assert not note in self.notebook.startup_notes + assert note.rank is None def test_save_new_startup_note( self ): self.test_save_new_note( startup = True ) @@ -753,7 +824,7 @@ class Test_notebooks( Test_controller ): title_with_tags = u"

my title

" junk = u"foo" more_junk = u"

blah

" - new_note = Note( "55", title_with_tags + junk + more_junk ) + new_note = Note.create( "55", title_with_tags + junk + more_junk ) previous_revision = new_note.revision result = self.http_post( "/notebooks/save_note/", dict( @@ -766,8 +837,9 @@ class Test_notebooks( Test_controller ): assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision assert result[ "previous_revision" ] == None - assert self.user.storage_bytes > 0 - assert result[ "storage_bytes" ] == self.user.storage_bytes + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes > 0 + assert result[ "storage_bytes" ] == user.storage_bytes # make sure the new title is now loadable result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -789,7 +861,7 @@ class Test_notebooks( Test_controller ): # save a completely new note contents = "

newest title

foo" junk = "\xa0bar" - new_note = Note( "55", contents + junk ) + new_note = Note.create( "55", contents + junk ) previous_revision = new_note.revision result = self.http_post( "/notebooks/save_note/", dict( notebook_id = self.notebook.object_id, @@ -801,8 +873,9 @@ class Test_notebooks( Test_controller ): assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision assert result[ "previous_revision" ] == None - assert self.user.storage_bytes > 0 - assert result[ "storage_bytes" ] == self.user.storage_bytes + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes > 0 + assert result[ "storage_bytes" ] == user.storage_bytes # make sure the new title is now loadable result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -816,158 +889,60 @@ class Test_notebooks( Test_controller ): assert note.title == new_note.title assert note.contents == contents + " bar" - def test_add_startup_note( self ): + def test_save_two_new_notes( self, startup = False ): self.login() - result = self.http_post( "/notebooks/add_startup_note/", dict( + # save a completely new note + new_note = Note.create( "55", u"

newest title

foo" ) + previous_revision = new_note.revision + result = self.http_post( "/notebooks/save_note/", dict( notebook_id = self.notebook.object_id, - note_id = self.note2.object_id, + note_id = new_note.object_id, + contents = new_note.contents, + startup = startup, + previous_revision = None, ), session_id = self.session_id ) - assert result[ "storage_bytes" ] == self.user.storage_bytes + user = self.database.load( User, self.user.object_id ) + previous_storage_bytes = user.storage_bytes - # test that the added note shows up in notebook.startup_notes - result = self.http_get( - "/notebooks/contents?notebook_id=%s" % self.notebook.object_id, - session_id = self.session_id, - ) - - notebook = result[ "notebook" ] - - assert len( notebook.startup_notes ) == 2 - assert notebook.startup_notes[ 0 ] == self.note - assert notebook.startup_notes[ 1 ] == self.note2 - assert self.user.storage_bytes > 0 - - def test_add_startup_note_without_login( self ): - result = self.http_post( "/notebooks/add_startup_note/", dict( + # save a completely new note + new_note = Note.create( "56", u"

my title

foo" ) + previous_revision = new_note.revision + result = self.http_post( "/notebooks/save_note/", dict( notebook_id = self.notebook.object_id, - note_id = self.note2.object_id, + note_id = new_note.object_id, + contents = new_note.contents, + startup = startup, + previous_revision = None, ), session_id = self.session_id ) - assert result.get( "error" ) - assert self.user.storage_bytes == 0 + assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision + assert result[ "previous_revision" ] == None + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes > 0 + assert result[ "storage_bytes" ] == user.storage_bytes - def test_add_startup_note_with_unknown_notebook( self ): - self.login() - - result = self.http_post( "/notebooks/add_startup_note/", dict( - notebook_id = self.unknown_notebook_id, - note_id = self.note2.object_id, - ), session_id = self.session_id ) - - # test that notebook.startup_notes hasn't changed - result = self.http_get( - "/notebooks/contents?notebook_id=%s" % self.notebook.object_id, - session_id = self.session_id, - ) - - notebook = result[ "notebook" ] - - assert len( notebook.startup_notes ) == 1 - assert notebook.startup_notes[ 0 ] == self.note - assert self.user.storage_bytes == 0 - - def test_add_startup_unknown_note( self ): - self.login() - - result = self.http_post( "/notebooks/add_startup_note/", dict( + # make sure the new title is now loadable + result = self.http_post( "/notebooks/load_note_by_title/", dict( notebook_id = self.notebook.object_id, - note_id = self.unknown_note_id, + note_title = new_note.title, ), session_id = self.session_id ) - # test that notebook.startup_notes hasn't changed - result = self.http_get( - "/notebooks/contents?notebook_id=%s" % self.notebook.object_id, - session_id = self.session_id, - ) + note = result[ "note" ] - notebook = result[ "notebook" ] + assert note.object_id == new_note.object_id + assert note.title == new_note.title + assert note.contents == new_note.contents + assert note.startup == startup - assert len( notebook.startup_notes ) == 1 - assert notebook.startup_notes[ 0 ] == self.note - assert self.user.storage_bytes == 0 + if startup: + assert note.rank == 1 # one greater than the previous new note's rank + else: + assert note.rank is None - def test_remove_startup_note( self ): - self.login() - - result = self.http_post( "/notebooks/remove_startup_note/", dict( - notebook_id = self.notebook.object_id, - note_id = self.note.object_id, - ), session_id = self.session_id ) - - assert result[ "storage_bytes" ] == self.user.storage_bytes - - # test that the remove note no longer shows up in notebook.startup_notes - result = self.http_get( - "/notebooks/contents?notebook_id=%s" % self.notebook.object_id, - session_id = self.session_id, - ) - - notebook = result[ "notebook" ] - - assert len( notebook.startup_notes ) == 0 - assert self.user.storage_bytes > 0 - - def test_remove_startup_note_without_login( self ): - result = self.http_post( "/notebooks/remove_startup_note/", dict( - notebook_id = self.notebook.object_id, - note_id = self.note.object_id, - ), session_id = self.session_id ) - - assert result.get( "error" ) - assert self.user.storage_bytes == 0 - - def test_remove_startup_note_with_unknown_notebook( self ): - self.login() - - result = self.http_post( "/notebooks/remove_startup_note/", dict( - notebook_id = self.unknown_notebook_id, - note_id = self.note.object_id, - ), session_id = self.session_id ) - - # test that notebook.startup_notes hasn't changed - result = self.http_get( - "/notebooks/contents?notebook_id=%s" % self.notebook.object_id, - session_id = self.session_id, - ) - - notebook = result[ "notebook" ] - - assert len( notebook.startup_notes ) == 1 - assert notebook.startup_notes[ 0 ] == self.note - assert self.user.storage_bytes == 0 - - def test_remove_startup_unknown_note( self ): - self.login() - - result = self.http_post( "/notebooks/remove_startup_note/", dict( - notebook_id = self.notebook.object_id, - note_id = self.unknown_note_id, - ), session_id = self.session_id ) - - assert result[ "storage_bytes" ] == self.user.storage_bytes - - # test that notebook.startup_notes hasn't changed - result = self.http_get( - "/notebooks/contents?notebook_id=%s" % self.notebook.object_id, - session_id = self.session_id, - ) - - notebook = result[ "notebook" ] - - assert len( notebook.startup_notes ) == 1 - assert notebook.startup_notes[ 0 ] == self.note - assert self.user.storage_bytes == 0 - - def test_is_startup_note( self ): - self.login() - - assert self.notebook.is_startup_note( self.note ) == True - assert self.notebook.is_startup_note( self.note2 ) == False - - # make sure that a different note object with the same id as self.note is considered a startup note - assert self.notebook.is_startup_note( self.note_duplicate ) == True + def test_save_two_new_startup_notes( self ): + self.test_save_two_new_notes( startup = True ) def test_delete_note( self ): self.login() @@ -977,7 +952,9 @@ class Test_notebooks( Test_controller ): note_id = self.note.object_id, ), session_id = self.session_id ) - assert result[ "storage_bytes" ] == self.user.storage_bytes + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes > 0 + assert result[ "storage_bytes" ] == user.storage_bytes # test that the deleted note is actually deleted result = self.http_post( "/notebooks/load_note/", dict( @@ -985,18 +962,7 @@ class Test_notebooks( Test_controller ): note_id = self.note.object_id, ), session_id = self.session_id ) - assert result.get( "note" ) == None - - # test that the note get moved to the trash - result = self.http_post( "/notebooks/load_note/", dict( - notebook_id = self.notebook.trash.object_id, - note_id = self.note.object_id, - ), session_id = self.session_id ) - - note = result.get( "note" ) - assert note - assert note.object_id == self.note.object_id - assert note.deleted_from == self.notebook.object_id + assert "access" in result.get( "error" ) def test_delete_note_from_trash( self ): self.login() @@ -1007,23 +973,27 @@ class Test_notebooks( Test_controller ): note_id = self.note.object_id, ), session_id = self.session_id ) - assert result[ "storage_bytes" ] == self.user.storage_bytes + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes > 0 + assert result[ "storage_bytes" ] == user.storage_bytes # then, delete the note from the trash result = self.http_post( "/notebooks/delete_note/", dict( - notebook_id = self.notebook.trash.object_id, + notebook_id = self.notebook.trash_id, note_id = self.note.object_id, ), session_id = self.session_id ) - assert result[ "storage_bytes" ] == self.user.storage_bytes + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes > 0 + assert result[ "storage_bytes" ] == user.storage_bytes # test that the deleted note is actually deleted from the trash result = self.http_post( "/notebooks/load_note/", dict( - notebook_id = self.notebook.trash.object_id, + notebook_id = self.notebook.trash_id, note_id = self.note.object_id, ), session_id = self.session_id ) - assert result.get( "note" ) == None + assert "access" in result.get( "error" ) def test_delete_note_without_login( self ): result = self.http_post( "/notebooks/delete_note/", dict( @@ -1082,7 +1052,9 @@ class Test_notebooks( Test_controller ): note_id = self.note.object_id, ), session_id = self.session_id ) - assert result[ "storage_bytes" ] == self.user.storage_bytes + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes > 0 + assert result[ "storage_bytes" ] == user.storage_bytes # test that the undeleted note is actually undeleted result = self.http_post( "/notebooks/load_note/", dict( @@ -1093,15 +1065,8 @@ class Test_notebooks( Test_controller ): note = result.get( "note" ) assert note assert note.object_id == self.note.object_id - assert note.deleted_from == None - - # test that the note is no longer in the trash - result = self.http_post( "/notebooks/load_note/", dict( - notebook_id = self.notebook.trash.object_id, - note_id = self.note.object_id, - ), session_id = self.session_id ) - - assert result.get( "note" ) == None + assert note.deleted_from_id == None + assert note.notebook_id == self.notebook.object_id def test_undelete_note_that_is_not_deleted( self ): self.login() @@ -1123,15 +1088,8 @@ class Test_notebooks( Test_controller ): note = result.get( "note" ) assert note assert note.object_id == self.note.object_id - assert note.deleted_from == None - - # test that the note is not somehow in the trash - result = self.http_post( "/notebooks/load_note/", dict( - notebook_id = self.notebook.trash.object_id, - note_id = self.note.object_id, - ), session_id = self.session_id ) - - assert result.get( "note" ) == None + assert note.deleted_from_id == None + assert note.notebook_id == self.notebook.object_id def test_undelete_note_without_login( self ): result = self.http_post( "/notebooks/undelete_note/", dict( @@ -1158,13 +1116,15 @@ class Test_notebooks( Test_controller ): # test that the note hasn't been undeleted result = self.http_post( "/notebooks/load_note/", dict( - notebook_id = self.notebook.trash.object_id, + notebook_id = self.notebook.trash_id, note_id = self.note.object_id, ), session_id = self.session_id ) note = result.get( "note" ) + assert note assert note.object_id == self.note.object_id - assert note.deleted_from == self.notebook.object_id + assert note.deleted_from_id == self.notebook.object_id + assert note.notebook_id == self.notebook.trash_id def test_undelete_unknown_note( self ): self.login() @@ -1181,8 +1141,10 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) note = result.get( "note" ) + assert note assert note.object_id == self.note.object_id - assert note.deleted_from == None + assert note.deleted_from_id == None + assert note.notebook_id == self.notebook.object_id def test_undelete_note_from_incorrect_notebook( self ): self.login() @@ -1201,15 +1163,17 @@ class Test_notebooks( Test_controller ): # test that the note hasn't been undeleted result = self.http_post( "/notebooks/load_note/", dict( - notebook_id = self.notebook.trash.object_id, + notebook_id = self.notebook.trash_id, note_id = self.note.object_id, ), session_id = self.session_id ) note = result.get( "note" ) + assert note assert note.object_id == self.note.object_id - assert note.deleted_from == self.notebook.object_id + assert note.deleted_from_id == self.notebook.object_id + assert note.notebook_id == self.notebook.trash_id - def test_undelete_note_that_is_not_deleted_from_incorrect_notebook( self ): + def test_undelete_note_that_is_not_deleted_from_id_incorrect_notebook( self ): self.login() result = self.http_post( "/notebooks/undelete_note/", dict( @@ -1226,8 +1190,10 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) note = result.get( "note" ) + assert note assert note.object_id == self.note.object_id - assert note.deleted_from == None + assert note.deleted_from_id == None + assert note.notebook_id == self.notebook.object_id def test_delete_all_notes( self ): self.login() @@ -1236,7 +1202,9 @@ class Test_notebooks( Test_controller ): notebook_id = self.notebook.object_id, ), session_id = self.session_id ) - assert result[ "storage_bytes" ] == self.user.storage_bytes + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes > 0 + assert result[ "storage_bytes" ] == user.storage_bytes # test that all notes are actually deleted result = self.http_post( "/notebooks/load_note/", dict( @@ -1244,35 +1212,14 @@ class Test_notebooks( Test_controller ): note_id = self.note.object_id, ), session_id = self.session_id ) - assert result.get( "note" ) == None + assert "access" in result.get( "error" ) result = self.http_post( "/notebooks/load_note/", dict( notebook_id = self.notebook.object_id, note_id = self.note2.object_id, ), session_id = self.session_id ) - assert result.get( "note" ) == None - - # test that the notes get moved to the trash - result = self.http_post( "/notebooks/load_note/", dict( - notebook_id = self.notebook.trash.object_id, - note_id = self.note.object_id, - ), session_id = self.session_id ) - - note = result.get( "note" ) - assert note - assert note.object_id == self.note.object_id - assert note.deleted_from == self.notebook.object_id - - result = self.http_post( "/notebooks/load_note/", dict( - notebook_id = self.notebook.trash.object_id, - note_id = self.note2.object_id, - ), session_id = self.session_id ) - - note2 = result.get( "note" ) - assert note2 - assert note2.object_id == self.note2.object_id - assert note2.deleted_from == self.notebook.object_id + assert "access" in result.get( "error" ) def test_delete_all_notes_from_trash( self ): self.login() @@ -1284,25 +1231,20 @@ class Test_notebooks( Test_controller ): # then, delete all notes from the trash result = self.http_post( "/notebooks/delete_all_notes/", dict( - notebook_id = self.notebook.trash.object_id, + notebook_id = self.notebook.trash_id, ), session_id = self.session_id ) - assert result[ "storage_bytes" ] == self.user.storage_bytes + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes > 0 + assert result[ "storage_bytes" ] == user.storage_bytes # test that all notes are actually deleted from the trash result = self.http_post( "/notebooks/load_note/", dict( - notebook_id = self.notebook.trash.object_id, + notebook_id = self.notebook.trash_id, note_id = self.note.object_id, ), session_id = self.session_id ) - assert result.get( "note" ) == None - - result = self.http_post( "/notebooks/load_note/", dict( - notebook_id = self.notebook.trash.object_id, - note_id = self.note2.object_id, - ), session_id = self.session_id ) - - assert result.get( "note" ) == None + assert "access" in result.get( "error" ) def test_delete_all_notes_without_login( self ): result = self.http_post( "/notebooks/delete_all_notes/", dict( @@ -1423,10 +1365,8 @@ class Test_notebooks( Test_controller ): # ensure that notes with titles matching the search text show up before notes with only # contents matching the search text - note3 = Note( "55", u"

blah

foo" ) - self.notebook.add_note( note3 ) - - self.database.save( self.notebook ) + note3 = Note.create( "55", u"

blah

foo", notebook_id = self.notebook.object_id ) + self.database.save( note3 ) search_text = "bla" @@ -1458,8 +1398,8 @@ class Test_notebooks( Test_controller ): def test_search_character_refs( self ): self.login() - note3 = Note( "55", u"

foo: bar

baz" ) - self.notebook.add_note( note3 ) + note3 = Note.create( "55", u"

foo: bar

baz", notebook_id = self.notebook.object_id ) + self.database.save( note3 ) search_text = "oo: b" @@ -1498,8 +1438,8 @@ class Test_notebooks( Test_controller ): def test_download_html( self ): self.login() - note3 = Note( "55", u"

blah

foo" ) - self.notebook.add_note( note3 ) + note3 = Note.create( "55", u"

blah

foo", notebook_id = self.notebook.object_id ) + self.database.save( note3 ) result = self.http_get( "/notebooks/download_html/%s" % self.notebook.object_id, @@ -1525,8 +1465,8 @@ class Test_notebooks( Test_controller ): previous_revision = note.revision def test_download_html( self ): - note3 = Note( "55", u"

blah

foo" ) - self.notebook.add_note( note3 ) + note3 = Note.create( "55", u"

blah

foo", notebook_id = self.notebook.object_id ) + self.database.save( note3 ) result = self.http_get( "/notebooks/download_html/%s" % self.notebook.object_id, diff --git a/controller/test/Test_root.py b/controller/test/Test_root.py index 19c8f54..7ae37e3 100644 --- a/controller/test/Test_root.py +++ b/controller/test/Test_root.py @@ -1,5 +1,5 @@ import cherrypy -from model.User import User +from new_model.User import User from controller.Scheduler import Scheduler from Test_controller import Test_controller @@ -14,13 +14,7 @@ class Test_root( Test_controller ): self.user = None self.session_id = None - thread = self.make_user() - self.scheduler.add( thread ) - self.scheduler.wait_for( thread ) - - def make_user( self ): - self.database.next_id( self.scheduler.thread ) - self.user = User( ( yield Scheduler.SLEEP ), self.username, self.password, self.email_address, [] ) + self.user = User.create( self.database.next_id( User ), self.username, self.password, self.email_address ) self.database.save( self.user ) def test_index( self ): diff --git a/controller/test/Test_users.py b/controller/test/Test_users.py index 1167228..ff18200 100644 --- a/controller/test/Test_users.py +++ b/controller/test/Test_users.py @@ -1,15 +1,15 @@ import re import cherrypy import smtplib +from pytz import utc from datetime import datetime, timedelta from nose.tools import raises from Test_controller import Test_controller from Stub_smtp import Stub_smtp -from controller.Scheduler import Scheduler -from model.User import User -from model.Notebook import Notebook -from model.Note import Note -from model.User_list import User_list +from new_model.User import User +from new_model.Notebook import Notebook +from new_model.Note import Note +from new_model.Password_reset import Password_reset class Test_users( Test_controller ): @@ -32,46 +32,42 @@ class Test_users( Test_controller ): self.anonymous = None self.notebooks = None - thread = self.make_users() - self.scheduler.add( thread ) - self.scheduler.wait_for( thread ) + self.make_users() def make_users( self ): - self.database.next_id( self.scheduler.thread ) - notebook_id1 = ( yield Scheduler.SLEEP ) - self.database.next_id( self.scheduler.thread ) - notebook_id2 = ( yield Scheduler.SLEEP ) + notebook_id1 = self.database.next_id( Notebook ) + notebook_id2 = self.database.next_id( Notebook ) + trash_id1 = self.database.next_id( Notebook ) + trash_id2 = self.database.next_id( Notebook ) self.notebooks = [ - Notebook( notebook_id1, u"my notebook" ), - Notebook( notebook_id2, u"my other notebook" ), + Notebook.create( notebook_id1, u"my notebook", trash_id = trash_id1 ), + Notebook.create( notebook_id2, u"my other notebook", trash_id = trash_id2 ), ] + self.database.save( self.notebooks[ 0 ] ) + self.database.save( self.notebooks[ 1 ] ) - self.database.next_id( self.scheduler.thread ) - self.anon_notebook = Notebook( ( yield Scheduler.SLEEP ), u"anon notebook" ) - self.database.next_id( self.scheduler.thread ) - self.startup_note = Note( ( yield Scheduler.SLEEP ), u"

login

" ) - self.anon_notebook.add_note( self.startup_note ) - self.anon_notebook.add_startup_note( self.startup_note ) + self.anon_notebook = Notebook.create( self.database.next_id( Notebook ), u"anon notebook" ) + self.database.save( self.anon_notebook ) + self.startup_note = Note.create( + self.database.next_id( Note ), u"

login

", + notebook_id = self.anon_notebook.object_id, startup = True, + ) + self.database.save( self.startup_note ) - self.database.next_id( self.scheduler.thread ) - self.user = User( ( yield Scheduler.SLEEP ), self.username, self.password, self.email_address, self.notebooks ) - self.database.next_id( self.scheduler.thread ) - self.user2 = User( ( yield Scheduler.SLEEP ), self.username2, self.password2, self.email_address2 ) - self.database.next_id( self.scheduler.thread ) - self.anonymous = User( ( yield Scheduler.SLEEP ), u"anonymous", None, None, [ self.anon_notebook ] ) + self.user = User.create( self.database.next_id( User ), self.username, self.password, self.email_address ) + self.database.save( self.user, commit = False ) + self.database.execute( self.user.sql_save_notebook( notebook_id1, read_write = True ), commit = False ) + self.database.execute( self.user.sql_save_notebook( notebook_id2, read_write = True ), commit = False ) - self.database.next_id( self.scheduler.thread ) - user_list_id = ( yield Scheduler.SLEEP ) - user_list = User_list( user_list_id, u"all" ) - user_list.add_user( self.user ) - user_list.add_user( self.user2 ) - user_list.add_user( self.anonymous ) + self.user2 = User.create( self.database.next_id( User ), self.username2, self.password2, self.email_address2 ) + self.database.save( self.user2, commit = False ) - self.database.save( self.user ) - self.database.save( self.user2 ) - self.database.save( self.anonymous ) - self.database.save( user_list ) + self.anonymous = User.create( self.database.next_id( User ), u"anonymous" ) + self.database.save( self.anonymous, commit = False ) + self.database.execute( self.anonymous.sql_save_notebook( self.anon_notebook.object_id ), commit = False ) + + self.database.commit() def test_signup( self ): result = self.http_post( "/users/signup", dict( @@ -103,20 +99,33 @@ class Test_users( Test_controller ): assert result[ u"user" ].username == self.new_username notebooks = result[ u"notebooks" ] - assert len( notebooks ) == 2 - assert notebooks[ 0 ] == self.anon_notebook - assert notebooks[ 0 ].trash == None + notebook = notebooks[ 0 ] + assert notebook.object_id == self.anon_notebook.object_id + assert notebook.revision == self.anon_notebook.revision + assert notebook.name == self.anon_notebook.name + assert notebook.trash_id == None + assert notebook.read_write == False notebook = notebooks[ 1 ] assert notebook.object_id == new_notebook_id - assert notebook.trash - assert len( notebook.notes ) == 1 - assert len( notebook.startup_notes ) == 1 + assert notebook.revision + assert notebook.name == u"my notebook" + assert notebook.trash_id + assert notebook.read_write == True + + notebook = notebooks[ 2 ] + assert notebook.object_id == notebooks[ 1 ].trash_id + assert notebook.revision + assert notebook.name == u"trash" + assert notebook.trash_id == None + assert notebook.read_write == True startup_notes = result[ "startup_notes" ] if include_startup_notes: assert len( startup_notes ) == 1 - assert startup_notes[ 0 ] == self.startup_note + assert startup_notes[ 0 ].object_id == self.startup_note.object_id + assert startup_notes[ 0 ].title == self.startup_note.title + assert startup_notes[ 0 ].contents == self.startup_note.contents else: assert startup_notes == [] @@ -156,20 +165,34 @@ class Test_users( Test_controller ): assert result[ u"user" ].username == None notebooks = result[ u"notebooks" ] - assert len( notebooks ) == 2 - assert notebooks[ 0 ] == self.anon_notebook - assert notebooks[ 0 ].trash == None + assert len( notebooks ) == 3 + notebook = notebooks[ 0 ] + assert notebook.object_id == self.anon_notebook.object_id + assert notebook.revision == self.anon_notebook.revision + assert notebook.name == self.anon_notebook.name + assert notebook.trash_id == None + assert notebook.read_write == False notebook = notebooks[ 1 ] assert notebook.object_id == new_notebook_id - assert notebook.trash - assert len( notebook.notes ) == 2 - assert len( notebook.startup_notes ) == 2 + assert notebook.revision + assert notebook.name == u"my notebook" + assert notebook.trash_id + assert notebook.read_write == True + + notebook = notebooks[ 2 ] + assert notebook.object_id == notebooks[ 1 ].trash_id + assert notebook.revision + assert notebook.name == u"trash" + assert notebook.trash_id == None + assert notebook.read_write == True startup_notes = result[ "startup_notes" ] if include_startup_notes: assert len( startup_notes ) == 1 - assert startup_notes[ 0 ] == self.startup_note + assert startup_notes[ 0 ].object_id == self.startup_note.object_id + assert startup_notes[ 0 ].title == self.startup_note.title + assert startup_notes[ 0 ].contents == self.startup_note.contents else: assert startup_notes == [] @@ -260,15 +283,25 @@ class Test_users( Test_controller ): session_id = session_id, ) - assert result[ u"user" ] == self.user - assert result[ u"notebooks" ] == [ self.anon_notebook ] + self.notebooks + assert result[ u"user" ] + assert result[ u"user" ].object_id == self.user.object_id + assert result[ u"user" ].username == self.user.username + assert len( result[ u"notebooks" ] ) == 3 + assert result[ u"notebooks" ][ 0 ].object_id == self.anon_notebook.object_id + assert result[ u"notebooks" ][ 0 ].read_write == False + assert result[ u"notebooks" ][ 1 ].object_id == self.notebooks[ 0 ].object_id + assert result[ u"notebooks" ][ 1 ].read_write == True + assert result[ u"notebooks" ][ 2 ].object_id == self.notebooks[ 1 ].object_id + assert result[ u"notebooks" ][ 2 ].read_write == True assert result[ u"http_url" ] == self.settings[ u"global" ].get( u"luminotes.http_url" ) assert result[ u"login_url" ] == None startup_notes = result[ "startup_notes" ] if include_startup_notes: assert len( startup_notes ) == 1 - assert startup_notes[ 0 ] == self.startup_note + assert startup_notes[ 0 ].object_id == self.startup_note.object_id + assert startup_notes[ 0 ].title == self.startup_note.title + assert startup_notes[ 0 ].contents == self.startup_note.contents else: assert startup_notes == [] @@ -281,10 +314,13 @@ class Test_users( Test_controller ): ) assert result[ u"user" ].username == "anonymous" - assert result[ u"notebooks" ] == [ self.anon_notebook ] + assert len( result[ u"notebooks" ] ) == 1 + assert result[ u"notebooks" ][ 0 ].object_id == self.anon_notebook.object_id + assert result[ u"notebooks" ][ 0 ].name == self.anon_notebook.name + assert result[ u"notebooks" ][ 0 ].read_write == False assert result[ u"http_url" ] == self.settings[ u"global" ].get( u"luminotes.http_url" ) - login_note = self.anon_notebook.lookup_note_by_title( u"login" ) + login_note = self.database.select_one( Note, self.anon_notebook.sql_load_note_by_title( u"login" ) ) assert result[ u"login_url" ] == u"%s/notebooks/%s?note_id=%s" % ( self.settings[ u"global" ][ u"luminotes.https_url" ], self.anon_notebook.object_id, @@ -294,72 +330,37 @@ class Test_users( Test_controller ): startup_notes = result[ "startup_notes" ] if include_startup_notes: assert len( startup_notes ) == 1 - assert startup_notes[ 0 ] == self.startup_note + assert startup_notes[ 0 ].object_id == self.startup_note.object_id + assert startup_notes[ 0 ].title == self.startup_note.title + assert startup_notes[ 0 ].contents == self.startup_note.contents else: assert startup_notes == [] def test_current_with_startup_notes_without_login( self ): self.test_current_without_login( include_startup_notes = True ) - def test_calculate_user_storage( self ): - size = cherrypy.root.users.calculate_storage( self.user ) - notebooks = self.user.notebooks - - # expected a sum of the sizes of all of this user's notebooks, notes, and revisions - expected_size = \ - self.database.size( notebooks[ 0 ].object_id ) + \ - self.database.size( notebooks[ 1 ].object_id ) - - assert size == expected_size - - def test_calculate_anon_storage( self ): - size = cherrypy.root.users.calculate_storage( self.anonymous ) - - expected_size = \ - self.database.size( self.anon_notebook.object_id ) + \ - self.database.size( self.anon_notebook.notes[ 0 ].object_id ) + \ - self.database.size( self.anon_notebook.notes[ 0 ].object_id, self.anon_notebook.notes[ 0 ].revision ) - - assert size == expected_size - def test_update_storage( self ): previous_revision = self.user.revision cherrypy.root.users.update_storage( self.user.object_id ) - self.scheduler.wait_until_idle() expected_size = cherrypy.root.users.calculate_storage( self.user ) - assert self.user.storage_bytes == expected_size - assert self.user.revision > previous_revision + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == expected_size + assert user.revision > previous_revision def test_update_storage_with_unknown_user_id( self ): original_revision = self.user.revision cherrypy.root.users.update_storage( 77 ) - self.scheduler.wait_until_idle() expected_size = cherrypy.root.users.calculate_storage( self.user ) + user = self.database.load( User, self.user.object_id ) assert self.user.storage_bytes == 0 assert self.user.revision == original_revision - def test_update_storage_with_callback( self ): - def gen(): - previous_revision = self.user.revision - - cherrypy.root.users.update_storage( self.user.object_id, self.scheduler.thread ) - user = ( yield Scheduler.SLEEP ) - - expected_size = cherrypy.root.users.calculate_storage( self.user ) - assert user == self.user - assert self.user.storage_bytes == expected_size - assert self.user.revision > previous_revision - - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) - def test_send_reset( self ): # trick send_reset() into using a fake SMTP server Stub_smtp.reset() @@ -408,7 +409,7 @@ class Test_users( Test_controller ): result = self.http_get( "/users/redeem_reset/%s" % password_reset_id ) - assert result[ u"notebook_id" ] == self.anonymous.notebooks[ 0 ].object_id + assert result[ u"notebook_id" ] == self.anon_notebook.object_id assert result[ u"note_id" ] assert u"password reset" in result[ u"note_contents" ] assert self.user.username in result[ u"note_contents" ] @@ -434,15 +435,9 @@ class Test_users( Test_controller ): assert password_reset_id # to trigger expiration, pretend that the password reset was made 25 hours ago - def gen(): - self.database.load( password_reset_id, self.scheduler.thread ) - password_reset = ( yield Scheduler.SLEEP ) - password_reset._Persistent__revision = datetime.now() - timedelta( hours = 25 ) - self.database.save( password_reset ) - - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) + password_reset = self.database.load( Password_reset, password_reset_id ) + password_reset._Persistent__revision = datetime.now( tz = utc ) - timedelta( hours = 25 ) + self.database.save( password_reset ) result = self.http_get( "/users/redeem_reset/%s" % password_reset_id ) @@ -461,15 +456,9 @@ class Test_users( Test_controller ): password_reset_id = matches.group( 2 ) assert password_reset_id - def gen(): - self.database.load( password_reset_id, self.scheduler.thread ) - password_reset = ( yield Scheduler.SLEEP ) - password_reset.redeemed = True - self.database.save( password_reset ) - - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) + password_reset = self.database.load( Password_reset, password_reset_id ) + password_reset.redeemed = True + self.database.save( password_reset ) result = self.http_get( "/users/redeem_reset/%s" % password_reset_id ) @@ -488,15 +477,9 @@ class Test_users( Test_controller ): password_reset_id = matches.group( 2 ) assert password_reset_id - def gen(): - self.database.load( password_reset_id, self.scheduler.thread ) - password_reset = ( yield Scheduler.SLEEP ) - password_reset._Password_reset__email_address = u"unknown@example.com" - self.database.save( password_reset ) - - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) + password_reset = self.database.load( Password_reset, password_reset_id ) + password_reset._Password_reset__email_address = u"unknown@example.com" + self.database.save( password_reset ) result = self.http_get( "/users/redeem_reset/%s" % password_reset_id ) @@ -525,20 +508,17 @@ class Test_users( Test_controller ): ( self.user2.object_id, u"" ), ) ) - # check that the password reset is now marked as redeemed - def gen(): - self.database.load( password_reset_id, self.scheduler.thread ) - password_reset = ( yield Scheduler.SLEEP ) - assert password_reset.redeemed + assert result[ u"redirect" ] - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) + # check that the password reset is now marked as redeemed + password_reset = self.database.load( Password_reset, password_reset_id ) + assert password_reset.redeemed # check that the password was actually reset for one of the users, but not the other - assert self.user.check_password( new_password ) - assert self.user2.check_password( self.password2 ) - assert result[ u"redirect" ] + user = self.database.load( User, self.user.object_id ) + assert user.check_password( new_password ) + user2 = self.database.load( User, self.user2.object_id ) + assert user2.check_password( self.password2 ) def test_reset_password_unknown_reset_id( self ): new_password = u"newpass" @@ -552,11 +532,14 @@ class Test_users( Test_controller ): ( self.user2.object_id, u"" ), ) ) - # check that neither user's password has changed - assert self.user.check_password( self.password ) - assert self.user2.check_password( self.password2 ) assert u"expired" in result[ "error" ] + # check that neither user's password has changed + user = self.database.load( User, self.user.object_id ) + assert user.check_password( self.password ) + user2 = self.database.load( User, self.user2.object_id ) + assert user2.check_password( self.password2 ) + def test_reset_password_invalid_reset_id( self ): new_password = u"newpass" password_reset_id = u"invalid reset id" @@ -569,11 +552,14 @@ class Test_users( Test_controller ): ( self.user2.object_id, u"" ), ) ) - # check that neither user's password has changed - assert self.user.check_password( self.password ) - assert self.user2.check_password( self.password2 ) assert u"valid" in result[ "error" ] + # check that neither user's password has changed + user = self.database.load( User, self.user.object_id ) + assert user.check_password( self.password ) + user2 = self.database.load( User, self.user2.object_id ) + assert user2.check_password( self.password2 ) + def test_reset_password_expired( self ): Stub_smtp.reset() smtplib.SMTP = Stub_smtp @@ -588,15 +574,9 @@ class Test_users( Test_controller ): assert password_reset_id # to trigger expiration, pretend that the password reset was made 25 hours ago - def gen(): - self.database.load( password_reset_id, self.scheduler.thread ) - password_reset = ( yield Scheduler.SLEEP ) - password_reset._Persistent__revision = datetime.now() - timedelta( hours = 25 ) - self.database.save( password_reset ) - - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) + password_reset = self.database.load( Password_reset, password_reset_id ) + password_reset._Persistent__revision = datetime.now( tz = utc ) - timedelta( hours = 25 ) + self.database.save( password_reset ) new_password = u"newpass" result = self.http_post( "/users/reset_password", ( @@ -609,86 +589,16 @@ class Test_users( Test_controller ): ) ) # check that the password reset is not marked as redeemed - def gen(): - self.database.load( password_reset_id, self.scheduler.thread ) - password_reset = ( yield Scheduler.SLEEP ) - assert password_reset.redeemed == False + password_reset = self.database.load( Password_reset, password_reset_id ) + assert password_reset.redeemed == False - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) - - # check that neither user's password has changed - assert self.user.check_password( self.password ) - assert self.user2.check_password( self.password2 ) assert u"expired" in result[ "error" ] - def test_reset_password_expired( self ): - Stub_smtp.reset() - smtplib.SMTP = Stub_smtp - - self.http_post( "/users/send_reset", dict( - email_address = self.user.email_address, - send_reset_button = u"email me", - ) ) - - matches = self.RESET_LINK_PATTERN.search( smtplib.SMTP.message ) - password_reset_id = matches.group( 2 ) - assert password_reset_id - - def gen(): - self.database.load( password_reset_id, self.scheduler.thread ) - password_reset = ( yield Scheduler.SLEEP ) - password_reset.redeemed = True - - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) - - new_password = u"newpass" - result = self.http_post( "/users/reset_password", ( - ( u"password_reset_id", password_reset_id ), - ( u"reset_button", u"reset passwords" ), - ( self.user.object_id, new_password ), - ( self.user.object_id, new_password ), - ( self.user2.object_id, u"" ), - ( self.user2.object_id, u"" ), - ) ) - # check that neither user's password has changed - assert self.user.check_password( self.password ) - assert self.user2.check_password( self.password2 ) - assert u"already" in result[ "error" ] - - def test_reset_password_unknown_user_id( self ): - Stub_smtp.reset() - smtplib.SMTP = Stub_smtp - - self.http_post( "/users/send_reset", dict( - email_address = self.user.email_address, - send_reset_button = u"email me", - ) ) - - matches = self.RESET_LINK_PATTERN.search( smtplib.SMTP.message ) - password_reset_id = matches.group( 2 ) - assert password_reset_id - - new_password = u"newpass" - result = self.http_post( "/users/reset_password", ( - ( u"password_reset_id", password_reset_id ), - ( u"reset_button", u"reset passwords" ), - ( self.user.object_id, new_password ), - ( self.user.object_id, new_password ), - ( u"unknown", u"foo" ), - ( u"unknown", u"foo" ), - ( self.user2.object_id, u"" ), - ( self.user2.object_id, u"" ), - ) ) - - # check that neither user's password has changed - assert self.user.check_password( self.password ) - assert self.user2.check_password( self.password2 ) - assert result[ "error" ] + user = self.database.load( User, self.user.object_id ) + assert user.check_password( self.password ) + user2 = self.database.load( User, self.user2.object_id ) + assert user2.check_password( self.password2 ) def test_reset_password_non_matching( self ): Stub_smtp.reset() @@ -713,10 +623,13 @@ class Test_users( Test_controller ): ( self.user2.object_id, u"" ), ) ) + assert u"password" in result[ "error" ] + # check that neither user's password has changed - assert self.user.check_password( self.password ) - assert self.user2.check_password( self.password2 ) - assert result[ "error" ] + user = self.database.load( User, self.user.object_id ) + assert user.check_password( self.password ) + user2 = self.database.load( User, self.user2.object_id ) + assert user2.check_password( self.password2 ) def test_reset_password_blank( self ): Stub_smtp.reset() @@ -740,10 +653,11 @@ class Test_users( Test_controller ): ( self.user2.object_id, u"" ), ) ) + assert result[ "error" ] + # check that neither user's password has changed assert self.user.check_password( self.password ) assert self.user2.check_password( self.password2 ) - assert result[ "error" ] def test_reset_password_multiple_users( self ): Stub_smtp.reset() @@ -769,17 +683,14 @@ class Test_users( Test_controller ): ( self.user2.object_id, new_password2 ), ) ) - # check that the password reset is now marked as redeemed - def gen(): - self.database.load( password_reset_id, self.scheduler.thread ) - password_reset = ( yield Scheduler.SLEEP ) - assert password_reset.redeemed + assert result[ u"redirect" ] - g = gen() - self.scheduler.add( g ) - self.scheduler.wait_for( g ) + # check that the password reset is now marked as redeemed + password_reset = self.database.load( Password_reset, password_reset_id ) + assert password_reset.redeemed # check that the password was actually reset for both users - assert self.user.check_password( new_password ) - assert self.user2.check_password( new_password2 ) - assert result[ u"redirect" ] + user = self.database.load( User, self.user.object_id ) + assert user.check_password( new_password ) + user2 = self.database.load( User, self.user2.object_id ) + assert user2.check_password( new_password2 ) diff --git a/luminotes.py b/luminotes.py index 2da51a1..433a731 100644 --- a/luminotes.py +++ b/luminotes.py @@ -1,13 +1,11 @@ import cherrypy from controller.Database import Database from controller.Root import Root -from controller.Scheduler import Scheduler from config import Common def main( args ): - scheduler = Scheduler() - database = Database( scheduler, "data.db" ) + database = Database() cherrypy.config.update( Common.settings ) @@ -21,12 +19,9 @@ def main( args ): cherrypy.config.update( settings ) cherrypy.lowercase_api = True - root = Root( scheduler, database, cherrypy.config.configMap ) + root = Root( database, cherrypy.config.configMap ) cherrypy.root = root - if scheduler.shutdown not in cherrypy.server.on_stop_server_list: - cherrypy.server.on_stop_server_list.append( scheduler.shutdown ) - cherrypy.server.start() diff --git a/new_model/Note.py b/new_model/Note.py new file mode 100644 index 0000000..bf35210 --- /dev/null +++ b/new_model/Note.py @@ -0,0 +1,150 @@ +import re +from Persistent import Persistent, quote +from controller.Html_nuker import Html_nuker + + +class Note( Persistent ): + """ + An single textual wiki note. + """ + TITLE_PATTERN = re.compile( u"

(.*?)

", flags = re.IGNORECASE ) + + def __init__( self, object_id, revision = None, title = None, contents = None, notebook_id = None, + startup = None, deleted_from_id = None, rank = None ): + """ + Create a new note with the given id and contents. + + @type object_id: unicode + @param object_id: id of the note + @type revision: datetime or NoneType + @param revision: revision timestamp of the object (optional, defaults to now) + @type title: unicode or NoneType + @param title: textual title of the note (optional, defaults to derived from contents) + @type contents: unicode or NoneType + @param contents: HTML contents of the note (optional) + @type notebook_id: unicode or NoneType + @param notebook_id: id of notebook containing this note (optional) + @type startup: bool or NoneType + @param startup: whether this note should be displayed upon startup (optional, defaults to False) + @type deleted_from_id: unicode or NoneType + @param deleted_from_id: id of the notebook that this note was deleted from (optional) + @type rank: float or NoneType + @param rank: indicates numeric ordering of this note in relation to other startup notes + @rtype: Note + @return: newly constructed note + """ + Persistent.__init__( self, object_id, revision ) + self.__title = title + self.__contents = contents + self.__notebook_id = notebook_id + self.__startup = startup or False + self.__deleted_from_id = deleted_from_id + self.__rank = rank + + @staticmethod + def create( object_id, contents = None, notebook_id = None, startup = None, rank = None ): + """ + Convenience constructor for creating a new note. + + @type object_id: unicode + @param object_id: id of the note + @type contents: unicode or NoneType + @param contents: HTML contents of the note (optional) + @type notebook_id: unicode or NoneType + @param notebook_id: id of notebook containing this note (optional) + @type startup: bool or NoneType + @param startup: whether this note should be displayed upon startup (optional, defaults to False) + @type rank: float or NoneType + @param rank: indicates numeric ordering of this note in relation to other startup notes + @rtype: Note + @return: newly constructed note + """ + note = Note( object_id, notebook_id = notebook_id, startup = startup, rank = rank ) + note.contents = contents + + return note + + def __set_contents( self, contents ): + self.update_revision() + self.__contents = contents + + if contents is None: + self.__title = None + return + + # parse title out of the beginning of the contents + result = Note.TITLE_PATTERN.search( contents ) + + if result: + self.__title = result.groups()[ 0 ] + self.__title = Html_nuker( allow_refs = True ).nuke( self.__title ) + else: + self.__title = None + + def __set_notebook_id( self, notebook_id ): + self.__notebook_id = notebook_id + self.update_revision() + + def __set_startup( self, startup ): + self.__startup = startup + self.update_revision() + + def __set_deleted_from_id( self, deleted_from_id ): + self.__deleted_from_id = deleted_from_id + self.update_revision() + + def __set_rank( self, rank ): + self.__rank = rank + self.update_revision() + + @staticmethod + def sql_load( object_id, revision = None ): + if revision: + return "select * from note where id = %s and revision = %s;" % ( quote( object_id ), quote( revision ) ) + + return "select * from note_current where id = %s;" % quote( object_id ) + + @staticmethod + def sql_id_exists( object_id, revision = None ): + if revision: + return "select id from note where id = %s and revision = %s;" % ( quote( object_id ), quote( revision ) ) + + return "select id from note_current where id = %s;" % quote( object_id ) + + def sql_exists( self ): + return Note.sql_id_exists( self.object_id, self.revision ) + + def sql_create( self ): + rank = self.__rank + if rank is None: + rank = quote( None ) + + return \ + "insert into note ( id, revision, title, contents, notebook_id, startup, deleted_from_id, rank ) " + \ + "values ( %s, %s, %s, %s, %s, %s, %s, %s );" % \ + ( quote( self.object_id ), quote( self.revision ), quote( self.__title ), + quote( self.__contents ), quote( self.__notebook_id ), quote( self.__startup and 't' or 'f' ), + quote( self.__deleted_from_id ), rank ) + + def sql_update( self ): + return self.sql_create() + + def sql_load_revisions( self ): + return "select revision from note where id = %s order by revision;" % quote( self.object_id ) + + def to_dict( self ): + d = Persistent.to_dict( self ) + d.update( dict( + contents = self.__contents, + title = self.__title, + deleted_from_id = self.__deleted_from_id, + ) ) + + return d + + title = property( lambda self: self.__title ) + contents = property( lambda self: self.__contents, __set_contents ) + notebook_id = property( lambda self: self.__notebook_id, __set_notebook_id ) + startup = property( lambda self: self.__startup, __set_startup ) + deleted_from_id = property( lambda self: self.__deleted_from_id, __set_deleted_from_id ) + rank = property( lambda self: self.__rank, __set_rank ) diff --git a/new_model/Notebook.py b/new_model/Notebook.py new file mode 100644 index 0000000..1baaf2c --- /dev/null +++ b/new_model/Notebook.py @@ -0,0 +1,148 @@ +from copy import copy +from Note import Note +from Persistent import Persistent, quote + + +class Notebook( Persistent ): + """ + A collection of wiki notes. + """ + def __init__( self, object_id, revision = None, name = None, trash_id = None, read_write = True ): + """ + Create a new notebook with the given id and name. + + @type object_id: unicode + @param object_id: id of the notebook + @type revision: datetime or NoneType + @param revision: revision timestamp of the object (optional, defaults to now) + @type name: unicode or NoneType + @param name: name of this notebook (optional) + @type trash_id: Notebook or NoneType + @param trash_id: id of the notebook where deleted notes from this notebook go to die (optional) + @type read_write: bool or NoneType + @param read_write: whether this view of the notebook is currently read-write (optional, defaults to True) + @rtype: Notebook + @return: newly constructed notebook + """ + Persistent.__init__( self, object_id, revision ) + self.__name = name + self.__trash_id = trash_id + self.__read_write = read_write + + @staticmethod + def create( object_id, name = None, trash_id = None, read_write = True ): + """ + Convenience constructor for creating a new notebook. + + @type object_id: unicode + @param object_id: id of the notebook + @type name: unicode or NoneType + @param name: name of this notebook (optional) + @type trash_id: Notebook or NoneType + @param trash_id: id of the notebook where deleted notes from this notebook go to die (optional) + @type read_write: bool or NoneType + @param read_write: whether this view of the notebook is currently read-write (optional, defaults to True) + @rtype: Notebook + @return: newly constructed notebook + """ + return Notebook( object_id, name = name, trash_id = trash_id, read_write = read_write ) + + @staticmethod + def sql_load( object_id, revision = None ): + if revision: + return "select * from notebook where id = %s and revision = %s;" % ( quote( object_id ), quote( revision ) ) + + return "select * from notebook_current where id = %s;" % quote( object_id ) + + @staticmethod + def sql_id_exists( object_id, revision = None ): + if revision: + return "select id from notebook where id = %s and revision = %s;" % ( quote( object_id ), quote( revision ) ) + + return "select id from notebook_current where id = %s;" % quote( object_id ) + + def sql_exists( self ): + return Notebook.sql_id_exists( self.object_id, self.revision ) + + def sql_create( self ): + return \ + "insert into notebook ( id, revision, name, trash_id ) " + \ + "values ( %s, %s, %s, %s );" % \ + ( quote( self.object_id ), quote( self.revision ), quote( self.__name ), + quote( self.__trash_id ) ) + + def sql_update( self ): + return self.sql_create() + + def sql_load_notes( self ): + """ + Return a SQL string to load a list of all the notes within this notebook. + """ + return "select * from note_current where notebook_id = %s order by revision desc;" % quote( self.object_id ) + + def sql_load_non_startup_notes( self ): + """ + Return a SQL string to load a list of the non-startup notes within this notebook. + """ + return "select * from note_current where notebook_id = %s and startup = 'f' order by revision desc;" % quote( self.object_id ) + + def sql_load_startup_notes( self ): + """ + Return a SQL string to load a list of the startup notes within this notebook. + """ + return "select * from note_current where notebook_id = %s and startup = 't' order by rank;" % quote( self.object_id ) + + def sql_load_note_by_id( self, note_id ): + """ + Return a SQL string to load a particular note within this notebook by the note's id. + + @type note_id: unicode + @param note_id: id of note to load + """ + return "select * from note_current where notebook_id = %s and id = %s;" % ( quote( self.object_id ), quote( note_id ) ) + + def sql_load_note_by_title( self, title ): + """ + Return a SQL string to load a particular note within this notebook by the note's title. + + @type note_id: unicode + @param note_id: title of note to load + """ + return "select * from note_current where notebook_id = %s and title = %s;" % ( quote( self.object_id ), quote( title ) ) + + def sql_search_notes( self, search_text ): + """ + Return a SQL string to search for notes whose contents contain the given search_text. This + is a case-insensitive search. + + @type search_text: unicode + @param search_text: text to search for within the notes + """ + return \ + "select * from note_current where notebook_id = %s and contents ilike %s;" % \ + ( quote( self.object_id ), quote( "%" + search_text + "%" ) ) + + def sql_highest_rank( self ): + return "select coalesce( max( rank ), -1 ) from note_current where notebook_id = %s;" % quote( self.object_id ) + + def to_dict( self ): + d = Persistent.to_dict( self ) + + d.update( dict( + name = self.__name, + trash_id = self.__trash_id, + read_write = self.__read_write, + ) ) + + return d + + def __set_name( self, name ): + self.__name = name + self.update_revision() + + def __set_read_write( self, read_write ): + self.__read_write = read_write + + name = property( lambda self: self.__name, __set_name ) + trash_id = property( lambda self: self.__trash_id ) + read_write = property( lambda self: self.__read_write, __set_read_write ) diff --git a/new_model/Password_reset.py b/new_model/Password_reset.py new file mode 100644 index 0000000..daf4083 --- /dev/null +++ b/new_model/Password_reset.py @@ -0,0 +1,57 @@ +from Persistent import Persistent, quote + + +class Password_reset( Persistent ): + """ + A request for a password reset. + """ + def __init__( self, object_id, email_address, redeemed = False ): + """ + Create a password reset request with the given id. + + @type object_id: unicode + @param object_id: id of the password reset + @type email_address: unicode + @param email_address: where the reset confirmation was emailed + @type redeemed: bool or NoneType + @param redeemed: whether this password reset has been redeemed yet (optional, defaults to False) + @rtype: Password_reset + @return: newly constructed password reset + """ + Persistent.__init__( self, object_id ) + self.__email_address = email_address + self.__redeemed = redeemed + + @staticmethod + def sql_load( object_id, revision = None ): + # password resets don't track revisions + if revision: + raise NotImplementedError() + + return "select * from password_reset where id = %s;" % quote( object_id ) + + @staticmethod + def sql_id_exists( object_id, revision = None ): + if revision: + raise NotImplementedError() + + return "select id from password_reset where id = %s;" % quote( object_id ) + + def sql_exists( self ): + return Password_reset.sql_id_exists( self.object_id ) + + def sql_create( self ): + return "insert into password_reset ( id, email_address, redeemed ) values ( %s, %s, %s );" % \ + ( quote( self.object_id ), quote( self.__email_address ), quote( self.__redeemed and "t" or "f" ) ) + + def sql_update( self ): + return "update password_reset set redeemed = %s where id = %s;" % \ + ( quote( self.__redeemed and "t" or "f" ), quote( self.object_id ) ) + + def __set_redeemed( self, redeemed ): + if redeemed != self.__redeemed: + self.update_revision() + self.__redeemed = redeemed + + email_address = property( lambda self: self.__email_address ) + redeemed = property( lambda self: self.__redeemed, __set_redeemed ) diff --git a/new_model/Persistent.py b/new_model/Persistent.py new file mode 100644 index 0000000..78f6503 --- /dev/null +++ b/new_model/Persistent.py @@ -0,0 +1,82 @@ +from datetime import datetime +from pytz import utc + + +class Persistent( object ): + """ + A persistent database object with a unique id. + """ + def __init__( self, object_id, revision = None ): + self.__object_id = object_id + self.__revision = revision + + if not revision: + self.update_revision() + + @staticmethod + def sql_load( object_id, revision = None ): + """ + Return a SQL string to load an object with the given information from the database. If a + revision is not provided, then the most current version of the given object will be loaded. + + @type object_id: unicode + @param object_id: id of object to load + @type revision: unicode or NoneType + @param revision: revision of the object to load (optional) + """ + raise NotImplementedError() + + @staticmethod + def sql_id_exists( object_id, revision = None ): + """ + Return a SQL string to determine whether the given object is present in the database. If a + revision is not provided, then the most current version of the given object will be used. + + @type object_id: unicode + @param object_id: id of object to check for existence + @type revision: unicode or NoneType + @param revision: revision of the object to check (optional) + """ + raise NotImplementedError() + + def sql_exists( self ): + """ + Return a SQL string to determine whether the current revision of this object is present in the + database. + """ + raise NotImplementedError() + + def sql_create( self ): + """ + Return a SQL string to save this object to the database for the first time. This should be in + the form of a SQL insert. + """ + raise NotImplementedError() + + def sql_update( self ): + """ + Return a SQL string to save an updated revision of this object to the database. Note that, + because of the retention of old row revisions in the database, this SQL string will usually + be in the form of an insert rather than an update to an existing row. + """ + raise NotImplementedError() + + def to_dict( self ): + return dict( + object_id = self.__object_id, + revision = self.__revision, + ) + + def update_revision( self ): + self.__revision = datetime.now( tz = utc ) + + object_id = property( lambda self: self.__object_id ) + revision = property( lambda self: self.__revision ) + + +def quote( value ): + if value is None: + return "null" + + value = unicode( value ) + return "'%s'" % value.replace( "'", "''" ).replace( "\\", "\\\\" ) diff --git a/new_model/User.py b/new_model/User.py new file mode 100644 index 0000000..1664819 --- /dev/null +++ b/new_model/User.py @@ -0,0 +1,212 @@ +import sha +import random +from copy import copy +from Persistent import Persistent, quote + + +class User( Persistent ): + """ + A Luminotes user. + """ + SALT_CHARS = [ chr( c ) for c in range( ord( "!" ), ord( "~" ) + 1 ) ] + SALT_SIZE = 12 + + def __init__( self, object_id, revision = None, username = None, salt = None, password_hash = None, + email_address = None, storage_bytes = None, rate_plan = None ): + """ + Create a new user with the given credentials and information. + + @type object_id: unicode + @param object_id: id of the user + @type revision: datetime or NoneType + @param revision: revision timestamp of the object (optional, defaults to now) + @type username: unicode or NoneType + @param username: unique user identifier for login purposes (optional) + @type salt: unicode or NoneType + @param salt: salt to use when hashing the password (optional, defaults to random) + @type password_hash: unicode or NoneType + @param password_hash: cryptographic hash of secret password for login purposes (optional) + @type email_address: unicode or NoneType + @param email_address: a hopefully valid email address (optional) + @type storage_bytes: int or NoneType + @param storage_bytes: count of bytes that the user is currently using for storage (optional) + @type rate_plan: int or NoneType + @param rate_plan: index into the rate plan array in config/Common.py (optional, defaults to 0) + @rtype: User + @return: newly created user + """ + Persistent.__init__( self, object_id, revision ) + self.__username = username + self.__salt = salt + self.__password_hash = password_hash + self.__email_address = email_address + self.__storage_bytes = storage_bytes or 0 + self.__rate_plan = rate_plan or 0 + + @staticmethod + def create( object_id, username = None, password = None, email_address = None ): + """ + Convenience constructor for creating a new user. + + @type object_id: unicode + @param object_id: id of the user + @type username: unicode or NoneType + @param username: unique user identifier for login purposes (optional) + @type password: unicode or NoneType + @param password: secret password for login purposes (optional) + @type email_address: unicode or NoneType + @param email_address: a hopefully valid email address (optional) + @rtype: User + @return: newly created user + """ + salt = User.__create_salt() + password_hash = User.__hash_password( salt, password ) + + return User( object_id, None, username, salt, password_hash, email_address ) + + @staticmethod + def __create_salt(): + return "".join( [ random.choice( User.SALT_CHARS ) for i in range( User.SALT_SIZE ) ] ) + + @staticmethod + def __hash_password( salt, password ): + if password is None or len( password ) == 0: + return None + + return sha.new( salt + password ).hexdigest() + + def check_password( self, password ): + """ + Check that the given password matches this user's password. + + @type password: unicode + @param password: password to check + @rtype: bool + @return: True if the password matches + """ + if self.__password_hash == None: + return False + + hash = User.__hash_password( self.__salt, password ) + if hash == self.__password_hash: + return True + + return False + + @staticmethod + def sql_load( object_id, revision = None ): + if revision: + return "select * from luminotes_user where id = %s and revision = %s;" % ( quote( object_id ), quote( revision ) ) + + return "select * from luminotes_user_current where id = %s;" % quote( object_id ) + + @staticmethod + def sql_id_exists( object_id, revision = None ): + if revision: + return "select id from luminotes_user where id = %s and revision = %s;" % ( quote( object_id ), quote( revision ) ) + + return "select id from luminotes_user_current where id = %s;" % quote( object_id ) + + def sql_exists( self ): + return User.sql_id_exists( self.object_id, self.revision ) + + def sql_create( self ): + return \ + "insert into luminotes_user ( id, revision, username, salt, password_hash, email_address, storage_bytes, rate_plan ) " + \ + "values ( %s, %s, %s, %s, %s, %s, %s, %s );" % \ + ( quote( self.object_id ), quote( self.revision ), quote( self.__username ), + quote( self.__salt ), quote( self.__password_hash ), quote( self.__email_address ), + self.__storage_bytes, self.__rate_plan ) + + def sql_update( self ): + return self.sql_create() + + @staticmethod + def sql_load_by_username( username ): + return "select * from luminotes_user_current where username = %s;" % quote( username ) + + @staticmethod + def sql_load_by_email_address( email_address ): + return "select * from luminotes_user_current where username = %s;" % quote( email_address ) + + def sql_load_notebooks( self, parents_only = False ): + """ + Return a SQL string to load a list of the notebooks to which this user has access. + """ + if parents_only: + parents_only_clause = " and trash_id is not null"; + else: + parents_only_clause = "" + + return \ + "select notebook_current.*, user_notebook.read_write from user_notebook, notebook_current " + \ + "where user_id = %s%s and user_notebook.notebook_id = notebook_current.id order by revision;" % \ + ( quote( self.object_id ), parents_only_clause ) + + def sql_save_notebook( self, notebook_id, read_write = True ): + """ + Return a SQL string to save the id of a notebook to which this user has access. + """ + return \ + "insert into user_notebook ( user_id, notebook_id, read_write ) values " + \ + "( %s, %s, %s );" % ( quote( self.object_id ), quote( notebook_id ), quote( read_write and 't' or 'f' ) ) + + def sql_has_access( self, notebook_id, read_write = False ): + """ + Return a SQL string to determine whether this user has access to the given notebook. + """ + if read_write is True: + return \ + "select user_id from user_notebook where user_id = %s and notebook_id = %s and read_write = 't';" % \ + ( quote( self.object_id ), quote( notebook_id ) ) + else: + return \ + "select user_id from user_notebook where user_id = %s and notebook_id = %s;" % \ + ( quote( self.object_id ), quote( notebook_id ) ) + + def sql_calculate_storage( self ): + """ + Return a SQL string to calculate the total bytes of storage usage by this user. Note that this + only includes storage for all the user's notes and past revisions. It doesn't include storage + for the notebooks themselves. + """ + return \ + """ + select + coalesce( sum( pg_column_size( note.* ) ), 0 ) + from + luminotes_user_current, user_notebook, note + where + luminotes_user_current.id = %s and + user_notebook.user_id = luminotes_user_current.id and + note.notebook_id = user_notebook.notebook_id; + """ % quote( self.object_id ) + + def to_dict( self ): + d = Persistent.to_dict( self ) + d.update( dict( + username = self.username, + storage_bytes = self.__storage_bytes, + rate_plan = self.__rate_plan, + ) ) + + return d + + def __set_password( self, password ): + self.update_revision() + self.__salt = User.__create_salt() + self.__password_hash = User.__hash_password( self.__salt, password ) + + def __set_storage_bytes( self, storage_bytes ): + self.update_revision() + self.__storage_bytes = storage_bytes + + def __set_rate_plan( self, rate_plan ): + self.update_revision() + self.__rate_plan = rate_plan + + username = property( lambda self: self.__username ) + email_address = property( lambda self: self.__email_address ) + password = property( None, __set_password ) + storage_bytes = property( lambda self: self.__storage_bytes, __set_storage_bytes ) + rate_plan = property( lambda self: self.__rate_plan, __set_rate_plan ) diff --git a/new_model/__init__.py b/new_model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/new_model/drop.sql b/new_model/drop.sql new file mode 100644 index 0000000..4998536 --- /dev/null +++ b/new_model/drop.sql @@ -0,0 +1,8 @@ +DROP VIEW luminotes_user_current; +DROP TABLE luminotes_user; +DROP VIEW note_current; +DROP TABLE note; +DROP VIEW notebook_current; +DROP TABLE notebook; +DROP TABLE password_reset; +DROP TABLE user_notebook; diff --git a/model/schema.sql b/new_model/schema.sql similarity index 97% rename from model/schema.sql rename to new_model/schema.sql index 3db16b6..8c3241c 100644 --- a/model/schema.sql +++ b/new_model/schema.sql @@ -6,13 +6,6 @@ SET client_encoding = 'UTF8'; SET check_function_bodies = false; SET client_min_messages = warning; --- --- Name: SCHEMA public; Type: COMMENT; Schema: -; Owner: postgres --- - -COMMENT ON SCHEMA public IS 'Standard public schema'; - - SET search_path = public, pg_catalog; SET default_tablespace = ''; diff --git a/new_model/test/Test_note.py b/new_model/test/Test_note.py new file mode 100644 index 0000000..172635a --- /dev/null +++ b/new_model/test/Test_note.py @@ -0,0 +1,133 @@ +from pytz import utc +from datetime import datetime, timedelta +from new_model.Note import Note + + +class Test_note( object ): + def setUp( self ): + self.object_id = u"17" + self.title = u"title goes here" + self.contents = u"

%s

blah" % self.title + self.notebook_id = u"18" + self.startup = False + self.rank = 17.5 + self.delta = timedelta( seconds = 1 ) + + self.note = Note.create( self.object_id, self.contents, self.notebook_id, self.startup, self.rank ) + + def test_create( self ): + assert self.note.object_id == self.object_id + assert datetime.now( tz = utc ) - self.note.revision < self.delta + assert self.note.contents == self.contents + assert self.note.title == self.title + assert self.note.notebook_id == self.notebook_id + assert self.note.startup == self.startup + assert self.note.deleted_from_id == None + assert self.note.rank == self.rank + + def test_set_contents( self ): + new_title = u"new title" + new_contents = u"

%s

new blah" % new_title + previous_revision = self.note.revision + + self.note.contents = new_contents + + assert self.note.revision > previous_revision + assert self.note.contents == new_contents + assert self.note.title == new_title + assert self.note.notebook_id == self.notebook_id + assert self.note.startup == self.startup + assert self.note.deleted_from_id == None + assert self.note.rank == self.rank + + def test_set_contents_with_html_title( self ): + new_title = u"new title" + new_contents = u"

new
title

new blah" + previous_revision = self.note.revision + + self.note.contents = new_contents + + # html should be stripped out of the title + assert self.note.revision > previous_revision + assert self.note.contents == new_contents + assert self.note.title == new_title + assert self.note.notebook_id == self.notebook_id + assert self.note.startup == self.startup + assert self.note.deleted_from_id == None + assert self.note.rank == self.rank + + def test_set_contents_with_multiple_titles( self ): + new_title = u"new title" + new_contents = u"

new
title

new blah

other title

hmm" + previous_revision = self.note.revision + + self.note.contents = new_contents + + # should only use the first title + assert self.note.revision > previous_revision + assert self.note.contents == new_contents + assert self.note.title == new_title + assert self.note.notebook_id == self.notebook_id + assert self.note.startup == self.startup + assert self.note.deleted_from_id == None + assert self.note.rank == self.rank + + def test_set_notebook_id( self ): + previous_revision = self.note.revision + self.note.notebook_id = u"54" + + assert self.note.revision > previous_revision + assert self.note.notebook_id == u"54" + + def test_set_startup( self ): + previous_revision = self.note.revision + self.note.startup = True + + assert self.note.revision > previous_revision + assert self.note.startup == True + + def test_set_deleted_from_id( self ): + previous_revision = self.note.revision + self.note.deleted_from_id = u"55" + + assert self.note.revision > previous_revision + assert self.note.deleted_from_id == u"55" + + def test_set_rank( self ): + previous_revision = self.note.revision + self.note.rank = 5 + + assert self.note.revision > previous_revision + assert self.note.rank == 5 + + def test_to_dict( self ): + d = self.note.to_dict() + + assert d.get( "object_id" ) == self.note.object_id + assert datetime.now( tz = utc ) - d.get( "revision" ) < self.delta + assert d.get( "contents" ) == self.contents + assert d.get( "title" ) == self.title + assert d.get( "deleted_from_id" ) == None + + +class Test_note_blank( Test_note ): + def setUp( self ): + self.object_id = u"17" + self.title = None + self.contents = None + self.notebook_id = None + self.startup = False + self.rank = None + self.delta = timedelta( seconds = 1 ) + + self.note = Note.create( self.object_id ) + + def test_create( self ): + assert self.note.object_id == self.object_id + assert datetime.now( tz = utc ) - self.note.revision < self.delta + assert self.note.contents == None + assert self.note.title == None + assert self.note.notebook_id == None + assert self.note.startup == False + assert self.note.deleted_from_id == None + assert self.note.rank == None diff --git a/new_model/test/Test_notebook.py b/new_model/test/Test_notebook.py new file mode 100644 index 0000000..e6de2eb --- /dev/null +++ b/new_model/test/Test_notebook.py @@ -0,0 +1,54 @@ +from pytz import utc +from datetime import datetime, timedelta +from new_model.Notebook import Notebook +from new_model.Note import Note + + +class Test_notebook( object ): + def setUp( self ): + self.object_id = "17" + self.trash_id = "18" + self.name = u"my notebook" + self.trash_name = u"trash" + self.delta = timedelta( seconds = 1 ) + + self.trash = Notebook.create( self.trash_id, self.trash_name, read_write = False ) + self.notebook = Notebook.create( self.object_id, self.name, trash_id = self.trash.object_id ) + self.note = Note.create( "19", u"

title

blah" ) + + def test_create( self ): + assert self.notebook.object_id == self.object_id + assert datetime.now( tz = utc ) - self.notebook.revision < self.delta + assert self.notebook.name == self.name + assert self.notebook.read_write == True + assert self.notebook.trash_id == self.trash_id + + assert self.trash.object_id == self.trash_id + assert datetime.now( tz = utc ) - self.trash.revision < self.delta + assert self.trash.name == self.trash_name + assert self.trash.read_write == False + assert self.trash.trash_id == None + + def test_set_name( self ): + new_name = u"my new notebook" + previous_revision = self.notebook.revision + self.notebook.name = new_name + + assert self.notebook.name == new_name + assert self.notebook.revision > previous_revision + + def test_set_read_write( self ): + original_revision = self.notebook.revision + self.notebook.read_write = True + + assert self.notebook.read_write == True + assert self.notebook.revision == original_revision + + def test_to_dict( self ): + d = self.notebook.to_dict() + + assert d.get( "name" ) == self.name + assert d.get( "trash_id" ) == self.trash.object_id + assert d.get( "read_write" ) == True + assert d.get( "object_id" ) == self.notebook.object_id + assert datetime.now( tz = utc ) - d.get( "revision" ) < self.delta diff --git a/new_model/test/Test_password_reset.py b/new_model/test/Test_password_reset.py new file mode 100644 index 0000000..da0c6bb --- /dev/null +++ b/new_model/test/Test_password_reset.py @@ -0,0 +1,38 @@ +from model.User import User +from new_model.Password_reset import Password_reset + + +class Test_password_reset( object ): + def setUp( self ): + self.object_id = u"17" + self.email_address = u"bob@example.com" + + self.password_reset = Password_reset( self.object_id, self.email_address ) + + def test_create( self ): + assert self.password_reset.object_id == self.object_id + assert self.password_reset.email_address == self.email_address + assert self.password_reset.redeemed == False + + def test_redeem( self ): + previous_revision = self.password_reset.revision + self.password_reset.redeemed = True + + assert self.password_reset.redeemed == True + assert self.password_reset.revision > previous_revision + + def test_redeem_twice( self ): + self.password_reset.redeemed = True + current_revision = self.password_reset.revision + self.password_reset.redeemed = True + + assert self.password_reset.redeemed == True + assert self.password_reset.revision == current_revision + + def test_unredeem( self ): + self.password_reset.redeemed = True + previous_revision = self.password_reset.revision + self.password_reset.redeemed = False + + assert self.password_reset.redeemed == False + assert self.password_reset.revision > previous_revision diff --git a/new_model/test/Test_persistent.py b/new_model/test/Test_persistent.py new file mode 100644 index 0000000..49f2a54 --- /dev/null +++ b/new_model/test/Test_persistent.py @@ -0,0 +1,73 @@ +from pytz import utc +from datetime import datetime, timedelta +from new_model.Persistent import Persistent, quote + + +class Test_persistent( object ): + def setUp( self ): + self.object_id = "17" + self.obj = Persistent( self.object_id ) + self.delta = timedelta( seconds = 1 ) + + def test_create( self ): + assert self.obj.object_id == self.object_id + assert datetime.now( tz = utc ) - self.obj.revision < self.delta + + def test_update_revision( self ): + previous_revision = self.obj.revision + self.obj.update_revision() + assert self.obj.revision > previous_revision + assert datetime.now( tz = utc ) - self.obj.revision < self.delta + + previous_revision = self.obj.revision + self.obj.update_revision() + assert self.obj.revision > previous_revision + assert datetime.now( tz = utc ) - self.obj.revision < self.delta + + def test_to_dict( self ): + d = self.obj.to_dict() + + assert d.get( "object_id" ) == self.object_id + assert d.get( "revision" ) == self.obj.revision + + +class Test_persistent_with_revision( object ): + def setUp( self ): + self.object_id = "17" + self.revision = datetime.now( tz = utc ) - timedelta( hours = 24 ) + self.obj = Persistent( self.object_id, self.revision ) + self.delta = timedelta( seconds = 1 ) + + def test_create( self ): + assert self.obj.object_id == self.object_id + assert self.revision - self.obj.revision < self.delta + + def test_update_revision( self ): + previous_revision = self.obj.revision + self.obj.update_revision() + assert self.obj.revision > previous_revision + assert datetime.now( tz = utc ) - self.obj.revision < self.delta + + previous_revision = self.obj.revision + self.obj.update_revision() + assert self.obj.revision > previous_revision + assert datetime.now( tz = utc ) - self.obj.revision < self.delta + + def test_to_dict( self ): + d = self.obj.to_dict() + + assert d.get( "object_id" ) == self.object_id + assert d.get( "revision" ) == self.obj.revision + + +def test_quote(): + assert "'foo'" == quote( "foo" ) + +def test_quote_apostrophe(): + assert "'it''s'" == quote( "it's" ) + +def test_quote_backslash(): + assert r"'c:\\\\whee'" == quote( r"c:\\whee" ) + +def test_quote_none(): + assert "null" == quote( None ) diff --git a/new_model/test/Test_user.py b/new_model/test/Test_user.py new file mode 100644 index 0000000..3ba8fa9 --- /dev/null +++ b/new_model/test/Test_user.py @@ -0,0 +1,69 @@ +from pytz import utc +from datetime import datetime, timedelta +from new_model.User import User + + +class Test_user( object ): + def setUp( self ): + self.object_id = u"17" + self.username = u"bob" + self.password = u"foobar" + self.email_address = u"bob@example.com" + self.delta = timedelta( seconds = 1 ) + + self.user = User.create( self.object_id, self.username, self.password, self.email_address ) + + def test_create( self ): + assert self.user.object_id == self.object_id + assert datetime.now( tz = utc ) - self.user.revision < self.delta + assert self.user.username == self.username + assert self.user.email_address == self.email_address + assert self.user.storage_bytes == 0 + assert self.user.rate_plan == 0 + + def test_check_correct_password( self ): + assert self.user.check_password( self.password ) == True + + def test_check_incorrect_password( self ): + assert self.user.check_password( u"wrong" ) == False + + def test_set_password( self ): + previous_revision = self.user.revision + new_password = u"newpass" + self.user.password = new_password + + assert self.user.check_password( self.password ) == False + assert self.user.check_password( new_password ) == True + assert self.user.revision > previous_revision + + def test_set_none_password( self ): + previous_revision = self.user.revision + new_password = None + self.user.password = new_password + + assert self.user.check_password( self.password ) == False + assert self.user.check_password( new_password ) == False + assert self.user.revision > previous_revision + + def test_set_storage_bytes( self ): + previous_revision = self.user.revision + storage_bytes = 44 + self.user.storage_bytes = storage_bytes + + assert self.user.storage_bytes == storage_bytes + assert self.user.revision > previous_revision + + def test_set_rate_plan( self ): + previous_revision = self.user.revision + rate_plan = 2 + self.user.rate_plan = rate_plan + + assert self.user.rate_plan == rate_plan + assert self.user.revision > previous_revision + + def test_to_dict( self ): + d = self.user.to_dict() + + assert d.get( "username" ) == self.username + assert d.get( "storage_bytes" ) == self.user.storage_bytes + assert d.get( "rate_plan" ) == self.user.rate_plan diff --git a/static/js/Editor.js b/static/js/Editor.js index a20855a..2f08f9d 100644 --- a/static/js/Editor.js +++ b/static/js/Editor.js @@ -1,9 +1,10 @@ -function Editor( id, notebook_id, note_text, deleted_from, revisions_list, read_write, startup, highlight, focus ) { +function Editor( id, notebook_id, note_text, deleted_from_id, revision, read_write, startup, highlight, focus ) { this.id = id; this.notebook_id = notebook_id; this.initial_text = note_text; - this.deleted_from = deleted_from || null; - this.revisions_list = revisions_list || new Array(); + this.deleted_from_id = deleted_from_id || null; + this.revision = revision; + this.revisions_list = new Array(); // cache for this note's list of revisions, loaded from the server on-demand this.read_write = read_write; this.startup = startup || false; // whether this Editor is for a startup note this.init_highlight = highlight || false; @@ -31,7 +32,7 @@ function Editor( id, notebook_id, note_text, deleted_from, revisions_list, read_ "type": "button", "class": "note_button", "id": "delete_" + iframe_id, - "value": "delete" + ( this.deleted_from ? " forever" : "" ), + "value": "delete" + ( this.deleted_from_id ? " forever" : "" ), "title": "delete note [ctrl-d]" } ); connect( this.delete_button, "onclick", function ( event ) { signal( self, "delete_clicked", event ); } ); @@ -45,7 +46,7 @@ function Editor( id, notebook_id, note_text, deleted_from, revisions_list, read_ } ); connect( this.changes_button, "onclick", function ( event ) { signal( self, "changes_clicked", event ); } ); - if ( this.deleted_from ) { + if ( this.deleted_from_id ) { this.undelete_button = createDOM( "input", { "type": "button", "class": "note_button", @@ -66,7 +67,7 @@ function Editor( id, notebook_id, note_text, deleted_from, revisions_list, read_ } } - if ( !this.deleted_from && ( read_write || !startup ) ) { + if ( !this.deleted_from_id && ( read_write || !startup ) ) { this.hide_button = createDOM( "input", { "type": "button", "class": "note_button", diff --git a/static/js/Wiki.js b/static/js/Wiki.js index 7c115e5..76a2c9a 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -72,7 +72,7 @@ Wiki.prototype.display_user = function ( result ) { for ( var i in result.notebooks ) { var notebook = result.notebooks[ i ]; - if ( notebook.name == "Luminotes" ) + if ( notebook.name == "Luminotes" || notebook.name == "trash" ) continue; var div_class = "link_area_item"; @@ -162,18 +162,20 @@ Wiki.prototype.populate = function ( result ) { createDOM( "a", { "href": location.href, "id": "all_notes_link", "title": "View a list of all notes in this notebook." }, "all notes" ) ) ); } - appendChildNodes( span, createDOM( "div", { "class": "link_area_item" }, - createDOM( "a", { "href": "/notebooks/download_html/" + this.notebook.object_id, "id": "download_html_link", "title": "Download a stand-alone copy of the entire wiki notebook." }, "download as html" ) - ) ); + if ( this.notebook.name != "Luminotes" ) { + appendChildNodes( span, createDOM( "div", { "class": "link_area_item" }, + createDOM( "a", { "href": "/notebooks/download_html/" + this.notebook.object_id, "id": "download_html_link", "title": "Download a stand-alone copy of the entire wiki notebook." }, "download as html" ) + ) ); + } if ( this.notebook.read_write ) { this.read_write = true; removeElementClass( "toolbar", "undisplayed" ); - if ( this.notebook.trash ) { + if ( this.notebook.trash_id ) { appendChildNodes( span, createDOM( "div", { "class": "link_area_item" }, createDOM( "a", { - "href": "/notebooks/" + this.notebook.trash.object_id + "?parent_id=" + this.notebook.object_id, + "href": "/notebooks/" + this.notebook.trash_id + "?parent_id=" + this.notebook.object_id, "id": "trash_link", "title": "Look here for notes you've deleted." }, "trash" ) @@ -221,9 +223,11 @@ Wiki.prototype.populate = function ( result ) { } ); } - connect( "download_html_link", "onclick", function ( event ) { - self.save_editor( null, true ); - } ); + if ( this.notebook.name != "Luminotes" ) { + connect( "download_html_link", "onclick", function ( event ) { + self.save_editor( null, true ); + } ); + } // create an editor for each startup note in the received notebook, focusing the first one var focus = true; @@ -234,7 +238,7 @@ Wiki.prototype.populate = function ( result ) { // don't actually create an editor if a particular note was provided in the result if ( !result.note ) { - var editor = this.create_editor( note.object_id, note.contents, note.deleted_from, note.revisions_list, undefined, this.read_write, false, focus ); + var editor = this.create_editor( note.object_id, note.contents, note.deleted_from_id, note.revision, this.read_write, false, focus ); this.open_editors[ note.title ] = editor; focus = false; } @@ -242,14 +246,16 @@ Wiki.prototype.populate = function ( result ) { // if one particular note was provided, then just display an editor for that note var read_write = this.read_write; - if ( getElement( "revision" ).value ) read_write = false; + var revision_element = getElement( "revision" ); + if ( revision_element && revision_element.value ) read_write = false; + if ( result.note ) this.create_editor( result.note.object_id, result.note.contents || getElement( "note_contents" ).value, - result.note.deleted_from, - result.note.revisions_list, - undefined, read_write, false, true + result.note.deleted_from_id, + result.note.revision, + read_write, false, true ); if ( result.startup_notes.length == 0 && !result.note ) @@ -282,7 +288,7 @@ Wiki.prototype.create_blank_editor = function ( event ) { } } - var editor = this.create_editor( undefined, undefined, undefined, undefined, undefined, this.read_write, true, true ); + var editor = this.create_editor( undefined, undefined, undefined, undefined, this.read_write, true, true ); this.blank_editor_id = editor.id; } @@ -448,26 +454,27 @@ Wiki.prototype.resolve_link = function ( note_title, link, callback ) { ); } -Wiki.prototype.parse_loaded_editor = function ( result, note_title, revision, link ) { +Wiki.prototype.parse_loaded_editor = function ( result, note_title, requested_revision, link ) { if ( result.note ) { var id = result.note.object_id; - if ( revision ) id += " " + revision; + if ( requested_revision ) + id += " " + requested_revision; + var actual_revision = result.note.revision; var note_text = result.note.contents; - var deleted_from = result.note.deleted; - var revisions_list = result.note.revisions_list; + var deleted_from_id = result.note.deleted; } else { var id = null; var note_text = "

" + note_title; - var deleted_from = null; - var revisions_list = new Array(); + var deleted_from_id = null; + var actual_revision = null; } - if ( revision ) + if ( requested_revision ) var read_write = false; // show previous revisions as read-only else var read_write = this.read_write; - var editor = this.create_editor( id, note_text, deleted_from, revisions_list, note_title, read_write, true, false ); + var editor = this.create_editor( id, note_text, deleted_from_id, actual_revision, read_write, true, false ); id = editor.id; // if a link that launched this editor was provided, update it with the created note's id @@ -475,7 +482,7 @@ Wiki.prototype.parse_loaded_editor = function ( result, note_title, revision, li link.href = "/notebooks/" + this.notebook_id + "?note_id=" + id; } -Wiki.prototype.create_editor = function ( id, note_text, deleted_from, revisions_list, note_title, read_write, highlight, focus ) { +Wiki.prototype.create_editor = function ( id, note_text, deleted_from_id, revision, read_write, highlight, focus ) { var self = this; if ( isUndefinedOrNull( id ) ) { if ( this.read_write ) { @@ -489,13 +496,13 @@ Wiki.prototype.create_editor = function ( id, note_text, deleted_from, revisions } // for read-only notes within read-write notebooks, tack the revision timestamp onto the start of the note text - if ( !read_write && this.read_write && revisions_list && revisions_list.length ) { - var short_revision = this.brief_revision( revisions_list[ revisions_list.length - 1 ] ); + if ( !read_write && this.read_write && revision ) { + var short_revision = this.brief_revision( revision ); note_text = "

Previous revision from " + short_revision + "

" + note_text; } var startup = this.startup_notes[ id ]; - var editor = new Editor( id, this.notebook_id, note_text, deleted_from, revisions_list, read_write, startup, highlight, focus ); + var editor = new Editor( id, this.notebook_id, note_text, deleted_from_id, revision, read_write, startup, highlight, focus ); if ( this.read_write ) { connect( editor, "state_changed", this, "editor_state_changed" ); @@ -613,7 +620,7 @@ Wiki.prototype.editor_key_pressed = function ( editor, event ) { this.create_blank_editor( event ); // ctrl-h: hide note } else if ( code == 72 ) { - if ( !editor.deleted_from ) + if ( !editor.deleted_from_id ) this.hide_editor( event ); // ctrl-d: delete note } else if ( code == 68 ) { @@ -727,7 +734,7 @@ Wiki.prototype.delete_editor = function ( event, editor ) { if ( editor == this.focused_editor ) this.focused_editor = null; - if ( this.notebook.trash && !editor.empty() ) { + if ( this.notebook.trash_id && !editor.empty() ) { var undo_button = createDOM( "input", { "type": "button", "class": "message_button", @@ -735,7 +742,7 @@ Wiki.prototype.delete_editor = function ( event, editor ) { "title": "undo deletion" } ); var trash_link = createDOM( "a", { - "href": "/notebooks/" + this.notebook.trash.object_id + "?parent_id=" + this.notebook.object_id + "href": "/notebooks/" + this.notebook.trash_id + "?parent_id=" + this.notebook.object_id }, "trash" ); this.display_message( 'The note has been moved to the', [ trash_link, ". ", undo_button ] ) var self = this; @@ -767,7 +774,7 @@ Wiki.prototype.undelete_editor_via_trash = function ( event, editor ) { if ( this.read_write && editor.read_write ) { var self = this; this.invoker.invoke( "/notebooks/undelete_note", "POST", { - "notebook_id": editor.deleted_from, + "notebook_id": editor.deleted_from_id, "note_id": editor.id }, function ( result ) { self.display_storage_usage( result.storage_bytes ); } ); } @@ -816,13 +823,12 @@ Wiki.prototype.save_editor = function ( editor, fire_and_forget ) { var self = this; if ( editor && editor.read_write && !editor.empty() ) { - var revisions = editor.revisions_list; this.invoker.invoke( "/notebooks/save_note", "POST", { "notebook_id": this.notebook_id, "note_id": editor.id, "contents": editor.contents(), "startup": editor.startup, - "previous_revision": revisions.length ? revisions[ revisions.length - 1 ] : "None" + "previous_revision": editor.revision ? editor.revision : "None" }, function ( result ) { self.update_editor_revisions( result, editor ); self.display_storage_usage( result.storage_bytes ); @@ -835,8 +841,8 @@ Wiki.prototype.update_editor_revisions = function ( result, editor ) { if ( !result.new_revision ) return; - var revisions = editor.revisions_list; - var client_previous_revision = revisions.length ? revisions[ revisions.length - 1 ] : null; + var client_previous_revision = editor.revision; + editor.revision = result.new_revision; // if the server's idea of the previous revision doesn't match the client's, then someone has // gone behind our back and saved the editor's note from another window @@ -854,11 +860,15 @@ Wiki.prototype.update_editor_revisions = function ( result, editor ) { self.compare_versions( event, editor, result.previous_revision ); } ); - revisions.push( result.previous_revision ); + if ( !editor.revisions_list || editor.revisions_list.length == 0 ) + return; + editor.revisions_list.push( result.previous_revision ); } // add the new revision to the editor's revisions list - revisions.push( result.new_revision ); + if ( !editor.revisions_list || editor.revisions_list.length == 0 ) + return; + editor.revisions_list.push( result.new_revision ); } Wiki.prototype.search = function ( event ) { @@ -898,7 +908,7 @@ Wiki.prototype.display_search_results = function ( result ) { } // otherwise, create an editor for the one note - this.create_editor( note.object_id, note.contents, note.deleted_from, note.revisions_list, undefined, this.read_write, true, true ); + this.create_editor( note.object_id, note.contents, note.deleted_from_id, note.revision, this.read_write, true, true ); return; } @@ -936,7 +946,7 @@ Wiki.prototype.display_search_results = function ( result ) { ); } - this.search_results_editor = this.create_editor( "search_results", "

search results

" + list.innerHTML, undefined, undefined, undefined, false, true, true ); + this.search_results_editor = this.create_editor( "search_results", "

search results

" + list.innerHTML, undefined, undefined, false, true, true ); } Wiki.prototype.display_all_notes_list = function ( result ) { @@ -954,6 +964,8 @@ Wiki.prototype.display_all_notes_list = function ( result ) { var note_tuple = result.notes[ i ] var note_id = note_tuple[ 0 ]; var note_title = note_tuple[ 1 ]; + if ( !note_title ) + note_title = "untitled note"; appendChildNodes( list, createDOM( "li", {}, @@ -962,7 +974,7 @@ Wiki.prototype.display_all_notes_list = function ( result ) { ); } - this.all_notes_editor = this.create_editor( "all_notes", "

all notes

" + list.innerHTML, undefined, undefined, undefined, false, true, true ); + this.all_notes_editor = this.create_editor( "all_notes", "

all notes

" + list.innerHTML, undefined, undefined, false, true, true ); } Wiki.prototype.display_message = function ( text, nodes ) { @@ -1099,8 +1111,22 @@ Wiki.prototype.display_empty_message = function () { this.display_message( "The trash is empty." ) } +DATE_PATTERN = /(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d).(\d+)[+-](\d\d:?\d\d)/; + Wiki.prototype.brief_revision = function ( revision ) { - return revision.split( /\.\d/ )[ 0 ]; // strip off seconds from the timestamp + var matches = DATE_PATTERN.exec( revision ); + + return new Date( Date.UTC( + matches[ 1 ], // year + matches[ 2 ] - 1, // month (zero-based) + matches[ 3 ], // day + matches[ 4 ], // hour + matches[ 5 ], // minute + matches[ 6 ], // second + matches[ 7 ] * 0.001 // milliseconds + ) ).toLocaleString(); + +// return revision.split( /\.\d/ )[ 0 ]; // strip off seconds from the timestamp } Wiki.prototype.toggle_editor_changes = function ( event, editor ) { @@ -1112,8 +1138,26 @@ Wiki.prototype.toggle_editor_changes = function ( event, editor ) { return; } - new Changes_pulldown( this, this.notebook_id, this.invoker, editor ); event.stop(); + + // if there's already a cached revision list, display the changes pulldown with it + if ( editor.revisions_list.length > 0 ) { + new Changes_pulldown( this, this.notebook_id, this.invoker, editor ); + return; + } + + // otherwise, load the revision list for this note from the server + var self = this; + this.invoker.invoke( + "/notebooks/load_note_revisions", "GET", { + "notebook_id": this.notebook_id, + "note_id": editor.id + }, + function ( result ) { + editor.revisions_list = result.revisions; + new Changes_pulldown( self, self.notebook_id, self.invoker, editor ); + } + ); } Wiki.prototype.toggle_editor_options = function ( event, editor ) { @@ -1236,13 +1280,13 @@ function Changes_pulldown( wiki, notebook_id, invoker, editor ) { this.editor = editor; this.links = new Array(); - // display list of revision timestamps in reverse chronological order - if ( isUndefinedOrNull( this.editor.revisions_list ) || this.editor.revisions_list.length == 0 ) { + if ( !editor.revisions_list || editor.revisions_list.length == 0 ) { appendChildNodes( this.div, createDOM( "span", "This note has no previous changes." ) ); return; } - var revisions_list = clone( this.editor.revisions_list ); + // display list of revision timestamps in reverse chronological order + var revisions_list = clone( editor.revisions_list ); revisions_list.reverse(); var self = this; diff --git a/static/js/test/Editor_setup.js b/static/js/test/Editor_setup.js index cc440ae..10f8129 100644 --- a/static/js/test/Editor_setup.js +++ b/static/js/test/Editor_setup.js @@ -3,14 +3,14 @@ function setUpPage() { notebook_id = "fake_notebook_id"; title = "the title" note_text = "

" + title + "

blah"; - deleted_from = undefined; + deleted_from_id = undefined; revisions_list = undefined; read_write = true; startup = false; highlight = false; editor_focus = false; - editor = new Editor( id, notebook_id, note_text, deleted_from, revisions_list, read_write, startup, highlight, editor_focus ); + editor = new Editor( id, notebook_id, note_text, deleted_from_id, revisions_list, read_write, startup, highlight, editor_focus ); init_complete = false; connect( editor, "init_complete", function () { init_complete = true; } ); diff --git a/static/js/test/Stub_editor.js b/static/js/test/Stub_editor.js index b4496cf..f05f054 100644 --- a/static/js/test/Stub_editor.js +++ b/static/js/test/Stub_editor.js @@ -1,8 +1,8 @@ -function Editor( id, notebook_id, note_text, deleted_from, revisions_list, read_write, startup, highlight, focus ) { +function Editor( id, notebook_id, note_text, deleted_from_id, revisions_list, read_write, startup, highlight, focus ) { this.id = id; this.notebook_id = notebook_id; this.initial_text = note_text; - this.deleted_from = deleted_from || null; + this.deleted_from_id = deleted_from_id || null; this.revisions_list = revisions_list || new Array(); this.read_write = read_write; this.startup = startup || false; // whether this Editor is for a startup note diff --git a/static/js/test/Test_editor.html b/static/js/test/Test_editor.html index 0ac20e8..c94fd3e 100644 --- a/static/js/test/Test_editor.html +++ b/static/js/test/Test_editor.html @@ -15,11 +15,10 @@ function test_Editor() { assertNotUndefined( "editor should have changes_button member", editor.changes_button ); assertNotUndefined( "editor should have options_button member", editor.options_button ); assertFalse( "editor should not have closed flag set", editor.closed ); - assertEquals( "editor should have correct deleted_from flag", editor.deleted_from, deleted_from || null ); + assertEquals( "editor should have correct deleted_from_id flag", editor.deleted_from_id, deleted_from_id || null ); assertNotUndefined( "editor should have document member", editor.document ); assertEquals( "editor id should have correct id", editor.id, id ); assertNotUndefined( "editor should have iframe member", editor.iframe ); - assertEquals( "editor should have empty revisions list", editor.revisions_list.length, 0 ); assertEquals( "editor should have correct startup flag", editor.startup, startup ); assertEquals( "editor should have correct title", editor.title, title ); assertEquals( "editor should have correct read_write flag", editor.read_write, read_write ); diff --git a/static/js/test/Test_wiki.html b/static/js/test/Test_wiki.html index fb24eb3..2e3a918 100644 --- a/static/js/test/Test_wiki.html +++ b/static/js/test/Test_wiki.html @@ -101,6 +101,7 @@ function test_Wiki() { + diff --git a/tools/convertdb.py b/tools/convertdb.py index 1709c50..a2129b9 100755 --- a/tools/convertdb.py +++ b/tools/convertdb.py @@ -3,21 +3,31 @@ import os import os.path import psycopg2 as psycopg -from controller.Database import Database +from pytz import timezone, utc +from datetime import datetime +from controller.Old_database import Old_database from controller.Scheduler import Scheduler +pacific = timezone( "US/Pacific" ) + + def quote( value ): if value is None: return "null" + # if this is a datetime, assume it's in the Pacific timezone, and then convert it to UTC + if isinstance( value, datetime ): + value = value.replace( tzinfo = pacific ).astimezone( utc ) + value = unicode( value ) + return "'%s'" % value.replace( "'", "''" ).replace( "\\", "\\\\" ) class Converter( object ): """ - Converts a Luminotes database from bsddb to PostgreSQL, using the old bsddb controller.Database. + Converts a Luminotes database from bsddb to PostgreSQL, using the old bsddb controller.Old_database. This assumes that the PostgreSQL schema from model/schema.sql is already in the database. """ def __init__( self, scheduler, database ): @@ -36,8 +46,8 @@ class Converter( object ): notes = {} # map of note object id to its notebook startup_notes = {} # map of startup note object id to its notebook - for key in self.database._Database__db.keys(): - if not self.database._Database__db.get( key ): + for key in self.database._Old_database__db.keys(): + if not self.database._Old_database__db.get( key ): continue self.database.load( key, self.scheduler.thread ) @@ -102,6 +112,14 @@ class Converter( object ): ( quote( value.object_id ), quote( notebook_id ), quote( read_only and "f" or "t" ) ) ) + if notebook.trash: + self.cursor.execute( + "insert into user_notebook " + + "( user_id, notebook_id, read_write ) " + + "values ( %s, %s, %s );" % + ( quote( value.object_id ), quote( notebook.trash.object_id ), + quote( read_only and "f" or "t" ) ) + ) elif class_name == "Read_only_notebook": pass elif class_name == "Password_reset": @@ -140,7 +158,7 @@ class Converter( object ): def main(): scheduler = Scheduler() - database = Database( scheduler, "data.db" ) + database = Old_database( scheduler, "data.db" ) initializer = Converter( scheduler, database ) scheduler.wait_until_idle() diff --git a/tools/deletenote.py b/tools/deletenote.py deleted file mode 100755 index 6ac272c..0000000 --- a/tools/deletenote.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/python2.5 - -import os -import os.path -from config.Common import settings -from controller.Database import Database -from controller.Scheduler import Scheduler -from model.Note import Note -from tools.initdb import fix_note_contents - - -class Deleter( object ): - HTML_PATH = u"static/html" - - def __init__( self, scheduler, database ): - self.scheduler = scheduler - self.database = database - - threads = ( - self.delete_note(), - ) - - for thread in threads: - self.scheduler.add( thread ) - self.scheduler.wait_for( thread ) - - def delete_note( self ): - self.database.load( u"User anonymous", self.scheduler.thread ) - anonymous = ( yield Scheduler.SLEEP ) - read_only_main_notebook = anonymous.notebooks[ 0 ] - main_notebook = anonymous.notebooks[ 0 ]._Read_only_notebook__wrapped - startup_notes = [] - - for note in main_notebook.notes: - if note and note.title == "try it out": # FIXME: make the note title to delete not hard-coded - print "deleting note %s: %s" % ( note.object_id, note.title ) - main_notebook.remove_note( note ) - - self.database.save( main_notebook ) - - -def main( args ): - print "IMPORTANT: Stop the Luminotes server before running this program." - - scheduler = Scheduler() - database = Database( scheduler, "data.db" ) - initializer = Deleter( scheduler, database ) - scheduler.wait_until_idle() - - -if __name__ == "__main__": - import sys - main( sys.argv[ 1: ] ) diff --git a/tools/dumpdb.py b/tools/dumpdb.py deleted file mode 100755 index b594e1d..0000000 --- a/tools/dumpdb.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/python2.5 - -import os -import os.path -from controller.Database import Database -from controller.Scheduler import Scheduler - - -class Dumper( object ): - def __init__( self, scheduler, database ): - self.scheduler = scheduler - self.database = database - - thread = self.dump_database() - self.scheduler.add( thread ) - self.scheduler.wait_for( thread ) - - def dump_database( self ): - for key in self.database._Database__db.keys(): - self.database.load( key, self.scheduler.thread ) - value = ( yield Scheduler.SLEEP ) - print "%s: %s" % ( key, value ) - - yield None - - -def main(): - scheduler = Scheduler() - database = Database( scheduler, "data.db" ) - initializer = Dumper( scheduler, database ) - scheduler.wait_until_idle() - - -if __name__ == "__main__": - main() diff --git a/tools/initdb.py b/tools/initdb.py index b9ca356..3b06b3e 100644 --- a/tools/initdb.py +++ b/tools/initdb.py @@ -1,14 +1,12 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python2.4 import os import os.path +import sys from controller.Database import Database -from controller.Scheduler import Scheduler -from model.Notebook import Notebook -from model.Read_only_notebook import Read_only_notebook -from model.Note import Note -from model.User import User -from model.User_list import User_list +from new_model.Notebook import Notebook +from new_model.Note import Note +from new_model.User import User class Initializer( object ): @@ -27,78 +25,68 @@ class Initializer( object ): ( u"advanced browser features.html", False ), ] - def __init__( self, scheduler, database ): - self.scheduler = scheduler + def __init__( self, database, nuke = False ): self.database = database self.main_notebook = None - self.read_only_main_notebook = None self.anonymous = None - threads = ( - self.create_main_notebook(), - self.create_anonymous_user(), - ) + if nuke is True: + self.database.execute( file( "new_model/drop.sql" ).read(), commit = False ) - for thread in threads: - self.scheduler.add( thread ) - self.scheduler.wait_for( thread ) + self.database.execute( file( "new_model/schema.sql" ).read(), commit = False ) + + self.create_main_notebook() + self.create_anonymous_user() + self.database.commit() def create_main_notebook( self ): - # create the main notebook and all of its notes - self.database.next_id( self.scheduler.thread ) - main_notebook_id = ( yield Scheduler.SLEEP ) - self.main_notebook = Notebook( main_notebook_id, u"Luminotes" ) - - # create the read-only view of the main notebook - self.database.next_id( self.scheduler.thread ) - read_only_main_notebook_id = ( yield Scheduler.SLEEP ) - self.read_only_main_notebook = Read_only_notebook( read_only_main_notebook_id, self.main_notebook ) + # create the main notebook + main_notebook_id = self.database.next_id( Notebook ) + self.main_notebook = Notebook.create( main_notebook_id, u"Luminotes" ) + self.database.save( self.main_notebook, commit = False ) # create an id for each note note_ids = {} for ( filename, startup ) in self.NOTE_FILES: - self.database.next_id( self.scheduler.thread ) - note_ids[ filename ] = ( yield Scheduler.SLEEP ) + note_ids[ filename ] = self.database.next_id( Note ) + rank = 0 for ( filename, startup ) in self.NOTE_FILES: full_filename = os.path.join( self.HTML_PATH, filename ) - contents = fix_note_contents( file( full_filename ).read(), read_only_main_notebook_id, note_ids ) - - note = Note( note_ids[ filename ], contents ) - self.main_notebook.add_note( note ) + contents = fix_note_contents( file( full_filename ).read(), main_notebook_id, note_ids ) if startup: - self.main_notebook.add_startup_note( note ) + rank += 1 - self.database.save( self.main_notebook ) - self.database.save( self.read_only_main_notebook ) + note = Note.create( note_ids[ filename ], contents, notebook_id = self.main_notebook.object_id, startup = startup, rank = startup and rank or None ) + self.database.save( note, commit = False ) def create_anonymous_user( self ): # create the anonymous user - self.database.next_id( self.scheduler.thread ) - anonymous_user_id = ( yield Scheduler.SLEEP ) - notebooks = [ self.read_only_main_notebook ] - self.anonymous = User( anonymous_user_id, u"anonymous", None, None, notebooks ) - self.database.save( self.anonymous ) + anonymous_user_id = self.database.next_id( User ) + self.anonymous = User.create( anonymous_user_id, u"anonymous", None, None ) + self.database.save( self.anonymous, commit = False ) - # create a user list - self.database.next_id( self.scheduler.thread ) - user_list_id = ( yield Scheduler.SLEEP ) - user_list = User_list( user_list_id, u"all" ) - user_list.add_user( self.anonymous ) - self.database.save( user_list ) + # give the anonymous user read-only access to the main notebook + self.database.execute( self.anonymous.sql_save_notebook( self.main_notebook.object_id, read_write = False ), commit = False ) -def main(): - print "IMPORTANT: Stop the Luminotes server before running this program." +def main( args = None ): + nuke = False - if os.path.exists( "data.db" ): - os.remove( "data.db" ) + if args and ( "-n" in args or "--nuke" in args ): + nuke = True + print "This will nuke the contents of the database before initializing it with default data. Continue (y/n)? ", + confirmation = sys.stdin.readline().strip() + print - scheduler = Scheduler() - database = Database( scheduler, "data.db" ) - initializer = Initializer( scheduler, database ) - scheduler.wait_until_idle() + if confirmation.lower()[ 0 ] != 'y': + print "Exiting without touching the database." + return + + print "Initializing the database with default data." + database = Database() + initializer = Initializer( database, nuke ) def fix_note_contents( contents, notebook_id, note_ids ): @@ -134,4 +122,5 @@ def fix_note_contents( contents, notebook_id, note_ids ): if __name__ == "__main__": - main() + import sys + main( sys.argv[ 1: ] ) diff --git a/tools/listuser.py b/tools/listuser.py deleted file mode 100755 index 2e14d6b..0000000 --- a/tools/listuser.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/python2.5 - -import os -import os.path -import sys -from config.Common import settings -from controller.Database import Database -from controller.Scheduler import Scheduler - - -class Lister( object ): - def __init__( self, scheduler, database, username ): - self.scheduler = scheduler - self.database = database - self.username = username - - threads = ( - self.list_user(), - ) - - for thread in threads: - self.scheduler.add( thread ) - self.scheduler.wait_for( thread ) - - def list_user( self ): - self.database.load( u"User %s" % self.username, self.scheduler.thread ) - user = ( yield Scheduler.SLEEP ) - if user is None: - print "user %s is unknown" % self.username - else: - print "user %s: %s" % ( self.username, user ) - - -def main( program_name, args ): - if len( args ) == 0: - print "usage: %s username" % program_name - sys.exit( 1 ) - - scheduler = Scheduler() - database = Database( scheduler, "data.db" ) - initializer = Lister( scheduler, database, args[ 0 ] ) - scheduler.wait_until_idle() - - -if __name__ == "__main__": - import sys - main( sys.argv[ 0 ], sys.argv[ 1: ] ) diff --git a/tools/release.sh b/tools/release.sh index d7c86a6..673989e 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -1,11 +1,7 @@ #!/bin/sh -# Run this from the root directory of Luminotes's source. Note: This will nuke your database! +# Run this from the root directory of Luminotes's source. -ORIG_PYTHONPATH="$PYTHONPATH" -export PYTHONPATH=. -python2.5 tools/initdb.py -export PYTHONPATH="$ORIG_PYTHONPATH" cd .. rm -f luminotes.tar.gz tar cvfz luminotes.tar.gz --exclude=session --exclude="*.log" --exclude="*.pyc" --exclude=".*" luminotes diff --git a/tools/reloaddb.py b/tools/reloaddb.py deleted file mode 100755 index 1bfb434..0000000 --- a/tools/reloaddb.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/python2.5 - -import os -import os.path -from controller.Database import Database -from controller.Scheduler import Scheduler - - -class Reloader( object ): - def __init__( self, scheduler, database ): - self.scheduler = scheduler - self.database = database - - thread = self.reload_database() - self.scheduler.add( thread ) - self.scheduler.wait_for( thread ) - - def reload_database( self ): - for key in self.database._Database__db.keys(): - self.database.reload( key, self.scheduler.thread ) - yield Scheduler.SLEEP - - yield None - - -def main(): - print "IMPORTANT: Stop the Luminotes server before running this program." - - scheduler = Scheduler() - database = Database( scheduler, "data.db" ) - initializer = Reloader( scheduler, database ) - scheduler.wait_until_idle() - - -if __name__ == "__main__": - main() diff --git a/tools/resetpw.py b/tools/resetpw.py deleted file mode 100755 index 8d974ce..0000000 --- a/tools/resetpw.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/python2.5 - -import os -import os.path -import sys -from config.Common import settings -from controller.Database import Database -from controller.Scheduler import Scheduler - - -class Resetter( object ): - def __init__( self, scheduler, database, username ): - self.scheduler = scheduler - self.database = database - self.username = username - self.password = None - - self.prompt_for_password() - - threads = ( - self.reset_password(), - ) - - for thread in threads: - self.scheduler.add( thread ) - self.scheduler.wait_for( thread ) - - def prompt_for_password( self ): - print "enter new password for user %s: " % self.username, - sys.stdout.flush() - self.password = sys.stdin.readline().strip() - print - - def reset_password( self ): - self.database.load( u"User %s" % self.username, self.scheduler.thread ) - user = ( yield Scheduler.SLEEP ) - if user is None: - raise Exception( "user %s is unknown" % self.username ) - - - user.password = self.password - self.database.save( user ) - print "password reset" - - -def main( program_name, args ): - print "IMPORTANT: Stop the Luminotes server before running this program." - - if len( args ) == 0: - print "usage: %s username" % program_name - sys.exit( 1 ) - - scheduler = Scheduler() - database = Database( scheduler, "data.db" ) - initializer = Resetter( scheduler, database, args[ 0 ] ) - scheduler.wait_until_idle() - - -if __name__ == "__main__": - import sys - main( sys.argv[ 0 ], sys.argv[ 1: ] ) diff --git a/tools/setemail.py b/tools/setemail.py deleted file mode 100755 index a1fda3a..0000000 --- a/tools/setemail.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/python2.5 - -import os -import os.path -import sys -from config.Common import settings -from controller.Database import Database -from controller.Scheduler import Scheduler - - -class Setter( object ): - def __init__( self, scheduler, database, username, email_address ): - self.scheduler = scheduler - self.database = database - self.username = username - self.email_address = email_address - self.password = None - - threads = ( - self.set_email_address(), - ) - - for thread in threads: - self.scheduler.add( thread ) - self.scheduler.wait_for( thread ) - - def set_email_address( self ): - self.database.load( u"User %s" % self.username, self.scheduler.thread ) - user = ( yield Scheduler.SLEEP ) - if user is None: - raise Exception( "user %s is unknown" % self.username ) - - user.email_address = self.email_address - self.database.save( user ) - print "email set" - - -def main( program_name, args ): - print "IMPORTANT: Stop the Luminotes server before running this program." - - if len( args ) < 2: - print "usage: %s username emailaddress" % program_name - sys.exit( 1 ) - - scheduler = Scheduler() - database = Database( scheduler, "data.db" ) - initializer = Setter( scheduler, database, *args ) - scheduler.wait_until_idle() - - -if __name__ == "__main__": - import sys - main( sys.argv[ 0 ], sys.argv[ 1: ] ) diff --git a/tools/setplan.py b/tools/setplan.py deleted file mode 100755 index 0209965..0000000 --- a/tools/setplan.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/python2.5 - -import os -import os.path -import sys -from config.Common import settings -from controller.Database import Database -from controller.Scheduler import Scheduler - - -class Setter( object ): - def __init__( self, scheduler, database, username, rate_plan ): - self.scheduler = scheduler - self.database = database - self.username = username - self.rate_plan = rate_plan - self.password = None - - threads = ( - self.set_rate_plan(), - ) - - for thread in threads: - self.scheduler.add( thread ) - self.scheduler.wait_for( thread ) - - def set_rate_plan( self ): - self.database.load( u"User %s" % self.username, self.scheduler.thread ) - user = ( yield Scheduler.SLEEP ) - if user is None: - raise Exception( "user %s is unknown" % self.username ) - - user.rate_plan = int( self.rate_plan ) - self.database.save( user ) - print "rate plan set" - - -def main( program_name, args ): - print "IMPORTANT: Stop the Luminotes server before running this program." - - if len( args ) < 2: - print "usage: %s username rateplan" % program_name - sys.exit( 1 ) - - scheduler = Scheduler() - database = Database( scheduler, "data.db" ) - initializer = Setter( scheduler, database, *args ) - scheduler.wait_until_idle() - - -if __name__ == "__main__": - import sys - main( sys.argv[ 0 ], sys.argv[ 1: ] ) diff --git a/tools/updatedb.py b/tools/updatedb.py index 9ec3234..c640cbf 100755 --- a/tools/updatedb.py +++ b/tools/updatedb.py @@ -1,16 +1,16 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python2.4 import os import os.path from config.Common import settings from controller.Database import Database -from controller.Scheduler import Scheduler -from model.Note import Note -from model.User_list import User_list +from new_model.Notebook import Notebook +from new_model.Note import Note +from new_model.User import User from tools.initdb import fix_note_contents -class Initializer( object ): +class Updater( object ): HTML_PATH = u"static/html" NOTE_FILES = [ # the second element of the tuple is whether to show the note on startup ( u"about.html", True ), @@ -25,101 +25,58 @@ class Initializer( object ): ( u"advanced browser features.html", False ), ] - def __init__( self, scheduler, database, navigation_note_id = None ): - self.scheduler = scheduler + def __init__( self, database, navigation_note_id = None ): self.database = database self.navigation_note_id = navigation_note_id - threads = ( - self.create_user_list(), - self.update_main_notebook(), - ) - - for thread in threads: - self.scheduler.add( thread ) - self.scheduler.wait_for( thread ) - - def create_user_list( self ): - # if there's no user list, create one and populate it with all users in the database - self.database.load( u"User_list all", self.scheduler.thread ) - user_list = ( yield Scheduler.SLEEP ) - if user_list is not None: - return - - self.database.next_id( self.scheduler.thread ) - user_list_id = ( yield Scheduler.SLEEP ) - user_list = User_list( user_list_id, u"all" ) - - for key in self.database._Database__db.keys(): - if not key.startswith( "User " ): continue - - self.database.load( key, self.scheduler.thread ) - user = ( yield Scheduler.SLEEP ) - if user: - user_list.add_user( user ) - - self.database.save( user_list ) + self.update_main_notebook() + self.database.commit() def update_main_notebook( self ): - self.database.load( u"User anonymous", self.scheduler.thread ) - anonymous = ( yield Scheduler.SLEEP ) - read_only_main_notebook = anonymous.notebooks[ 0 ] - main_notebook = anonymous.notebooks[ 0 ]._Read_only_notebook__wrapped - startup_notes = [] + anonymous = self.database.select_one( User, User.sql_load_by_username( u"anonymous" ) ) + main_notebook = self.database.select_one( Notebook, anonymous.sql_load_notebooks() ) # get the id for each note note_ids = {} for ( filename, startup ) in self.NOTE_FILES: title = filename.replace( u".html", u"" ) - note = main_notebook.lookup_note_by_title( title ) + note = self.database.select_one( Note, main_notebook.sql_load_note_by_title( title ) ) if note is not None: note_ids[ filename ] = note.object_id # update the navigation note if its id was given if self.navigation_note_id: - self.database.next_id( self.scheduler.thread ) - next_id = ( yield Scheduler.SLEEP ) - note = main_notebook.lookup_note( self.navigation_note_id ) - self.update_note( "navigation.html", True, main_notebook, read_only_main_notebook, startup_notes, next_id, note_ids, note ) + note = self.database.load( Note, self.navigation_note_id ) + self.update_note( "navigation.html", True, main_notebook, note_ids, note ) + self.database.save( note, commit = False ) # update all of the notes in the main notebook for ( filename, startup ) in self.NOTE_FILES: - self.database.next_id( self.scheduler.thread ) - next_id = ( yield Scheduler.SLEEP ) title = filename.replace( u".html", u"" ) - note = main_notebook.lookup_note_by_title( title ) - self.update_note( filename, startup, main_notebook, read_only_main_notebook, startup_notes, next_id, note_ids, note ) + note = self.database.select_one( Note, main_notebook.sql_load_note_by_title( title ) ) + self.update_note( filename, startup, main_notebook, note_ids, note ) - for note in startup_notes: - main_notebook.add_startup_note( note ) + if main_notebook.name != u"Luminotes": + main_notebook.name = u"Luminotes" + self.database.save( main_notebook, commit = False ) - main_notebook.name = u"Luminotes" - self.database.save( main_notebook ) - - def update_note( self, filename, startup, main_notebook, read_only_main_notebook, startup_notes, next_id, note_ids, note = None ): + def update_note( self, filename, startup, main_notebook, note_ids, note = None ): full_filename = os.path.join( self.HTML_PATH, filename ) - contents = fix_note_contents( file( full_filename ).read(), read_only_main_notebook.object_id, note_ids ) + contents = fix_note_contents( file( full_filename ).read(), main_notebook.object_id, note_ids ) if note: - main_notebook.update_note( note, contents ) + note.contents = contents # if for some reason the note isn't present, create it else: - note = Note( next_id, contents ) - main_notebook.add_note( note ) - - main_notebook.remove_startup_note( note ) - if startup: - startup_notes.append( note ) + next_id = self.database.next_id( Note ) + note = Note.create( next_id, contents, notebook_id = main_notebook.object_id, startup = startup ) + self.database.save( note, commit = False ) def main( args ): - print "IMPORTANT: Stop the Luminotes server before running this program." - - scheduler = Scheduler() - database = Database( scheduler, "data.db" ) - initializer = Initializer( scheduler, database, args and args[ 0 ] or None ) - scheduler.wait_until_idle() + database = Database() + initializer = Updater( database, args and args[ 0 ] or None ) if __name__ == "__main__": diff --git a/tools/verifyconvertdb.py b/tools/verifyconvertdb.py index 858c6e5..8e2dcf9 100755 --- a/tools/verifyconvertdb.py +++ b/tools/verifyconvertdb.py @@ -3,7 +3,7 @@ import os import os.path import psycopg2 as psycopg -from controller.Database import Database +from controller.Old_database import Old_database from controller.Scheduler import Scheduler @@ -34,8 +34,8 @@ class Verifier( object ): def verify_database( self ): inserts = set() - for key in self.database._Database__db.keys(): - if not self.database._Database__db.get( key ): + for key in self.database._Old_database__db.keys(): + if not self.database._Old_database__db.get( key ): continue self.database.load( key, self.scheduler.thread ) @@ -92,6 +92,16 @@ class Verifier( object ): assert row[ 1 ] == notebook.object_id assert row[ 2 ] == read_write + if notebook.trash: + self.cursor.execute( + "select * from user_notebook where user_id = %s and notebook_id = %s;" % ( quote( value.object_id ), quote( notebook.trash.object_id ) ) + ) + + for row in self.cursor.fetchmany(): + assert row[ 0 ] == value.object_id + assert row[ 1 ] == notebook.trash.object_id + assert row[ 2 ] == read_write + self.verify_notebook( notebook ) elif class_name == "Read_only_notebook": @@ -105,9 +115,9 @@ class Verifier( object ): ) for row in self.cursor.fetchmany(): - assert row[ 0 ] == value.email_address - assert row[ 1 ] == False - assert row[ 2 ] == value.object_id + assert row[ 0 ] == value.object_id + assert row[ 1 ] == value.email_address + assert row[ 2 ] == False elif class_name == "User_list": pass else: @@ -167,7 +177,7 @@ class Verifier( object ): def main(): scheduler = Scheduler() - database = Database( scheduler, "data.db" ) + database = Old_database( scheduler, "data.db" ) initializer = Verifier( scheduler, database ) scheduler.wait_until_idle()