witten
/
luminotes
Archived
1
0
Fork 0

Fixed some bugs in convertdb.py.

Wrote a tool to verify that convertdb.py does what it's supposed to.
Added a todo comment to Scheduler about threading and generators.
Updated INSTALL documentation about eventual Postgres requirement.
This commit is contained in:
Dan Helfman 2007-10-02 22:43:18 +00:00
parent d3e953f8da
commit 43e8c7fb17
4 changed files with 243 additions and 3 deletions

61
INSTALL
View File

@ -6,12 +6,17 @@ First, install the prerequisites:
* Python 2.5 * Python 2.5
* CherryPy 2.2 * CherryPy 2.2
* PostgreSQL 8.1
* psycopg 2.0
* simplejson 1.3 * simplejson 1.3
In Debian GNU/Linux, you can issue the following command to install these In Debian GNU/Linux, you can issue the following command to install these
packages: packages:
apt-get install python2.5 python-cherrypy python-simplejson 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".
development mode development mode
@ -22,6 +27,24 @@ changes, because it uses CherryPy's built-in web server with auto-reload
enabled, so the server will automatically reload any modified source files as enabled, so the server will automatically reload any modified source files as
soon as they're modified. soon as they're modified.
Configure PostgreSQL's pg_hba.conf to require passwords for local connections:
local all all md5
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".
createuser -S -d -R -P -E luminotes
Initialize the database with the starting schema and basic data:
psql -U luminotes postgres -f model/schema.sql
psql -U luminotes postgres -f model/data.sql
To start the server in development mode, run: To start the server in development mode, run:
python2.5 luminotes.py -d python2.5 luminotes.py -d
@ -82,6 +105,24 @@ domain you're using. For instance:
"luminotes.http_url": "http://luminotes.com", "luminotes.http_url": "http://luminotes.com",
"luminotes.https_url": "https://luminotes.com", "luminotes.https_url": "https://luminotes.com",
Configure PostgreSQL's pg_hba.conf to require passwords for local connections:
local all all md5
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".
createuser -S -d -R -P -E luminotes
Initialize the database with the starting schema and basic data:
psql -U luminotes postgres -f model/schema.sql
psql -U luminotes postgres -f model/data.sql
Then to actually start the production mode server, run: Then to actually start the production mode server, run:
python2.5 luminotes.py python2.5 luminotes.py
@ -119,3 +160,21 @@ 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 have both Firefox and Internet Explorer 6 installed. Edit run_tests.py if you
need to specify different paths to the browser binaries or want to test with need to specify different paths to the browser binaries or want to test with
additional browsers. 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.

View File

@ -20,6 +20,11 @@ class Scheduler( object ):
self.add( self.__idle_thread() ) self.add( self.__idle_thread() )
self.__idle.acquire() # don't count the idle thread self.__idle.acquire() # don't count the idle thread
# TODO: Running the scheduler from anything other than the main Python thread somehow prevents
# tracebacks from within a generator from indicating the offending line and line number. So it
# would be really useful for debugging purposes to start the scheduler from the main thread.
# The reason that it's not done here is because CherryPy's blocking server must be started
# from the main Python thread.
self.__scheduler_thread = Thread( target = self.run ) self.__scheduler_thread = Thread( target = self.run )
self.__scheduler_thread.setDaemon( True ) self.__scheduler_thread.setDaemon( True )
self.__scheduler_thread.start() self.__scheduler_thread.start()

View File

@ -53,7 +53,7 @@ class Converter( object ):
"insert into notebook " + "insert into notebook " +
"( id, revision, name, trash_id ) " + "( id, revision, name, trash_id ) " +
"values ( %s, %s, %s, %s );" % "values ( %s, %s, %s, %s );" %
( quote( value.object_id ), quote( value.revision ), quote( value.name ), quote( value.trash and value.trash.object_id or "null" ) ) ( quote( value.object_id ), quote( value.revision ), quote( value.name ), value.trash and quote( value.trash.object_id ) or "null" )
) )
for note in value.notes: for note in value.notes:
@ -69,7 +69,7 @@ class Converter( object ):
"insert into note " + "insert into note " +
"( id, revision, title, contents, notebook_id, startup, deleted_from_id, rank ) " + "( id, revision, title, contents, notebook_id, startup, deleted_from_id, rank ) " +
"values ( %s, %s, %s, %s, %s, %s, %s, %s );" % "values ( %s, %s, %s, %s, %s, %s, %s, %s );" %
( quote( value.object_id ), quote( value.revision ), quote( value.title ), quote( value.contents ), quote( None ), quote( "f" ), quote( value.deleted_from or None ), quote( None ) ) ( quote( value.object_id ), quote( value.revision ), quote( value.title or None ), quote( value.contents or None ), quote( None ), quote( "f" ), quote( value.deleted_from or None ), quote( None ) )
) )
elif class_name == "User": elif class_name == "User":
if value.username is None: continue # note: this will skip all demo users if value.username is None: continue # note: this will skip all demo users

176
tools/verifyconvertdb.py Executable file
View File

@ -0,0 +1,176 @@
#!/usr/bin/python2.5
import os
import os.path
import psycopg2 as psycopg
from controller.Database import Database
from controller.Scheduler import Scheduler
def quote( value ):
if value is None:
return "null"
value = unicode( value )
return "'%s'" % value.replace( "'", "''" ).replace( "\\", "\\\\" )
class Verifier( object ):
"""
Verifies a conversion of a Luminotes database from bsddb to PostgreSQL that was performed with
convertdb.py.
"""
def __init__( self, scheduler, database ):
self.scheduler = scheduler
self.database = database
self.conn = psycopg.connect( "dbname=luminotes user=luminotes password=dev" )
self.cursor = self.conn.cursor()
thread = self.verify_database()
self.scheduler.add( thread )
self.scheduler.wait_for( thread )
def verify_database( self ):
inserts = set()
for key in self.database._Database__db.keys():
if not self.database._Database__db.get( key ):
continue
self.database.load( key, self.scheduler.thread )
value = ( yield Scheduler.SLEEP )
class_name = value.__class__.__name__
if class_name == "Notebook":
self.verify_notebook( value )
elif class_name == "Note":
self.cursor.execute(
"select * from note where id = %s and revision = %s;" % ( quote( value.object_id ), quote( value.revision ) )
)
for row in self.cursor.fetchmany():
assert row[ 0 ] == value.object_id
assert row[ 1 ].replace( tzinfo = None ) == value.revision
assert row[ 2 ] == ( value.title and value.title.encode( "utf8" ) or None )
assert row[ 3 ] == ( value.contents and value.contents.encode( "utf8" ) or None )
# not checking for existence of row 4 (notebook_id), because notes deleted from the trash don't have a notebook id
assert row[ 5 ] is not None
assert row[ 6 ] == ( value.deleted_from or None )
if row[ 5 ] is True: # if this is a startup note, it should have a rank
assert row[ 7 ] is not None
elif class_name == "User":
# skip demo users
if value.username is None: continue
self.cursor.execute(
"select * from luminotes_user where id = %s and revision = %s;" % ( quote( value.object_id ), quote( value.revision ) )
)
for row in self.cursor.fetchmany():
assert row[ 0 ] == value.object_id
assert row[ 1 ].replace( tzinfo = None ) == value.revision
assert row[ 2 ] == value.username
assert row[ 3 ] == value._User__salt
assert row[ 4 ] == value._User__password_hash
assert row[ 5 ] == value.email_address
assert row[ 6 ] == value.storage_bytes
assert row[ 7 ] == value.rate_plan
for notebook in value.notebooks:
if notebook is None: continue
read_write = ( notebook.__class__.__name__ == "Notebook" )
self.cursor.execute(
"select * from user_notebook where user_id = %s and notebook_id = %s;" % ( quote( value.object_id ), quote( notebook.object_id ) )
)
for row in self.cursor.fetchmany():
assert row[ 0 ] == value.object_id
assert row[ 1 ] == notebook.object_id
assert row[ 2 ] == read_write
self.verify_notebook( notebook )
elif class_name == "Read_only_notebook":
self.verify_notebook( value._Read_only_notebook__wrapped )
elif class_name == "Password_reset":
# skip password resets that are already redeemed
if value.redeemed: continue
self.cursor.execute(
"select * from password_reset where id = %s;" % quote( value.object_id )
)
for row in self.cursor.fetchmany():
assert row[ 0 ] == value.email_address
assert row[ 1 ] == False
assert row[ 2 ] == value.object_id
elif class_name == "User_list":
pass
else:
raise Exception( "Unverified value of type %s" % class_name )
self.conn.commit()
yield None
def verify_notebook( self, value ):
self.cursor.execute(
"select * from notebook where id = %s and revision = %s;" % ( quote( value.object_id ), quote( value.revision ) )
)
for row in self.cursor.fetchmany():
assert row[ 0 ] == value.object_id
assert row[ 1 ].replace( tzinfo = None ) == value.revision
assert row[ 2 ] == value.name
if value.trash:
assert row[ 3 ] == value.trash.object_id
else:
assert row[ 3 ] == None
startup_note_ids = [ note.object_id for note in value.startup_notes ]
for note in value.notes:
self.cursor.execute(
"select * from note where id = %s and revision = %s;" % ( quote( note.object_id ), quote( value.revision ) )
)
for row in self.cursor.fetchmany():
assert row[ 0 ] == note.object_id
assert row[ 1 ].replace( tzinfo = None ) == note.revision
assert row[ 2 ] == note.title
assert row[ 3 ] == note.contents
assert row[ 4 ] == value.object_id
assert row[ 5 ] == ( note.object_id in startup_note_ids )
assert row[ 6 ] == note.deleted_from
if row[ 5 ] is True: # if this is a startup note, it should have a rank
assert row[ 7 ] is not None
for note in value.startup_notes:
self.cursor.execute(
"select * from note where id = %s and revision = %s order by rank;" % ( quote( note.object_id ), quote( value.revision ) )
)
rank = 0
for row in self.cursor.fetchmany():
assert row[ 0 ] == note.object_id
assert row[ 1 ].replace( tzinfo = None ) == note.revision
assert row[ 2 ] == note.title
assert row[ 3 ] == note.contents
assert row[ 4 ] == value.object_id
assert row[ 5 ] == True
assert row[ 6 ] == note.deleted_from
assert row[ 7 ] == rank
rank += 1
def main():
scheduler = Scheduler()
database = Database( scheduler, "data.db" )
initializer = Verifier( scheduler, database )
scheduler.wait_until_idle()
if __name__ == "__main__":
main()