From 82bf5bca2909aa42a4a85991b2c6938a1c3fe90c Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sat, 1 Dec 2007 01:08:16 +0000 Subject: [PATCH] Schema and model changes to support invitations and multiple user collaboration: * added new database table of invited email addresses * altered user_notebook table to have a new owner boolean column, indicating whether the user has owner access to the notebook * altered notebook and note tables/views to have an additional user_id field to indicate the user that created that revision * updated model.Notebook and model.Note to support new user_id field --- model/Note.py | 23 ++++++++++++++-------- model/Notebook.py | 35 ++++++++++++++++++++------------- model/User.py | 2 +- model/delta/1.0.4.sql | 23 ++++++++++++++++++++++ model/schema.sql | 39 ++++++++++++++++++++++++++++++++----- model/test/Test_note.py | 11 ++++++++++- model/test/Test_notebook.py | 8 ++++++-- 7 files changed, 110 insertions(+), 31 deletions(-) create mode 100644 model/delta/1.0.4.sql diff --git a/model/Note.py b/model/Note.py index bf76576..c96aaec 100644 --- a/model/Note.py +++ b/model/Note.py @@ -10,7 +10,7 @@ class Note( Persistent ): 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, creation = None, summary = None ): + startup = None, deleted_from_id = None, rank = None, user_id = None, creation = None, summary = None ): """ Create a new note with the given id and contents. @@ -30,6 +30,8 @@ class Note( Persistent ): @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 + @type user_id: unicode or NoneType + @param user_id: id of the user who most recently updated this note object (optional) @type creation: datetime or NoneType @param creation: creation timestamp of the object (optional, defaults to None) @type summary: unicode or NoneType @@ -45,10 +47,11 @@ class Note( Persistent ): self.__startup = startup or False self.__deleted_from_id = deleted_from_id self.__rank = rank + self.__user_id = user_id self.__creation = creation @staticmethod - def create( object_id, contents = None, notebook_id = None, startup = None, rank = None, creation = None, summary = None ): + def create( object_id, contents = None, notebook_id = None, startup = None, rank = None, user_id = None, creation = None, summary = None ): """ Convenience constructor for creating a new note. @@ -62,6 +65,8 @@ class Note( Persistent ): @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 + @type user_id: unicode or NoneType + @param user_id: id of the user who most recently updated this note object (optional) @type creation: datetime or NoneType @param creation: creation timestamp of the object (optional, defaults to None) @type summary: unicode or NoneType @@ -69,7 +74,7 @@ class Note( Persistent ): @rtype: Note @return: newly constructed note """ - note = Note( object_id, notebook_id = notebook_id, startup = startup, rank = rank, creation = creation, summary = summary ) + note = Note( object_id, notebook_id = notebook_id, startup = startup, rank = rank, user_id = user_id, creation = creation, summary = summary ) note.contents = contents return note @@ -113,9 +118,9 @@ class Note( Persistent ): @staticmethod def sql_load( object_id, revision = None ): if revision: - return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank from note where id = %s and revision = %s;" % ( quote( object_id ), quote( revision ) ) + return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id from note where id = %s and revision = %s;" % ( quote( object_id ), quote( revision ) ) - return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank from note_current where id = %s;" % quote( object_id ) + return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id from note_current where id = %s;" % quote( object_id ) @staticmethod def sql_id_exists( object_id, revision = None ): @@ -133,11 +138,11 @@ class Note( Persistent ): 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 );" % \ + "insert into note ( id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id ) " + \ + "values ( %s, %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 ) + quote( self.__deleted_from_id ), rank, quote( self.user_id ) ) def sql_update( self ): return self.sql_create() @@ -152,6 +157,7 @@ class Note( Persistent ): summary = self.__summary, title = self.__title, deleted_from_id = self.__deleted_from_id, + user_id = self.__user_id, creation = self.__creation, ) ) @@ -164,4 +170,5 @@ class Note( Persistent ): 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 ) + user_id = property( lambda self: self.__user_id ) creation = property( lambda self: self.__creation ) diff --git a/model/Notebook.py b/model/Notebook.py index 86cae87..2700e0d 100644 --- a/model/Notebook.py +++ b/model/Notebook.py @@ -12,7 +12,7 @@ class Notebook( Persistent ): WHITESPACE_PATTERN = re.compile( r"\s+" ) SEARCH_OPERATORS = re.compile( r"[&|!()]" ) - def __init__( self, object_id, revision = None, name = None, trash_id = None, deleted = False, read_write = True ): + def __init__( self, object_id, revision = None, name = None, trash_id = None, deleted = False, user_id = None, read_write = True ): """ Create a new notebook with the given id and name. @@ -26,6 +26,8 @@ class Notebook( Persistent ): @param trash_id: id of the notebook where deleted notes from this notebook go to die (optional) @type deleted: bool or NoneType @param deleted: whether this notebook is currently deleted (optional, defaults to False) + @type user_id: unicode or NoneType + @param user_id: id of the user who most recently updated this notebook object (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 @@ -35,10 +37,11 @@ class Notebook( Persistent ): self.__name = name self.__trash_id = trash_id self.__deleted = deleted + self.__user_id = user_id self.__read_write = read_write @staticmethod - def create( object_id, name = None, trash_id = None, deleted = False, read_write = True ): + def create( object_id, name = None, trash_id = None, deleted = False, user_id = None, read_write = True ): """ Convenience constructor for creating a new notebook. @@ -50,12 +53,14 @@ class Notebook( Persistent ): @param trash_id: id of the notebook where deleted notes from this notebook go to die (optional) @type deleted: bool or NoneType @param deleted: whether this notebook is currently deleted (optional, defaults to False) + @type user_id: unicode or NoneType + @param user_id: id of the user who most recently updated this notebook object (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 ) + return Notebook( object_id, name = name, trash_id = trash_id, user_id = user_id, read_write = read_write ) @staticmethod def sql_load( object_id, revision = None ): @@ -76,10 +81,10 @@ class Notebook( Persistent ): def sql_create( self ): return \ - "insert into notebook ( id, revision, name, trash_id, deleted ) " + \ - "values ( %s, %s, %s, %s, %s );" % \ + "insert into notebook ( id, revision, name, trash_id, deleted, user_id ) " + \ + "values ( %s, %s, %s, %s, %s, %s );" % \ ( quote( self.object_id ), quote( self.revision ), quote( self.__name ), - quote( self.__trash_id ), quote( self.deleted ) ) + quote( self.__trash_id ), quote( self.deleted ), quote( self.user_id ) ) def sql_update( self ): return self.sql_create() @@ -88,19 +93,19 @@ class Notebook( Persistent ): """ Return a SQL string to load a list of all the notes within this notebook. """ - return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank from note_current where notebook_id = %s order by revision desc;" % quote( self.object_id ) + return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id 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 id, revision, title, contents, notebook_id, startup, deleted_from_id, rank from note_current where notebook_id = %s and startup = 'f' order by revision desc;" % quote( self.object_id ) + return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id 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 id, revision, title, contents, notebook_id, startup, deleted_from_id, rank from note_current where notebook_id = %s and startup = 't' order by rank;" % quote( self.object_id ) + return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id from note_current where notebook_id = %s and startup = 't' order by rank;" % quote( self.object_id ) def sql_load_recent_notes( self, start = 0, count = 10 ): """ @@ -116,7 +121,7 @@ class Notebook( Persistent ): select note_current.id, note_current.revision, note_current.title, note_current.contents, note_current.notebook_id, note_current.startup, note_current.deleted_from_id, - note_current.rank, note_creation.revision as creation + note_current.rank, note_current.user_id, note_creation.revision as creation from note_current, ( select id, min( revision ) as revision from note where notebook_id = %s group by id ) as note_creation @@ -134,7 +139,7 @@ class Notebook( Persistent ): @type note_id: unicode @param note_id: id of note to load """ - return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank from note_current where notebook_id = %s and id = %s;" % ( quote( self.object_id ), quote( note_id ) ) + return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id 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 ): """ @@ -143,7 +148,7 @@ class Notebook( Persistent ): @type note_id: unicode @param note_id: title of note to load """ - return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank from note_current where notebook_id = %s and title = %s;" % ( quote( self.object_id ), quote( title ) ) + return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id from note_current where notebook_id = %s and title = %s;" % ( quote( self.object_id ), quote( title ) ) def sql_search_notes( self, search_text ): """ @@ -161,10 +166,10 @@ class Notebook( Persistent ): return \ """ - select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, null, + select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id, null, headline( drop_html_tags( contents ), query ) as summary from ( select - id, revision, title, contents, notebook_id, startup, deleted_from_id, rank_cd( search, query ) as rank, null, query + id, revision, title, contents, notebook_id, startup, deleted_from_id, rank_cd( search, query ) as rank, user_id, null, query from note_current, to_tsquery( 'default', %s ) query where @@ -194,6 +199,7 @@ class Notebook( Persistent ): trash_id = self.__trash_id, read_write = self.__read_write, deleted = self.__deleted, + user_id = self.__user_id, ) ) return d @@ -215,3 +221,4 @@ class Notebook( Persistent ): trash_id = property( lambda self: self.__trash_id ) read_write = property( lambda self: self.__read_write, __set_read_write ) deleted = property( lambda self: self.__deleted, __set_deleted ) + user_id = property( lambda self: self.__user_id ) diff --git a/model/User.py b/model/User.py index 36a6485..12245d4 100644 --- a/model/User.py +++ b/model/User.py @@ -145,7 +145,7 @@ class User( Persistent ): return \ "select notebook_current.*, user_notebook.read_write from user_notebook, notebook_current " + \ - "where user_id = %s%s%s and user_notebook.notebook_id = notebook_current.id order by revision;" % \ + "where user_notebook.user_id = %s%s%s and user_notebook.notebook_id = notebook_current.id order by revision;" % \ ( quote( self.object_id ), parents_only_clause, undeleted_only_clause ) def sql_save_notebook( self, notebook_id, read_write = True ): diff --git a/model/delta/1.0.4.sql b/model/delta/1.0.4.sql new file mode 100644 index 0000000..855899c --- /dev/null +++ b/model/delta/1.0.4.sql @@ -0,0 +1,23 @@ +create table invite ( + id text not null, + revision timestamp with time zone not null, + sent_user_id text, + notebook_id text, + email_address text, + read_write boolean, + owner boolean, + redeemed_user_id text +); +alter table invite add primary key ( id ); +alter table user_notebook add column owner boolean default false; +update user_notebook set owner = 't' where read_write = 't'; +alter table notebook add column user_id text; +alter table note add column user_id text; +drop view notebook_current; +create view notebook_current as SELECT id, revision, name, trash_id, deleted, user_id + from notebook + where ( notebook.revision IN ( SELECT max( sub_notebook.revision ) AS max FROM notebook sub_notebook + where sub_notebook.id = notebook.id ) ); +drop view note_current; +create view note_current as select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, search, user_id + from note where ( note.revision in ( select max( sub_note.revision ) as max from note sub_note where sub_note.id = note.id ) ); diff --git a/model/schema.sql b/model/schema.sql index 9f6654c..9ce81ad 100644 --- a/model/schema.sql +++ b/model/schema.sql @@ -31,6 +31,24 @@ CREATE FUNCTION drop_html_tags(text) RETURNS text ALTER FUNCTION public.drop_html_tags(text) OWNER TO luminotes; +-- +-- Name: invite; Type: TABLE; Schema: public; Owner: luminotes; Tablespace: +-- + +CREATE TABLE invite ( + id text NOT NULL, + revision timestamp with time zone NOT NULL, + sent_user_id text, + notebook_id text, + email_address text, + read_write boolean, + "owner" boolean, + redeemed_user_id text +); + + +ALTER TABLE public.invite OWNER TO luminotes; + -- -- Name: luminotes_user; Type: TABLE; Schema: public; Owner: luminotes; Tablespace: -- @@ -72,7 +90,8 @@ CREATE TABLE note ( startup boolean DEFAULT false, deleted_from_id text, rank numeric, - search tsvector + search tsvector, + user_id text ); @@ -83,7 +102,7 @@ ALTER TABLE public.note OWNER TO luminotes; -- CREATE VIEW note_current AS - SELECT note.id, note.revision, note.title, note.contents, note.notebook_id, note.startup, note.deleted_from_id, note.rank, note.search FROM note WHERE (note.revision IN (SELECT max(sub_note.revision) AS max FROM note sub_note WHERE (sub_note.id = note.id))); + SELECT note.id, note.revision, note.title, note.contents, note.notebook_id, note.startup, note.deleted_from_id, note.rank, note.search, note.user_id FROM note WHERE (note.revision IN (SELECT max(sub_note.revision) AS max FROM note sub_note WHERE (sub_note.id = note.id))); ALTER TABLE public.note_current OWNER TO luminotes; @@ -97,7 +116,8 @@ CREATE TABLE notebook ( revision timestamp with time zone NOT NULL, name text, trash_id text, - deleted boolean DEFAULT false + deleted boolean DEFAULT false, + user_id text ); @@ -108,7 +128,7 @@ ALTER TABLE public.notebook OWNER TO luminotes; -- CREATE VIEW notebook_current AS - SELECT notebook.id, notebook.revision, notebook.name, notebook.trash_id, notebook.deleted FROM notebook WHERE (notebook.revision IN (SELECT max(sub_notebook.revision) AS max FROM notebook sub_notebook WHERE (sub_notebook.id = notebook.id))); + SELECT notebook.id, notebook.revision, notebook.name, notebook.trash_id, notebook.deleted, notebook.user_id FROM notebook WHERE (notebook.revision IN (SELECT max(sub_notebook.revision) AS max FROM notebook sub_notebook WHERE (sub_notebook.id = notebook.id))); ALTER TABLE public.notebook_current OWNER TO luminotes; @@ -134,12 +154,21 @@ ALTER TABLE public.password_reset OWNER TO luminotes; CREATE TABLE user_notebook ( user_id text NOT NULL, notebook_id text NOT NULL, - read_write boolean DEFAULT false + read_write boolean DEFAULT false, + "owner" boolean DEFAULT false ); ALTER TABLE public.user_notebook OWNER TO luminotes; +-- +-- Name: invite_pkey; Type: CONSTRAINT; Schema: public; Owner: luminotes; Tablespace: +-- + +ALTER TABLE ONLY invite + ADD CONSTRAINT invite_pkey PRIMARY KEY (id); + + -- -- Name: luminotes_user_pkey; Type: CONSTRAINT; Schema: public; Owner: luminotes; Tablespace: -- diff --git a/model/test/Test_note.py b/model/test/Test_note.py index d0920de..e62a1b5 100644 --- a/model/test/Test_note.py +++ b/model/test/Test_note.py @@ -12,10 +12,11 @@ class Test_note( object ): self.notebook_id = u"18" self.startup = False self.rank = 17.5 + self.user_id = u"me" self.creation = datetime.now() self.delta = timedelta( seconds = 1 ) - self.note = Note.create( self.object_id, self.contents, self.notebook_id, self.startup, self.rank, self.creation, self.summary ) + self.note = Note.create( self.object_id, self.contents, self.notebook_id, self.startup, self.rank, self.user_id, self.creation, self.summary ) def test_create( self ): assert self.note.object_id == self.object_id @@ -27,6 +28,7 @@ class Test_note( object ): assert self.note.startup == self.startup assert self.note.deleted_from_id == None assert self.note.rank == self.rank + assert self.note.user_id == self.user_id assert self.note.creation == self.creation def test_set_contents( self ): @@ -44,6 +46,7 @@ class Test_note( object ): assert self.note.startup == self.startup assert self.note.deleted_from_id == None assert self.note.rank == self.rank + assert self.note.user_id == self.user_id assert self.note.creation == self.creation def test_set_contents_with_html_title( self ): @@ -62,6 +65,7 @@ class Test_note( object ): assert self.note.startup == self.startup assert self.note.deleted_from_id == None assert self.note.rank == self.rank + assert self.note.user_id == self.user_id assert self.note.creation == self.creation def test_set_contents_with_multiple_titles( self ): @@ -80,6 +84,7 @@ class Test_note( object ): assert self.note.startup == self.startup assert self.note.deleted_from_id == None assert self.note.rank == self.rank + assert self.note.user_id == self.user_id assert self.note.creation == self.creation def test_set_summary( self ): @@ -96,6 +101,7 @@ class Test_note( object ): assert self.note.startup == self.startup assert self.note.deleted_from_id == None assert self.note.rank == self.rank + assert self.note.user_id == self.user_id assert self.note.creation == self.creation def test_set_notebook_id( self ): @@ -135,6 +141,7 @@ class Test_note( object ): assert d.get( "summary" ) == self.summary assert d.get( "title" ) == self.title assert d.get( "deleted_from_id" ) == None + assert d.get( "user_id" ) == self.user_id assert d.get( "creation" ) == self.note.creation @@ -147,6 +154,7 @@ class Test_note_blank( Test_note ): self.notebook_id = None self.startup = False self.rank = None + self.user_id = None self.creation = None self.delta = timedelta( seconds = 1 ) @@ -162,4 +170,5 @@ class Test_note_blank( Test_note ): assert self.note.startup == False assert self.note.deleted_from_id == None assert self.note.rank == None + assert self.note.user_id == None assert self.note.creation == None diff --git a/model/test/Test_notebook.py b/model/test/Test_notebook.py index 8331c99..4a8aac6 100644 --- a/model/test/Test_notebook.py +++ b/model/test/Test_notebook.py @@ -10,10 +10,11 @@ class Test_notebook( object ): self.trash_id = "18" self.name = u"my notebook" self.trash_name = u"trash" + self.user_id = u"me" self.delta = timedelta( seconds = 1 ) - self.trash = Notebook.create( self.trash_id, self.trash_name, read_write = False, deleted = False ) - self.notebook = Notebook.create( self.object_id, self.name, trash_id = self.trash.object_id, deleted = False ) + self.trash = Notebook.create( self.trash_id, self.trash_name, read_write = False, deleted = False, user_id = self.user_id ) + self.notebook = Notebook.create( self.object_id, self.name, trash_id = self.trash.object_id, deleted = False, user_id = self.user_id ) self.note = Note.create( "19", u"

title

blah" ) def test_create( self ): @@ -23,6 +24,7 @@ class Test_notebook( object ): assert self.notebook.read_write == True assert self.notebook.trash_id == self.trash_id assert self.notebook.deleted == False + assert self.notebook.user_id == self.user_id assert self.trash.object_id == self.trash_id assert datetime.now( tz = utc ) - self.trash.revision < self.delta @@ -30,6 +32,7 @@ class Test_notebook( object ): assert self.trash.read_write == False assert self.trash.trash_id == None assert self.trash.deleted == False + assert self.trash.user_id == self.user_id def test_set_name( self ): new_name = u"my new notebook" @@ -60,5 +63,6 @@ class Test_notebook( object ): assert d.get( "trash_id" ) == self.trash.object_id assert d.get( "read_write" ) == True assert d.get( "deleted" ) == self.notebook.deleted + assert d.get( "user_id" ) == self.notebook.user_id assert d.get( "object_id" ) == self.notebook.object_id assert datetime.now( tz = utc ) - d.get( "revision" ) < self.delta