1
0
Fork 0

Foundational work for both tags and discussion forums. Should have checked this in in smaller pieces.

master
Dan Helfman 14 years ago
parent 86e5e38d69
commit 388f2fcb02
  1. 8
      NEWS
  2. 38
      UPGRADE
  3. 2
      config/Version.py
  4. 22
      controller/Files.py
  5. 62
      controller/Forums.py
  6. 192
      controller/Notebooks.py
  7. 4
      controller/Root.py
  8. 71
      controller/Users.py
  9. 2
      controller/test/Test_database.py
  10. 171
      controller/test/Test_notebooks.py
  11. 12
      controller/test/Test_root.py
  12. 320
      controller/test/Test_users.py
  13. 7
      luminotes.py
  14. 72
      model/Notebook.py
  15. 135
      model/Tag.py
  16. 67
      model/User.py
  17. 30
      model/delta/1.5.5.sql
  18. 37
      model/delta/1.5.5.sqlite
  19. 3
      model/drop.sql
  20. 246
      model/schema.sql
  21. 163
      model/schema.sqlite
  22. 124
      model/test/Test_notebook.py
  23. 36
      model/test/Test_tag.py
  24. 1
      setup.py
  25. 4
      static/html/support.html
  26. 63
      static/js/Wiki.js
  27. 2
      tools/initdb.py
  28. 113
      tools/make_forum_thread.py
  29. 15
      view/Forums_page.py
  30. 14
      view/Link_area.py
  31. 16
      view/Main_page.py

@ -1,3 +1,11 @@
1.5.5:
* Improved speed of Luminotes Desktop by adding some database indices. This
will help in particular for larger notebooks with many notes.
* Added some code to automatically upgrade your database when upgrading to a
new Luminotes release. This applies to all Luminotes products.
* Added code to support Luminotes discussion forums.
* Laid some of the foundational groundwork for future tags support.
1.5.4: October 9, 2008
* Fixed a visual bug in which clicking up or down to reorder your notebooks
didn't display correctly.

@ -1,18 +1,44 @@
When upgrading Luminotes, if you are using memcached, it is recommended
that you restart memcached to clear your cache.
Upgrading from Luminotes 1.5.0 or higher
----------------------------------------
If you're using Luminotes 1.5.0 or higher and you'd like to upgrade to a
newer version, Luminotes will automatically upgrade your database when
you start Luminotes after an upgrade. This means that all of your notes
and notebooks created in an older versions of Luminotes will be included
in the upgrade. You don't have to do a thing other than install the
software for the new release, and then execute the following command:
export PYTHONPATH=.
python2.4 tools/updatedb.py
Upgrading from Luminotes 1.0, 1.2, 1.3, or 1.4
----------------------------------------------
If you're using an older version of Luminotes (prior to 1.5.0) and you'd
like to upgrade to a newer version, you'll have to perform database
upgrades manually. Below are the intructions for doing so.
To upgrade the Luminotes database from an earlier version, manually apply each
relevant schema delta file within model/delta/
For instance, if you were upgrading from version 5.0.1 to 5.0.4, you would
For instance, if you are upgrading from version 1.3.12 to 1.5.0, you would
apply the following deltas in order:
psql -U luminotes luminotes -f model/delta/5.0.2.sql
psql -U luminotes luminotes -f model/delta/5.0.3.sql
psql -U luminotes luminotes -f model/delta/5.0.4.sql
psql -U luminotes luminotes -f model/delta/1.3.14.sql
psql -U luminotes luminotes -f model/delta/1.4.0.sql
psql -U luminotes luminotes -f model/delta/1.5.0.sql
Any version which does not introduce a schema change does not have a
corresponding schema delta file.
Sometimes I include comments within a schema delta file with additional
manual steps you need to take.
IMPORTANT: Even if you are upgrading past version 1.5.0 to a newer version,
you should stop applying schema delta files after 1.5.0. This is because the
Luminotes automatic schema upgrade process will pick up after that point.
After you've updated the schema, run the updatedb.py script:

@ -1 +1 @@
VERSION = u"1.5.4"
VERSION = u"1.5.5"

@ -290,7 +290,7 @@ class Files( object ):
"""
db_file = self.__database.load( File, file_id )
if not db_file or not self.__users.check_access( user_id, db_file.notebook_id ):
if not db_file or not self.__users.load_notebook( user_id, db_file.notebook_id ):
raise Access_error()
# if the file is openable as an image, then allow the user to view it instead of downloading it
@ -396,7 +396,7 @@ class Files( object ):
"""
db_file = self.__database.load( File, file_id )
if not db_file or not self.__users.check_access( user_id, db_file.notebook_id ):
if not db_file or not self.__users.load_notebook( user_id, db_file.notebook_id ):
raise Access_error()
filename = db_file.filename.replace( '"', r"\"" )
@ -432,7 +432,7 @@ class Files( object ):
"""
db_file = self.__database.load( File, file_id )
if not db_file or not self.__users.check_access( user_id, db_file.notebook_id ):
if not db_file or not self.__users.load_notebook( user_id, db_file.notebook_id ):
raise Access_error()
cherrypy.response.headerMap[ u"Content-Type" ] = u"image/png"
@ -491,7 +491,7 @@ class Files( object ):
"""
db_file = self.__database.load( File, file_id )
if not db_file or not self.__users.check_access( user_id, db_file.notebook_id ):
if not db_file or not self.__users.load_notebook( user_id, db_file.notebook_id ):
raise Access_error()
cherrypy.response.headerMap[ u"Content-Type" ] = db_file.content_type
@ -531,7 +531,7 @@ class Files( object ):
@return: rendered HTML page
@raise Access_error: the current user doesn't have access to the given notebook
"""
if not self.__users.check_access( user_id, notebook_id, read_write = True ):
if not self.__users.load_notebook( user_id, notebook_id, read_write = True, note_id = note_id ):
raise Access_error()
file_id = self.__database.next_id( File )
@ -565,7 +565,7 @@ class Files( object ):
@return: rendered HTML page
@raise Access_error: the current user doesn't have access to the given notebook
"""
if not self.__users.check_access( user_id, notebook_id, read_write = True ):
if not self.__users.load_notebook( user_id, notebook_id, read_write = True ):
raise Access_error()
file_id = self.__database.next_id( File )
@ -622,7 +622,7 @@ class Files( object ):
current_uploads_lock.release()
user = self.__database.load( User, user_id )
if not user or not self.__users.check_access( user_id, notebook_id, read_write = True ):
if not user or not self.__users.load_notebook( user_id, notebook_id, read_write = True ):
uploaded_file.delete()
return dict( script = general_error_script % u"Sorry, you don't have access to do that. Please make sure you're logged in as the correct user." )
@ -739,7 +739,7 @@ class Files( object ):
"""
db_file = self.__database.load( File, file_id )
if not db_file or not self.__users.check_access( user_id, db_file.notebook_id ):
if not db_file or not self.__users.load_notebook( user_id, db_file.notebook_id ):
raise Access_error()
user = self.__database.load( User, user_id )
@ -778,7 +778,7 @@ class Files( object ):
"""
db_file = self.__database.load( File, file_id )
if not db_file or not self.__users.check_access( user_id, db_file.notebook_id, read_write = True ):
if not db_file or not self.__users.load_notebook( user_id, db_file.notebook_id, read_write = True ):
raise Access_error()
self.__database.execute( db_file.sql_delete(), commit = False )
@ -817,7 +817,7 @@ class Files( object ):
"""
db_file = self.__database.load( File, file_id )
if not db_file or not self.__users.check_access( user_id, db_file.notebook_id, read_write = True ):
if not db_file or not self.__users.load_notebook( user_id, db_file.notebook_id, read_write = True ):
raise Access_error()
db_file.filename = filename
@ -919,7 +919,7 @@ class Files( object ):
db_file = self.__database.load( File, file_id )
if not db_file or not self.__users.check_access( user_id, db_file.notebook_id ):
if not db_file or not self.__users.load_notebook( user_id, db_file.notebook_id ):
raise Access_error()
parser = self.parse_csv( file_id )

@ -1,26 +1,35 @@
import cherrypy
from model.User import User
from model.Notebook import Notebook
from Expose import expose
from Validate import validate
from Validate import validate, Valid_string
from Database import Valid_id, end_transaction
from Users import grab_user_id
from Notebooks import Notebooks
from view.Forums_page import Forums_page
from view.Forum_page import Forum_page
from view.Main_page import Main_page
class Forums( object ):
"""
Controller for dealing with discussion forums, corresponding to the "/forums" URL.
"""
def __init__( self, database, users ):
def __init__( self, database, notebooks, users ):
"""
Create a new Forums object.
@type database: controller.Database
@param database: database that forums are stored in
@type notebooks: controller.Users
@param notebooks: controller for all notebooks
@type users: controller.Users
@param users: controller for all users
@rtype: Forums
@return: newly constructed Forums
"""
self.__database = database
self.__notebooks = notebooks
self.__users = users
@expose( view = Forums_page )
@ -32,6 +41,34 @@ class Forums( object ):
def index( self, user_id ):
"""
Provide the information necessary to display the listing of available forums (currently hard-coded).
@type user_id: unicode or NoneType
@param user_id: id of the current user
"""
result = self.__users.current( user_id )
parents = [ notebook for notebook in result[ u"notebooks" ] if notebook.trash_id and not notebook.deleted ]
if len( parents ) > 0:
result[ "first_notebook" ] = parents[ 0 ]
else:
result[ "first_notebook" ] = None
return result
@expose( view = Forum_page )
@end_transaction
@grab_user_id
@validate(
forum_name = Valid_string( max = 100 ),
user_id = Valid_id( none_okay = True ),
)
def default( self, forum_name, user_id ):
"""
Provide the information necessary to display the current threads within a forum.
@type forum_name: unicode
@param forum_name: name of the forum to display
@type user_id: unicode or NoneType
@param user_id: id of the current user
"""
result = self.__users.current( user_id )
parents = [ notebook for notebook in result[ u"notebooks" ] if notebook.trash_id and not notebook.deleted ]
@ -40,4 +77,25 @@ class Forums( object ):
else:
result[ "first_notebook" ] = None
anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ), use_cache = True )
if anonymous is None:
raise Access_error()
# TODO: this needs to sort by either thread/note creation or modification date
threads = self.__database.select_many(
Notebook,
anonymous.sql_load_notebooks(
parents_only = False, undeleted_only = True, tag_name = u"forum", tag_value = forum_name
)
)
# if there are no matching threads, then this forum doesn't exist
if len( threads ) == 0:
raise cherrypy.NotFound
result[ "forum_name" ] = forum_name
result[ "threads" ] = threads
return result
# threads() is just an alias for Notebooks.default()
threads = Notebooks.default

@ -135,7 +135,7 @@ class Notebooks( object ):
]
if len( result[ u"notebooks" ] ) == 0:
raise Access_error()
result[ u"notebooks" ][ 0 ].read_write = False
result[ u"notebooks" ][ 0 ].read_write = Notebook.READ_ONLY
result[ u"notebooks" ][ 0 ].owner = False
elif preview in ( u"owner", u"default", None ):
read_write = True
@ -148,9 +148,8 @@ class Notebooks( object ):
if revision:
result[ "note_read_write" ] = False
notebook = self.__database.load( Notebook, notebook_id )
if not notebook:
raise Access_error()
notebook = result[ u"notebook" ]
if notebook.name != u"Luminotes":
result[ "recent_notes" ] = self.__database.select_many( Note, notebook.sql_load_notes( start = 0, count = 10 ) )
@ -181,9 +180,11 @@ class Notebooks( object ):
@type previous_revision: unicode or NoneType
@param previous_revision: older revision timestamp to diff with the given revision (optional)
@type read_write: bool or NoneType
@param read_write: whether the notebook should be returned as read-write (optional, defaults to True)
@param read_write: whether the notebook should be returned as read-write (optional, defaults to True).
this can only lower access, not elevate it
@type owner: bool or NoneType
@param owner: whether the notebook should be returned as owner-level access (optional, defaults to True)
@param owner: whether the notebook should be returned as owner-level access (optional, defaults to True).
this can only lower access, not elevate it
@type user_id: unicode or NoneType
@param user_id: id of current logged-in user (if any)
@rtype: dict
@ -197,23 +198,16 @@ class Notebooks( object ):
@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()
notebook = self.__database.load( Notebook, notebook_id )
notebook = self.__users.load_notebook( user_id, notebook_id )
if notebook is None:
raise Access_error()
if read_write is False:
notebook.read_write = False
elif not self.__users.check_access( user_id, notebook_id, read_write = True ):
notebook.read_write = False
notebook.read_write = Notebook.READ_ONLY
if owner is False:
notebook.owner = False
elif not self.__users.check_access( user_id, notebook_id, owner = True ):
notebook.owner = False
if note_id:
note = self.__database.load( Note, note_id, revision )
@ -234,7 +228,7 @@ class Notebooks( object ):
startup_notes = self.__database.select_many( Note, notebook.sql_load_startup_notes() )
total_notes_count = self.__database.select_one( int, notebook.sql_count_notes(), use_cache = True )
if self.__users.check_access( user_id, notebook_id, owner = True ):
if self.__users.load_notebook( user_id, notebook_id, owner = True ):
invites = self.__database.select_many( Invite, Invite.sql_load_notebook_invites( notebook_id ) )
else:
invites = []
@ -350,7 +344,9 @@ class Notebooks( object ):
@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 ):
notebook = self.__users.load_notebook( user_id, notebook_id )
if not notebook:
raise Access_error()
note = self.__database.load( Note, note_id, revision )
@ -362,8 +358,7 @@ class Notebooks( object ):
)
if note and note.notebook_id != notebook_id:
notebook = self.__database.load( Notebook, notebook_id )
if notebook and note.notebook_id == notebook.trash_id:
if note.notebook_id == notebook.trash_id:
if revision:
return dict(
note = summarize and self.summarize_note( note ) or note,
@ -414,15 +409,12 @@ 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
"""
if not self.__users.check_access( user_id, notebook_id ):
raise Access_error()
notebook = self.__users.load_notebook( user_id, notebook_id )
notebook = self.__database.load( Notebook, notebook_id )
if not notebook:
raise Access_error()
if notebook is None:
note = None
else:
note = self.__database.select_one( Note, notebook.sql_load_note_by_title( note_title ) )
note = self.__database.select_one( Note, notebook.sql_load_note_by_title( note_title ) )
return dict(
note = summarize and self.summarize_note( note ) or note,
@ -520,15 +512,12 @@ 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
"""
if not self.__users.check_access( user_id, notebook_id ):
raise Access_error()
notebook = self.__users.load_notebook( user_id, notebook_id )
notebook = self.__database.load( Notebook, notebook_id )
if not notebook:
raise Access_error()
if notebook is None:
note = None
else:
note = self.__database.select_one( Note, notebook.sql_load_note_by_title( note_title ) )
note = self.__database.select_one( Note, notebook.sql_load_note_by_title( note_title ) )
return dict(
note_id = note and note.object_id or None,
@ -558,7 +547,9 @@ class Notebooks( object ):
@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 ):
notebook = self.__users.load_notebook( user_id, notebook_id )
if not notebook:
raise Access_error()
note = self.__database.load( Note, note_id )
@ -570,8 +561,7 @@ class Notebooks( object ):
)
if note.notebook_id != notebook_id:
notebook = self.__database.load( Notebook, notebook_id )
if notebook and note.notebook_id == notebook.trash_id:
if note.notebook_id == notebook.trash_id:
return dict(
revisions = None,
)
@ -610,10 +600,8 @@ class Notebooks( object ):
@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()
notebook = self.__users.load_notebook( user_id, notebook_id )
notebook = self.__database.load( Notebook, notebook_id )
if not notebook:
raise Access_error()
@ -696,17 +684,19 @@ 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
"""
if not self.__users.check_access( user_id, notebook_id, read_write = True ):
raise Access_error()
notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, note_id = note_id )
user = self.__database.load( User, user_id )
notebook = self.__database.load( Notebook, notebook_id )
if not user or not notebook:
raise Access_error()
raise Access_error();
note = self.__database.load( Note, note_id )
# if the user has read-write access only to their own notes in this notebook, force the startup
# flag to be True for this note
if notebook.read_write == Notebook.READ_WRITE_FOR_OWN_NOTES:
startup = True
# check whether the provided note contents have been changed since the previous revision
def update_note( current_notebook, old_note, startup, user ):
# the note hasn't been changed, so bail without updating it
@ -805,11 +795,8 @@ 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
"""
if not self.__users.check_access( user_id, notebook_id, read_write = True ):
raise Access_error()
notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True )
user = self.__database.load( User, user_id )
notebook = self.__database.load( Notebook, notebook_id )
if not user or not notebook:
raise Access_error()
@ -819,6 +806,9 @@ class Notebooks( object ):
if not note:
raise Access_error()
if not self.__users.load_notebook( user_id, note.notebook_id, read_write = True, note_id = note.object_id ):
raise Access_error()
# check whether the provided note contents have been changed since the previous revision
def update_note( current_notebook, old_note, user ):
# if the revision to revert to is already the newest revision, bail without updating the note
@ -895,10 +885,7 @@ 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
"""
if not self.__users.check_access( user_id, notebook_id, read_write = True ):
raise Access_error()
notebook = self.__database.load( Notebook, notebook_id )
notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, note_id = note_id )
if not notebook:
raise Access_error()
@ -949,10 +936,7 @@ 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
"""
if not self.__users.check_access( user_id, notebook_id, read_write = True ):
raise Access_error()
notebook = self.__database.load( Notebook, notebook_id )
notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, note_id = note_id )
if not notebook:
raise Access_error()
@ -1005,12 +989,9 @@ 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
"""
if not self.__users.check_access( user_id, notebook_id, read_write = True ):
raise Access_error()
notebook = self.__database.load( Notebook, notebook_id )
notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True )
if not notebook:
if not notebook or notebook.read_write == Notebook.READ_WRITE_FOR_OWN_NOTES:
raise Access_error()
notes = self.__database.select_many( Note, notebook.sql_load_notes() )
@ -1063,7 +1044,9 @@ class Notebooks( object ):
@raise Validation_error: one of the arguments is invalid
@raise Search_error: the provided search_text is invalid
"""
if not self.__users.check_access( user_id, notebook_id ):
notebook = self.__users.load_notebook( user_id, notebook_id )
if not notebook:
raise Access_error()
MAX_SEARCH_TEXT_LENGTH = 256
@ -1112,14 +1095,19 @@ class Notebooks( object ):
@raise Validation_error: one of the arguments is invalid
@raise Search_error: the provided search_text is invalid
"""
if not self.__users.check_access( user_id, notebook_id ):
raise Access_error()
# if the anonymous user has access to the given notebook, then run the search as the anonymous
# user instead of the given user id
if self.__users.check_access( user_id = None, notebook_id = notebook_id ) is True:
anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ), use_cache = True )
anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ), use_cache = True )
if not anonymous:
raise Access_error()
notebook = self.__users.load_notebook( anonymous.object_id, notebook_id )
if notebook:
user_id = anonymous.object_id
else:
notebook = self.__users.load_notebook( user_id, notebook_id )
if not notebook:
raise Access_error()
MAX_SEARCH_TEXT_LENGTH = 256
if len( search_text ) > MAX_SEARCH_TEXT_LENGTH:
@ -1162,10 +1150,7 @@ 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
"""
if not self.__users.check_access( user_id, notebook_id ):
raise Access_error()
notebook = self.__database.load( Notebook, notebook_id )
notebook = self.__users.load_notebook( user_id, notebook_id )
if not notebook:
raise Access_error()
@ -1197,10 +1182,7 @@ 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
"""
if not self.__users.check_access( user_id, notebook_id ):
raise Access_error()
notebook = self.__database.load( Notebook, notebook_id )
notebook = self.__users.load_notebook( user_id, notebook_id )
if not notebook:
raise Access_error()
@ -1234,10 +1216,7 @@ 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
"""
if not self.__users.check_access( user_id, notebook_id ):
raise Access_error()
notebook = self.__database.load( Notebook, notebook_id )
notebook = self.__users.load_notebook( user_id, notebook_id )
if not notebook:
raise Access_error()
@ -1346,13 +1325,10 @@ 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
"""
notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, owner = True )
user = self.__database.load( User, user_id )
if not self.__users.check_access( user_id, notebook_id, read_write = True, owner = True ):
raise Access_error()
notebook = self.__database.load( Notebook, notebook_id )
if not notebook:
if not user or not notebook:
raise Access_error()
# prevent renaming of the trash notebook to anything
@ -1399,14 +1375,10 @@ class Notebooks( object ):
if user_id is None:
raise Access_error()
notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, owner = True )
user = self.__database.load( User, user_id )
if not self.__users.check_access( user_id, notebook_id, read_write = True, owner = True ):
raise Access_error()
notebook = self.__database.load( Notebook, notebook_id )
if not notebook:
if not user or not notebook:
raise Access_error()
# prevent deletion of a trash notebook directly
@ -1454,14 +1426,10 @@ class Notebooks( object ):
if user_id is None:
raise Access_error()
notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, owner = True )
user = self.__database.load( User, user_id )
if not self.__users.check_access( user_id, notebook_id, read_write = True, owner = True ):
raise Access_error()
notebook = self.__database.load( Notebook, notebook_id )
if not notebook:
if not user or not notebook:
raise Access_error()
# prevent deletion of a trash notebook directly
@ -1498,10 +1466,7 @@ class Notebooks( object ):
if user_id is None:
raise Access_error()
if not self.__users.check_access( user_id, notebook_id, read_write = True, owner = True ):
raise Access_error()
notebook = self.__database.load( Notebook, notebook_id )
notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, owner = True )
if not notebook:
raise Access_error()
@ -1537,11 +1502,10 @@ 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
"""
if not self.__users.check_access( user_id, notebook_id ):
raise Access_error()
notebook = self.__users.load_notebook( user_id, notebook_id )
user = self.__database.load( User, user_id )
if not user:
if not user or not notebook:
raise Access_error()
# load the notebooks to which this user has access
@ -1609,11 +1573,10 @@ 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
"""
if not self.__users.check_access( user_id, notebook_id ):
raise Access_error()
notebook = self.__users.load_notebook( user_id, notebook_id )
user = self.__database.load( User, user_id )
if not user:
if not user or not notebook:
raise Access_error()
# load the notebooks to which this user has access
@ -1676,7 +1639,6 @@ class Notebooks( object ):
Provide the information necessary to display a notebook's recent updated/created notes, in
reverse chronological order by update time.
@type notebook_id: unicode
@param notebook_id: id of the notebook containing the notes
@type start: unicode or NoneType
@ -1689,10 +1651,7 @@ class Notebooks( object ):
@return: { 'notes': recent_notes_list }
@raise Access_error: the current user doesn't have access to the given notebook or note
"""
if not self.__users.check_access( user_id, notebook_id ):
raise Access_error()
notebook = self.__database.load( Notebook, notebook_id )
notebook = self.__users.load_notebook( user_id, notebook_id )
if notebook is None:
raise Access_error()
@ -1720,10 +1679,7 @@ class Notebooks( object ):
@return: data for Main_page() constructor
@raise Access_error: the current user doesn't have access to the given notebook or note
"""
if not self.__users.check_access( user_id, notebook_id ):
raise Access_error()
notebook = self.__database.load( Notebook, notebook_id )
notebook = self.__users.load_notebook( user_id, notebook_id )
if notebook is None:
raise Access_error()
@ -1798,7 +1754,7 @@ class Notebooks( object ):
raise Access_error()
db_file = self.__database.load( File, file_id )
if db_file is None or not self.__users.check_access( user_id, db_file.notebook_id ):
if db_file is None or not self.__users.load_notebook( user_id, db_file.notebook_id ):
raise Access_error()
# if the file has a "note_id" header column, record its index

@ -58,7 +58,7 @@ class Root( object ):
settings[ u"global" ].get( u"luminotes.download_products", [] ),
)
self.__notebooks = Notebooks( database, self.__users, self.__files, settings[ u"global" ].get( u"luminotes.https_url", u"" ) )
self.__forums = Forums( database, self.__users )
self.__forums = Forums( database, self.__notebooks, self.__users )
self.__suppress_exceptions = suppress_exceptions # used for unit tests
@expose( Main_page )
@ -486,4 +486,4 @@ class Root( object ):
users = property( lambda self: self.__users )
groups = property( lambda self: self.__groups )
files = property( lambda self: self.__files )
# forums = property( lambda self: self.__forums )
forums = property( lambda self: self.__forums )

@ -702,34 +702,62 @@ class Users( object ):
return user
def check_access( self, user_id, notebook_id, read_write = False, owner = False ):
def load_notebook( self, user_id, notebook_id, read_write = False, owner = False, note_id = None ):
"""
Determine whether the given user has access to the given notebook.
Determine whether the given user has access to the given notebook, and if so, return that
notebook.
If the notebook.read_write member is READ_WRITE_FOR_OWN_NOTES, and a particular note_id is
given, then make sure that the given note_id is one of the user's own notes.
@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)
@type read_write: boolean
@param read_write: True if the notebook must be READ_WRITE or READ_WRITE_FOR_OWN_NOTES,
False if read-write access is not to be checked (defaults to False)
@type owner: bool
@param owner: True if owner-level access is being checked (defaults to False)
@rtype: bool
@return: True if the user has access
@type note_id: unicode
@param note_id: id of the note in the given notebook that the user is trying to access.
if the notebook is READ_WRITE_FOR_OWN_NOTES, then the given note is checked
to make sure its user_id is the same as the given user_id. for READ_WRITE
and READ_ONLY notebooks, this note_id parameter is ignored
@rtype: Notebook or NoneType
@return: the loaded notebook if the user has access to it, None otherwise
"""
anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ), use_cache = True )
notebook = self.__database.select_one( Notebook, anonymous.sql_load_notebooks( notebook_id = notebook_id ) )
if self.__database.select_one( bool, anonymous.sql_has_access( notebook_id, read_write, owner ) ):
return True
if user_id:
# check if the given user has access to this notebook
if not notebook and user_id:
user = self.__database.load( User, user_id )
if not user:
return None
if user and self.__database.select_one( bool, user.sql_has_access( notebook_id, read_write, owner ) ):
return True
notebook = self.__database.select_one( Notebook, user.sql_load_notebooks( notebook_id = notebook_id ) )
return False
# if the user has no access to this notebook, bail
if notebook is None:
return None
if read_write and notebook.read_write == Notebook.READ_ONLY:
return None
if owner and not notebook.owner:
return None
# if a particular note_id is given, and the notebook is READ_WRITE_FOR_OWN_NOTES, then check
# that the user is associated with that note
if note_id and notebook.read_write == Notebook.READ_WRITE_FOR_OWN_NOTES:
note = self.__database.load( Note, note_id )
if not note:
return None
if user_id != note.user_id or notebook_id != note.notebook_id:
return None
return notebook
def check_group( self, user_id, group_id, admin = False ):
"""
@ -1006,7 +1034,9 @@ class Users( object ):
if len( email_addresses ) > 5000:
raise Invite_error( u"Please enter fewer email addresses." )
if not self.check_access( user_id, notebook_id, read_write = True, owner = True ):
notebook = self.load_notebook( user_id, notebook_id, read_write = True, owner = True )
if not notebook:
raise Access_error()
# except for viewer-only invites, this feature requires a rate plan above basic
@ -1027,10 +1057,6 @@ class Users( object ):
else:
raise Access_error()
notebook = self.__database.load( Notebook, notebook_id )
if notebook is None:
raise Access_error()
# parse email_addresses string into individual email addresses
email_addresses_list = set()
for piece in WHITESPACE_OR_COMMA_PATTERN.split( email_addresses ):
@ -1136,12 +1162,13 @@ class Users( object ):
@raise Validation_error: one of the arguments is invalid
@raise Access_error: user_id doesn't have owner-level notebook access to revoke an invite
"""
if not self.check_access( user_id, notebook_id, read_write = True, owner = True ):
notebook = self.load_notebook( user_id, notebook_id, read_write = True, owner = True )
if not notebook:
raise Access_error()
invite = self.__database.load( Invite, invite_id )
notebook = self.__database.load( Notebook, notebook_id )
if not notebook or not invite or not invite.email_address or invite.notebook_id != notebook_id:
if not invite or not invite.email_address or invite.notebook_id != notebook_id:
raise Access_error()
self.__database.execute(

@ -10,7 +10,7 @@ from controller.Database import Database, Connection_wrapper
class Test_database( object ):
def setUp( self ):
# make an in-memory sqlite database to use in place of PostgreSQL during testing
# make an in-memory sqlite database to use during testing
self.connection = Connection_wrapper( sqlite.connect( ":memory:", detect_types = sqlite.PARSE_DECLTYPES, check_same_thread = False ) )
self.cache = Stub_cache()
cursor = self.connection.cursor()

@ -175,13 +175,13 @@ class Test_notebooks( Test_controller ):
assert result.get( u"user" ).object_id == self.user.object_id
assert len( result.get( u"notebooks" ) ) == 3
assert result.get( u"notebooks" )[ 2 ].object_id == self.notebook.object_id
assert result.get( u"notebooks" )[ 2 ].read_write == True
assert result.get( u"notebooks" )[ 2 ].read_write == Notebook.READ_WRITE
assert result.get( u"notebooks" )[ 2 ].owner == True
assert result.get( u"login_url" ) is None
assert result.get( u"logout_url" )
assert result.get( u"rate_plan" )
assert result.get( u"notebook" ).object_id == self.notebook.object_id
assert result.get( u"notebook" ).read_write == True
assert result.get( u"notebook" ).read_write == Notebook.READ_WRITE
assert result.get( u"notebook" ).owner == True
assert len( result.get( u"startup_notes" ) ) == 1
assert result[ "total_notes_count" ] == 2
@ -211,13 +211,13 @@ class Test_notebooks( Test_controller ):
assert result.get( u"user" ).object_id == self.user.object_id
assert len( result.get( u"notebooks" ) ) == 1
assert result.get( u"notebooks" )[ 0 ].object_id == self.notebook.object_id
assert result.get( u"notebooks" )[ 0 ].read_write == False
assert result.get( u"notebooks" )[ 0 ].read_write == Notebook.READ_ONLY
assert result.get( u"notebooks" )[ 0 ].owner == False
assert result.get( u"login_url" ) is None
assert result.get( u"logout_url" )
assert result.get( u"rate_plan" )
assert result.get( u"notebook" ).object_id == self.notebook.object_id
assert result.get( u"notebook" ).read_write == False
assert result.get( u"notebook" ).read_write == Notebook.READ_ONLY
assert result.get( u"notebook" ).owner == False
assert len( result.get( u"startup_notes" ) ) == 1
assert result[ "total_notes_count" ] == 2
@ -247,13 +247,13 @@ class Test_notebooks( Test_controller ):
assert result.get( u"user" ).object_id == self.user.object_id
assert len( result.get( u"notebooks" ) ) == 1
assert result.get( u"notebooks" )[ 0 ].object_id == self.notebook.object_id
assert result.get( u"notebooks" )[ 0 ].read_write == True
assert result.get( u"notebooks" )[ 0 ].read_write == Notebook.READ_WRITE
assert result.get( u"notebooks" )[ 0 ].owner == False
assert result.get( u"login_url" ) is None
assert result.get( u"logout_url" )
assert result.get( u"rate_plan" )
assert result.get( u"notebook" ).object_id == self.notebook.object_id
assert result.get( u"notebook" ).read_write == True
assert result.get( u"notebook" ).read_write == Notebook.READ_WRITE
assert result.get( u"notebook" ).owner == False
assert len( result.get( u"startup_notes" ) ) == 1
assert result[ "total_notes_count" ] == 2
@ -283,13 +283,13 @@ class Test_notebooks( Test_controller ):
assert result.get( u"user" ).object_id == self.user.object_id
assert len( result.get( u"notebooks" ) ) == 3
assert result.get( u"notebooks" )[ 2 ].object_id == self.notebook.object_id
assert result.get( u"notebooks" )[ 2 ].read_write == True
assert result.get( u"notebooks" )[ 2 ].read_write == Notebook.READ_WRITE
assert result.get( u"notebooks" )[ 2 ].owner == True
assert result.get( u"login_url" ) is None
assert result.get( u"logout_url" )
assert result.get( u"rate_plan" )
assert result.get( u"notebook" ).object_id == self.notebook.object_id
assert result.get( u"notebook" ).read_write == True
assert result.get( u"notebook" ).read_write == Notebook.READ_WRITE
assert result.get( u"notebook" ).owner == True
assert len( result.get( u"startup_notes" ) ) == 1
assert result[ "total_notes_count" ] == 2
@ -319,13 +319,13 @@ class Test_notebooks( Test_controller ):
assert result.get( u"user" ).object_id == self.user.object_id
assert len( result.get( u"notebooks" ) ) == 1
assert result.get( u"notebooks" )[ 0 ].object_id == self.anon_notebook.object_id
assert result.get( u"notebooks" )[ 0 ].read_write == False
assert result.get( u"notebooks" )[ 0 ].read_write == Notebook.READ_ONLY
assert result.get( u"notebooks" )[ 0 ].owner == False
assert result.get( u"login_url" ) is None
assert result.get( u"logout_url" )
assert result.get( u"rate_plan" )
assert result.get( u"notebook" ).object_id == self.anon_notebook.object_id
assert result.get( u"notebook" ).read_write == False
assert result.get( u"notebook" ).read_write == Notebook.READ_ONLY
assert result.get( u"notebook" ).owner == False
assert len( result.get( u"startup_notes" ) ) == 0
assert result[ "total_notes_count" ] == 0
@ -351,13 +351,13 @@ class Test_notebooks( Test_controller ):
assert result.get( u"user" ).object_id == self.user.object_id
assert len( result.get( u"notebooks" ) ) == 1
assert result.get( u"notebooks" )[ 0 ].object_id == self.anon_notebook.object_id
assert result.get( u"notebooks" )[ 0 ].read_write == False
assert result.get( u"notebooks" )[ 0 ].read_write == Notebook.READ_ONLY
assert result.get( u"notebooks" )[ 0 ].owner == False
assert result.get( u"login_url" ) is None
assert result.get( u"logout_url" )
assert result.get( u"rate_plan" )
assert result.get( u"notebook" ).object_id == self.anon_notebook.object_id
assert result.get( u"notebook" ).read_write == False
assert result.get( u"notebook" ).read_write == Notebook.READ_ONLY
assert result.get( u"notebook" ).owner == False
assert len( result.get( u"startup_notes" ) ) == 0
assert result[ "total_notes_count" ] == 0
@ -380,14 +380,18 @@ class Test_notebooks( Test_controller ):
assert result.get( u"user" ).object_id == self.user.object_id
assert len( result.get( u"notebooks" ) ) == 3
assert result.get( u"notebooks" )[ 1 ].object_id == self.anon_notebook.object_id
assert result.get( u"notebooks" )[ 1 ].read_write == False
assert result.get( u"notebooks" )[ 1 ].owner == False
notebook = result[ u"notebooks" ][ 0 ]
if notebook.name == u"trash":
notebook = result[ u"notebooks" ][ 1 ]
assert notebook.object_id == self.anon_notebook.object_id
assert notebook.read_write == Notebook.READ_ONLY
assert notebook.owner == False
assert result.get( u"login_url" ) is None
assert result.get( u"logout_url" )
assert result.get( u"rate_plan" )
assert result.get( u"notebook" ).object_id == self.anon_notebook.object_id
assert result.get( u"notebook" ).read_write == False
assert result.get( u"notebook" ).read_write == Notebook.READ_ONLY
assert result.get( u"notebook" ).owner == False
assert len( result.get( u"startup_notes" ) ) == 0
assert result[ "total_notes_count" ] == 0
@ -468,13 +472,13 @@ class Test_notebooks( Test_controller ):
assert result.get( u"user" ).object_id == self.user.object_id
assert len( result.get( u"notebooks" ) ) == 3
assert result.get( u"notebooks" )[ 2 ].object_id == self.notebook.object_id
assert result.get( u"notebooks" )[ 2 ].read_write == True
assert result.get( u"notebooks" )[ 2 ].read_write == Notebook.READ_WRITE
assert result.get( u"notebooks" )[ 2 ].owner == True
assert result.get( u"login_url" ) is None
assert result.get( u"logout_url" )
assert result.get( u"rate_plan" )
assert result.get( u"notebook" ).object_id == self.notebook.object_id
assert result.get( u"notebook" ).read_write == True
assert result.get( u"notebook" ).read_write == Notebook.READ_WRITE
assert result.get( u"notebook" ).owner == True
assert len( result.get( u"startup_notes" ) ) == 1
assert result[ "total_notes_count" ] == 2
@ -511,13 +515,13 @@ class Test_notebooks( Test_controller ):
assert result.get( u"user" ).object_id == self.user.object_id
assert len( result.get( u"notebooks" ) ) == 3
assert result.get( u"notebooks" )[ 2 ].object_id == self.notebook.object_id
assert result.get( u"notebooks" )[ 2 ].read_write == True
assert result.get( u"notebooks" )[ 2 ].read_write == Notebook.READ_WRITE
assert result.get( u"notebooks" )[ 2 ].owner == True
assert result.get( u"login_url" ) is None
assert result.get( u"logout_url" )
assert result.get( u"rate_plan" )
assert result.get( u"notebook" ).object_id == self.notebook.object_id
assert result.get( u"notebook" ).read_write == True
assert result.get( u"notebook" ).read_write == Notebook.READ_WRITE
assert result.get( u"notebook" ).owner == True
assert len( result.get( u"startup_notes" ) ) == 1
assert result[ "total_notes_count" ] == 2
@ -560,13 +564,13 @@ class Test_notebooks( Test_controller ):
assert result.get( u"user" ).object_id == self.user.object_id
assert len( result.get( u"notebooks" ) ) == 3
assert result.get( u"notebooks" )[ 2 ].object_id == self.notebook.object_id
assert result.get( u"notebooks" )[ 2 ].read_write == True
assert result.get( u"notebooks" )[ 2 ].read_write == Notebook.READ_WRITE
assert result.get( u"notebooks" )[ 2 ].owner == True
assert result.get( u"login_url" ) is None
assert result.get( u"logout_url" )
assert result.get( u"rate_plan" )
assert result.get( u"notebook" ).object_id == self.notebook.object_id
assert result.get( u"notebook" ).read_write == True
assert result.get( u"notebook" ).read_write == Notebook.READ_WRITE
assert result.get( u"notebook" ).owner == True
assert len( result.get( u"startup_notes" ) ) == 1
assert result[ "total_notes_count" ] == 2
@ -602,13 +606,13 @@ class Test_notebooks( Test_controller ):
assert result.get( u"user" ).object_id == self.user.object_id
assert len( result.get( u"notebooks" ) ) == 3
assert result.get( u"notebooks" )[ 2 ].object_id == self.notebook.object_id
assert result.get( u"notebooks" )[ 2 ].read_write == True
assert result.get( u"notebooks" )[ 2 ].read_write == Notebook.READ_WRITE
assert result.get( u"notebooks" )[ 2 ].owner == True
assert result.get( u"login_url" ) is None
assert result.get( u"logout_url" )
assert result.get( u"rate_plan" )
assert result.get( u"notebook" ).object_id == self.notebook.object_id
assert result.get( u"notebook" ).read_write == True
assert result.get( u"notebook" ).read_write == Notebook.READ_WRITE
assert result.get( u"notebook" ).owner == True
assert len( result.get( u"startup_notes" ) ) == 1
assert result[ "total_notes_count" ] == 2
@ -644,7 +648,7 @@ class Test_notebooks( Test_controller ):
assert invite.object_id == self.invite.object_id
assert notebook.object_id == self.notebook.object_id
assert notebook.read_write == True
assert notebook.read_write == Notebook.READ_WRITE
assert notebook.owner == True
assert len( startup_notes ) == 1
assert startup_notes[ 0 ].object_id == self.note.object_id
@ -669,7 +673,7 @@ class Test_notebooks( Test_controller ):
assert invite.object_id == self.invite.object_id
assert notebook.object_id == self.notebook.object_id
assert notebook.read_write == False
assert notebook.read_write == Notebook.READ_ONLY
assert notebook.owner == True
assert len( startup_notes ) == 1
assert startup_notes[ 0 ].object_id == self.note.object_id
@ -694,7 +698,7 @@ class Test_notebooks( Test_controller ):
assert invite.object_id == self.invite.object_id
assert notebook.object_id == self.notebook.object_id
assert notebook.read_write == True
assert notebook.read_write == Notebook.READ_WRITE
assert notebook.owner == False
assert len( startup_notes ) == 1
assert startup_notes[ 0 ].object_id == self.note.object_id
@ -718,7 +722,7 @@ class Test_notebooks( Test_controller ):
assert invite.object_id == self.invite.object_id
assert notebook.object_id == self.notebook.object_id
assert notebook.read_write == True
assert notebook.read_write == Notebook.READ_WRITE
assert notebook.owner == True
assert len( startup_notes ) == 1
assert startup_notes[ 0 ].object_id == self.note.object_id
@ -751,7 +755,7 @@ class Test_notebooks( Test_controller ):
assert invite.object_id == self.invite.object_id
assert notebook.object_id == self.notebook.object_id
assert notebook.read_write == True
assert notebook.read_write == Notebook.READ_WRITE
assert notebook.owner == True
assert len( startup_notes ) == 1
assert startup_notes[ 0 ].object_id == self.note.object_id
@ -790,7 +794,7 @@ class Test_notebooks( Test_controller ):
assert invite.object_id == self.invite.object_id
assert notebook.object_id == self.notebook.object_id
assert notebook.read_write == True
assert notebook.read_write == Notebook.READ_WRITE
assert notebook.owner == True