From 43c6f54e9fe9a70b50b864c4f2d0ecf5e73d8436 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Thu, 11 Oct 2007 09:03:43 +0000 Subject: [PATCH] Merged revisions 401-446 via svnmerge from svn+ssh://torsion.org/home/luminotes/repos/luminotes/branches/postgres ................ r402 | witten | 2007-10-04 00:48:49 -0700 (Thu, 04 Oct 2007) | 3 lines Initialized merge tracking via "svnmerge" with revisions "1-401" from svn+ssh://torsion.org/home/luminotes/repos/luminotes/trunk ................ r404 | witten | 2007-10-04 01:17:07 -0700 (Thu, 04 Oct 2007) | 2 lines Beginning a conversion from bsddb to postgres. ................ r405 | witten | 2007-10-04 01:18:58 -0700 (Thu, 04 Oct 2007) | 9 lines Merged revisions 402-404 via svnmerge from svn+ssh://torsion.org/home/luminotes/repos/luminotes/trunk ........ r403 | witten | 2007-10-04 01:14:45 -0700 (Thu, 04 Oct 2007) | 2 lines Yay, no more stupid deprecation warnings from simplejson about the sre module. ........ ................ r406 | witten | 2007-10-04 15:34:39 -0700 (Thu, 04 Oct 2007) | 4 lines * Switched back to Python 2.4 because many Python modules in Debian are not packaged to work with Python 2.5 * Began removal of all references to Scheduler, @async, yield, and so on. * Converted Database.py to support PostgreSQL and updated its unit tests accordingly. ................ r407 | witten | 2007-10-04 16:34:01 -0700 (Thu, 04 Oct 2007) | 2 lines All unit tests for the new model classes now pass. ................ r409 | witten | 2007-10-05 00:53:56 -0700 (Fri, 05 Oct 2007) | 2 lines Reordering some columns and adding some indices. ................ r410 | witten | 2007-10-05 16:08:37 -0700 (Fri, 05 Oct 2007) | 4 lines Now adding trash notebooks to user_notebook table. Also switching db conversion/verification tools back to require Python 2.5, since they still use the old Scheduler, which requires 2.5 generator features. ................ r411 | witten | 2007-10-06 16:26:56 -0700 (Sat, 06 Oct 2007) | 2 lines Lots more unit tests passing. Most of the recent work was on controller.Users and related stuff. ................ r412 | witten | 2007-10-07 01:52:12 -0700 (Sun, 07 Oct 2007) | 2 lines controller.Users unit tests now finally pass! ................ r413 | witten | 2007-10-07 02:14:10 -0700 (Sun, 07 Oct 2007) | 3 lines Got controller.Root unit tests passing. Moved fake sql_* function shenanigans from Test_users.py to Test_controller.py, for use by other controller unit tests. ................ r414 | witten | 2007-10-08 23:11:11 -0700 (Mon, 08 Oct 2007) | 2 lines All unit tests pass! Fuck yeah! ................ r415 | witten | 2007-10-08 23:13:07 -0700 (Mon, 08 Oct 2007) | 2 lines Removing all references to Scheduler from luminotes.py ................ r416 | witten | 2007-10-08 23:54:51 -0700 (Mon, 08 Oct 2007) | 3 lines Converted deleted_from to deleted_from_id in a few more places. Fixed bug in Users.contents(). ................ r417 | witten | 2007-10-09 00:11:59 -0700 (Tue, 09 Oct 2007) | 3 lines Typo fix in Note sql method. Adding autocommit flag to Database.next_id() method. ................ r418 | witten | 2007-10-09 00:13:19 -0700 (Tue, 09 Oct 2007) | 2 lines Updating unit test for new auto commit flag. ................ r419 | witten | 2007-10-09 00:14:09 -0700 (Tue, 09 Oct 2007) | 2 lines Removing debugging print. ................ r420 | witten | 2007-10-09 00:20:55 -0700 (Tue, 09 Oct 2007) | 2 lines More sql fixes. I really need some funtional tests that hit the database and exercise the SQL. ................ r421 | witten | 2007-10-09 00:51:34 -0700 (Tue, 09 Oct 2007) | 3 lines Fixed controller.Database handling of tuple as an Object_type. Made SQL for user storage calculation better at handling null values and also more succinct. ................ r422 | witten | 2007-10-09 13:32:16 -0700 (Tue, 09 Oct 2007) | 2 lines Converting Wiki.js to trash_id notebook member instead of trash object. ................ r423 | witten | 2007-10-09 13:42:10 -0700 (Tue, 09 Oct 2007) | 2 lines No longer displaying "download as html" on the front page, as people see "download" and think they're downloading the software. ................ r424 | witten | 2007-10-09 14:24:40 -0700 (Tue, 09 Oct 2007) | 2 lines Notebooks.contents() now returns notebooks with correct read-write status. ................ r425 | witten | 2007-10-09 14:32:25 -0700 (Tue, 09 Oct 2007) | 2 lines Fixed reporting of validation errors to the user. Now says "The blah is missing." instead of just "is missing" ................ r426 | witten | 2007-10-09 17:05:22 -0700 (Tue, 09 Oct 2007) | 2 lines No longer redirecting to trash notebook upon login. ................ r427 | witten | 2007-10-09 17:20:33 -0700 (Tue, 09 Oct 2007) | 2 lines Made controller.Database use a connection pool. ................ r429 | witten | 2007-10-09 20:13:30 -0700 (Tue, 09 Oct 2007) | 2 lines Converted initdb.py and updatedb.py to Postgres from bsddb. ................ r430 | witten | 2007-10-09 20:37:14 -0700 (Tue, 09 Oct 2007) | 2 lines Changing error message to remove underscores from variable names. ................ r431 | witten | 2007-10-10 13:23:30 -0700 (Wed, 10 Oct 2007) | 2 lines Removing unused note_title parameter from Wiki.create_editor(). ................ r432 | witten | 2007-10-10 13:25:16 -0700 (Wed, 10 Oct 2007) | 2 lines Revision regular expression now supports timezone notation. ................ r433 | witten | 2007-10-10 14:43:47 -0700 (Wed, 10 Oct 2007) | 2 lines Finished implementing ranked ordering for startup notes. (However, there's no way to change the rank from the client yet.) ................ r434 | witten | 2007-10-10 16:25:19 -0700 (Wed, 10 Oct 2007) | 4 lines More strict access checking. Fixed oversight in Postgres DB conversion where, in certain controller.Notebook methods, access was only checked at the notebook level, not at the note level as well. ................ r435 | witten | 2007-10-10 17:45:18 -0700 (Wed, 10 Oct 2007) | 3 lines Now loading revisions on demand from client when the "changes" button is clicked. Also caching loading revisions so subsequent clicks don't have to reload. ................ r436 | witten | 2007-10-10 21:31:20 -0700 (Wed, 10 Oct 2007) | 2 lines Tweaking some of the error handling in Expose and Root so that unhandled errors give a generic error message to the client. ................ r437 | witten | 2007-10-10 21:33:49 -0700 (Wed, 10 Oct 2007) | 2 lines The release script no longer runs initdb.py, because the default database is no longer a single file included in the tarball. ................ r438 | witten | 2007-10-10 21:40:11 -0700 (Wed, 10 Oct 2007) | 2 lines Updated install instructuctions to include use of initdb.py. ................ r439 | witten | 2007-10-10 21:56:42 -0700 (Wed, 10 Oct 2007) | 3 lines Made initdb.py only nuke (drop tables/views) when given a command-line flag. Also made install directions more correct. ................ r440 | witten | 2007-10-10 21:58:48 -0700 (Wed, 10 Oct 2007) | 2 lines IE 6 doesn't like commas. ................ r441 | witten | 2007-10-10 22:08:50 -0700 (Wed, 10 Oct 2007) | 4 lines load your notebook. without clicking on "changes", edit a note that has previous revisions. click on "changes". it'll only show the most recent revision. fixed by not appending to changes as a result of a save unless the client-side revisions list cache has something in it ................ r442 | witten | 2007-10-10 23:30:41 -0700 (Wed, 10 Oct 2007) | 2 lines Forgot to actually save off the new revision as editor.revision. ................ r443 | witten | 2007-10-11 01:35:54 -0700 (Thu, 11 Oct 2007) | 13 lines More intelligent datetime handling: * convertdb.py assumes old bsddb database timestamps are Pacific, and then converts them to UTC before inserting them into the new PostgreSQL database. * No longer using naked timezoneless datetime objects in model/controller code, except in unit tests that need compatability with pysqlite. Now using UTC everwhere. * Asking PostgreSQL to give us all timestamps back in UTC. * New dependency on python-tz (pytz) package, noted in INSTALL doc. * Client now responsible for converting UTC timestamps to local time for display. ................ r444 | witten | 2007-10-11 01:46:09 -0700 (Thu, 11 Oct 2007) | 2 lines Tweak to prevent potential race in IE. ................ r445 | witten | 2007-10-11 01:49:58 -0700 (Thu, 11 Oct 2007) | 2 lines Got JavaScript "unit" tests passing again. ................ r446 | witten | 2007-10-11 01:53:58 -0700 (Thu, 11 Oct 2007) | 2 lines Noting that js tests require the Luminotes server on localhost. ................ --- INSTALL | 61 +-- controller/Database.py | 350 ++++++-------- controller/Expose.py | 22 +- controller/Notebooks.py | 504 +++++++------------- controller/Old_database.py | 303 ++++++++++++ controller/Root.py | 43 +- controller/Updater.py | 72 --- controller/Users.py | 331 ++++++------- controller/Validate.py | 2 +- controller/test/Stub_database.py | 72 +++ controller/test/Stub_object.py | 79 ++++ controller/test/Test_controller.py | 177 ++++++- controller/test/Test_database.py | 415 ++++++----------- controller/test/Test_notebooks.py | 640 ++++++++++++-------------- controller/test/Test_root.py | 10 +- controller/test/Test_users.py | 395 ++++++---------- luminotes.py | 9 +- new_model/Note.py | 150 ++++++ new_model/Notebook.py | 148 ++++++ new_model/Password_reset.py | 57 +++ new_model/Persistent.py | 82 ++++ new_model/User.py | 212 +++++++++ new_model/__init__.py | 0 new_model/drop.sql | 8 + {model => new_model}/schema.sql | 7 - new_model/test/Test_note.py | 133 ++++++ new_model/test/Test_notebook.py | 54 +++ new_model/test/Test_password_reset.py | 38 ++ new_model/test/Test_persistent.py | 73 +++ new_model/test/Test_user.py | 69 +++ static/js/Editor.js | 13 +- static/js/Wiki.js | 134 ++++-- static/js/test/Editor_setup.js | 4 +- static/js/test/Stub_editor.js | 4 +- static/js/test/Test_editor.html | 3 +- static/js/test/Test_wiki.html | 1 + tools/convertdb.py | 28 +- tools/deletenote.py | 53 --- tools/dumpdb.py | 35 -- tools/initdb.py | 99 ++-- tools/listuser.py | 47 -- tools/release.sh | 6 +- tools/reloaddb.py | 36 -- tools/resetpw.py | 61 --- tools/setemail.py | 53 --- tools/setplan.py | 53 --- tools/updatedb.py | 97 ++-- tools/verifyconvertdb.py | 24 +- 48 files changed, 2959 insertions(+), 2308 deletions(-) create mode 100644 controller/Old_database.py delete mode 100644 controller/Updater.py create mode 100644 controller/test/Stub_database.py create mode 100644 controller/test/Stub_object.py create mode 100644 new_model/Note.py create mode 100644 new_model/Notebook.py create mode 100644 new_model/Password_reset.py create mode 100644 new_model/Persistent.py create mode 100644 new_model/User.py create mode 100644 new_model/__init__.py create mode 100644 new_model/drop.sql rename {model => new_model}/schema.sql (97%) create mode 100644 new_model/test/Test_note.py create mode 100644 new_model/test/Test_notebook.py create mode 100644 new_model/test/Test_password_reset.py create mode 100644 new_model/test/Test_persistent.py create mode 100644 new_model/test/Test_user.py delete mode 100755 tools/deletenote.py delete mode 100755 tools/dumpdb.py delete mode 100755 tools/listuser.py delete mode 100755 tools/reloaddb.py delete mode 100755 tools/resetpw.py delete mode 100755 tools/setemail.py delete mode 100755 tools/setplan.py 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()