From 613ee8a2170e03f6b62bca27914f2540f4512196 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Tue, 16 Oct 2007 21:37:12 +0000 Subject: [PATCH] Completely revamped the way the main page and the notes on it are loaded by the client. Previously, the main page would load as mostly blank, then the client would immediately issue two async json calls to load the user and notebook data, including startup notes. Now, the main page loads with the note data actually as part of the page. If JavaScript is off, then you see all the notes displayed, including startup notes and any designated note. If JavaScript is on, then those "static" notes are instantly hidden and their contents are loaded into iframes for editing/display. The real upshot is that Luminotes in read-only mode is now more useful when JavaScript is off, and actually displays notes and their contents. This is very useful for search engine indexing. Updated all Python unit tests. Still have to get to JavaScript unit tests, what few their are. --- controller/Notebooks.py | 51 +++--- controller/Root.py | 16 +- controller/Users.py | 63 ++++--- controller/test/Stub_database.py | 2 + controller/test/Test_controller.py | 2 +- controller/test/Test_notebooks.py | 190 ++++++++++++-------- controller/test/Test_users.py | 163 ++++++++--------- static/css/style.css | 33 +++- static/html/no javascript.html | 4 - static/js/Wiki.js | 274 +++++++++-------------------- view/Error_page.py | 18 +- view/Json.py | 12 +- view/Link_area.py | 60 ++++++- view/Main_page.py | 100 ++++++++--- view/Toolbar.py | 4 +- view/User_area.py | 24 +++ 16 files changed, 558 insertions(+), 458 deletions(-) delete mode 100644 static/html/no javascript.html create mode 100644 view/User_area.py diff --git a/controller/Notebooks.py b/controller/Notebooks.py index 18f919b..435318f 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -46,13 +46,15 @@ class Notebooks( object ): self.__users = users @expose( view = Main_page ) + @grab_user_id @validate( notebook_id = Valid_id(), note_id = Valid_id(), parent_id = Valid_id(), revision = Valid_revision(), + user_id = Valid_id( none_okay = True ), ) - def default( self, notebook_id, note_id = None, parent_id = None, revision = None ): + def default( self, notebook_id, note_id = None, parent_id = None, revision = None, user_id = None ): """ Provide the information necessary to display the page for a particular notebook. If a particular note id is given without a revision, then the most recent version of that note is @@ -69,26 +71,18 @@ class Notebooks( object ): @rtype: unicode @return: rendered HTML page """ - return dict( - notebook_id = notebook_id, - note_id = note_id, - parent_id = parent_id, - revision = revision, - ) + result = self.__users.current( user_id ) + result.update( self.contents( notebook_id, note_id, revision, user_id ) ) + result[ "parent_id" ] = parent_id + if revision: + result[ "note_read_write" ] = False + + return result - @expose( view = Json ) - @strongly_expire - @grab_user_id - @validate( - notebook_id = Valid_id(), - note_id = Valid_id( none_okay = True ), - 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 ): """ - Return the information on particular notebook, including the contents of its startup notes. - Optionally include the contents of a single requested note as well. + Return the startup notes for the given notebook. Optionally include a single requested note as + well. @type notebook_id: unicode @param notebook_id: id of notebook to return @@ -97,9 +91,13 @@ class Notebooks( object ): @type revision: unicode or NoneType @param revision: revision timestamp of the provided note (optional) @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: { 'notebook': notebookdict, 'note': notedict or None } + @param user_id: id of current logged-in user (if any) + @rtype: dict + @return: { + 'notebook': notebook, + 'startup_notes': notelist, + 'note': note 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 """ @@ -108,17 +106,18 @@ class Notebooks( object ): notebook = self.__database.load( Notebook, notebook_id ) + if notebook is None: + raise Access_error() + 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.create( note_id ) - else: + if note_id: note = self.__database.load( Note, note_id, revision ) if note and note.notebook_id != notebook_id: raise Access_error() + else: + note = None startup_notes = self.__database.select_many( Note, notebook.sql_load_startup_notes() ) diff --git a/controller/Root.py b/controller/Root.py index 1338b3e..cfee3e8 100644 --- a/controller/Root.py +++ b/controller/Root.py @@ -1,9 +1,10 @@ import cherrypy from Expose import expose +from Expire import strongly_expire from Validate import validate from Notebooks import Notebooks -from Users import Users +from Users import Users, grab_user_id from Database import Valid_id from model.Note import Note from view.Main_page import Main_page @@ -52,7 +53,12 @@ class Root( object ): ) @expose( view = Main_page ) - def index( self ): + @strongly_expire + @grab_user_id + @validate( + user_id = Valid_id( none_okay = True ), + ) + def index( self, user_id ): """ Provide the information necessary to display the web site's front page, potentially performing a redirect to the https version of the page. @@ -64,7 +70,11 @@ class Root( object ): if cherrypy.session.get( "user_id" ) and https_url and cherrypy.request.remote_addr != https_proxy_ip: return dict( redirect = https_url ) - return dict() + result = self.__users.current( user_id ) + first_notebook_id = result[ u"notebooks" ][ 0 ].object_id + result.update( self.__notebooks.contents( first_notebook_id, user_id = user_id ) ) + + return result # TODO: move this method to controller.Notebooks, and maybe give it a more sensible name @expose( view = Json ) diff --git a/controller/Users.py b/controller/Users.py index fabc3ae..e4aedaa 100644 --- a/controller/Users.py +++ b/controller/Users.py @@ -68,6 +68,17 @@ class Password_reset_error( Exception ): ) +class Access_error( Exception ): + def __init__( self, message ): + Exception.__init__( self, message ) + self.__message = message + + def to_dict( self ): + return dict( + error = self.__message + ) + + def grab_user_id( function ): """ A decorator to grab the current logged in user id from the cherrypy session and pass it as a @@ -329,29 +340,19 @@ class Users( object ): deauthenticated = True, ) - @expose( view = Json ) - @strongly_expire - @grab_user_id - @validate( - include_startup_notes = Valid_bool(), - user_id = Valid_id( none_okay = True ), - ) - def current( self, include_startup_notes, user_id ): + def current( self, user_id ): """ Return information on the currently logged-in user. If not logged in, default to the anonymous user. - @type include_startup_notes: bool - @param include_startup_notes: True to return startup notes for the first notebook @type user_id: unicode - @param user_id: id of current logged-in user (if any), determined by @grab_user_id + @param user_id: id of current logged-in user (if any) @rtype: json dict @return: { - 'user': userdict or None, - 'notebooks': notebooksdict, - 'startup_notes': noteslist, - 'http_url': url, + 'user': user or None, + 'notebooks': notebookslist, 'login_url': url, + 'logout_url': url, 'rate_plan': rateplandict, } @raise Validation_error: one of the arguments is invalid @@ -364,37 +365,27 @@ class Users( object ): user = anonymous if not user or not anonymous: - return dict( - user = None, - notebooks = None, - http_url = u"", - ) + raise Access_error( u"Sorry, you don't have access to do that." ) # in addition to this user's own notebooks, add to that list the anonymous user's notebooks login_url = None notebooks = self.__database.select_many( Notebook, anonymous.sql_load_notebooks() ) - if user_id: + if user_id and user_id != anonymous.object_id: notebooks += self.__database.select_many( Notebook, user.sql_load_notebooks() ) # if the user is not logged in, return a login URL else: - if len( notebooks ) > 0: + if len( notebooks ) > 0 and 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, main_notebook.object_id, login_note.object_id ) - if include_startup_notes and len( notebooks ) > 0: - startup_notes = self.__database.select_many( Note, notebooks[ 0 ].sql_load_startup_notes() ) - else: - startup_notes = [] - return dict( user = user, notebooks = notebooks, - startup_notes = startup_notes, - http_url = self.__http_url, login_url = login_url, + logout_url = self.__https_url + u"/", rate_plan = ( user.rate_plan < len( self.__rate_plans ) ) and self.__rate_plans[ user.rate_plan ] or {}, ) @@ -452,7 +443,7 @@ class Users( object ): # 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 ) ): + if user and self.__database.select_one( bool, user.sql_has_access( notebook_id, read_write ) ): return True return False @@ -551,12 +542,18 @@ class Users( object ): if len( matching_users ) == 0: raise Password_reset_error( u"There are no Luminotes users with the email address %s" % password_reset.email_address ) - return dict( + result = self.current( anonymous.object_id ) + result[ "notebook" ] = main_notebook + result[ "startup_notes" ] = self.__database.select_many( Note, main_notebook.sql_load_startup_notes() ) + result[ "note_read_write" ] = False + result[ "note" ] = Note.create( + object_id = u"password_reset", + contents = unicode( Redeem_reset_note( password_reset_id, matching_users ) ), notebook_id = main_notebook.object_id, - note_id = u"blank", - note_contents = unicode( Redeem_reset_note( password_reset_id, matching_users ) ), ) + return result + @expose( view = Json ) def reset_password( self, password_reset_id, reset_button, **new_passwords ): """ diff --git a/controller/test/Stub_database.py b/controller/test/Stub_database.py index 52b0d30..60ef9b5 100644 --- a/controller/test/Stub_database.py +++ b/controller/test/Stub_database.py @@ -6,9 +6,11 @@ class Stub_database( object ): # 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.last_saved_obj = None self.__next_id = 0 def save( self, obj, commit = False ): + self.last_saved_obj = obj if obj.object_id in self.objects: self.objects[ obj.object_id ].append( copy( obj ) ) else: diff --git a/controller/test/Test_controller.py b/controller/test/Test_controller.py index 74f8482..d42278c 100644 --- a/controller/test/Test_controller.py +++ b/controller/test/Test_controller.py @@ -29,7 +29,7 @@ class Test_controller( object ): notebooks = [] notebook_tuples = database.user_notebook.get( self.object_id ) - if not notebook_tuples: return None + if not notebook_tuples: return [] for notebook_tuple in notebook_tuples: ( notebook_id, read_write ) = notebook_tuple diff --git a/controller/test/Test_notebooks.py b/controller/test/Test_notebooks.py index c3add00..b99f0d4 100644 --- a/controller/test/Test_notebooks.py +++ b/controller/test/Test_notebooks.py @@ -1,10 +1,12 @@ import cherrypy import cgi +from nose.tools import raises from urllib import quote from Test_controller import Test_controller from model.Notebook import Notebook from model.Note import Note from model.User import User +from controller.Notebooks import Access_error class Test_notebooks( Test_controller ): @@ -53,53 +55,118 @@ class Test_notebooks( Test_controller ): 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 ) + def test_default_without_login( self ): + result = self.http_get( + "/notebooks/%s" % self.notebook.object_id, + ) - assert result.get( u"notebook_id" ) == self.notebook.object_id + assert u"access" in result[ u"error" ] + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 + + def test_default( self ): + self.login() + + result = self.http_get( + "/notebooks/%s" % self.notebook.object_id, + session_id = self.session_id, + ) + + assert result.get( u"user" ).object_id == self.user.object_id + assert len( result.get( u"notebooks" ) ) == 3 + 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 len( result.get( u"startup_notes" ) ) == 1 + assert result.get( u"note" ) is None + assert result.get( u"parent_id" ) == None + assert result.get( u"note_read_write" ) in ( None, True ) + 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 ) ) + self.login() + + result = self.http_get( + "/notebooks/%s?note_id=%s" % ( self.notebook.object_id, self.note.object_id ), + session_id = self.session_id, + ) - assert result.get( u"notebook_id" ) == self.notebook.object_id - assert result.get( u"note_id" ) == self.note.object_id + assert result.get( u"user" ).object_id == self.user.object_id + assert len( result.get( u"notebooks" ) ) == 3 + 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 len( result.get( u"startup_notes" ) ) == 1 + assert result.get( u"note" ).object_id == self.note.object_id + assert result.get( u"parent_id" ) == None + assert result.get( u"note_read_write" ) in ( None, True ) + 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" % ( - self.notebook.object_id, - self.note.object_id, - quote( unicode( self.note.revision ) ), - ) ) + self.login() + + result = self.http_get( + "/notebooks/%s?note_id=%s&revision=%s" % ( + self.notebook.object_id, + self.note.object_id, + quote( unicode( self.note.revision ) ), + ), + session_id = self.session_id, + ) - 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 result.get( u"user" ).object_id == self.user.object_id + assert len( result.get( u"notebooks" ) ) == 3 + 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 len( result.get( u"startup_notes" ) ) == 1 + assert result.get( u"note" ).object_id == self.note.object_id + assert result.get( u"note" ).revision == self.note.revision + assert result.get( u"parent_id" ) == None + assert result.get( u"note_read_write" ) == False + user = self.database.load( User, self.user.object_id ) assert user.storage_bytes == 0 def test_default_with_parent( self ): - parent_id = "foo" - result = self.http_get( "/notebooks/%s?parent_id=%s" % ( self.notebook.object_id, parent_id ) ) + self.login() + + parent_id = u"foo" + result = self.http_get( + "/notebooks/%s?parent_id=%s" % ( self.notebook.object_id, parent_id ), + session_id = self.session_id, + ) - assert result.get( u"notebook_id" ) == self.notebook.object_id + assert result.get( u"user" ).object_id == self.user.object_id + assert len( result.get( u"notebooks" ) ) == 3 + 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 len( result.get( u"startup_notes" ) ) == 1 + assert result.get( u"note" ) is None assert result.get( u"parent_id" ) == parent_id + assert result.get( u"note_read_write" ) in ( None, True ) + user = self.database.load( User, self.user.object_id ) assert user.storage_bytes == 0 def test_contents( self ): - self.login() - - result = self.http_get( - "/notebooks/contents?notebook_id=%s" % self.notebook.object_id, - session_id = self.session_id, + result = cherrypy.root.notebooks.contents( + notebook_id = self.notebook.object_id, + user_id = self.user.object_id, ) notebook = result[ "notebook" ] startup_notes = result[ "startup_notes" ] + assert result[ "note" ] == None assert notebook.object_id == self.notebook.object_id assert notebook.read_write == True @@ -109,11 +176,10 @@ class Test_notebooks( Test_controller ): assert user.storage_bytes == 0 def test_contents_with_note( self ): - self.login() - - result = self.http_get( - "/notebooks/contents?notebook_id=%s¬e_id=%s" % ( self.notebook.object_id, self.note.object_id ), - session_id = self.session_id, + result = cherrypy.root.notebooks.contents( + notebook_id = self.notebook.object_id, + note_id = self.note.object_id, + user_id = self.user.object_id, ) notebook = result[ "notebook" ] @@ -131,16 +197,13 @@ class Test_notebooks( Test_controller ): assert user.storage_bytes == 0 def test_contents_with_note_and_revision( self ): - self.login() - - result = self.http_get( - "/notebooks/contents?notebook_id=%s¬e_id=%s&revision=%s" % ( - self.notebook.object_id, - self.note.object_id, - quote( unicode( self.note.revision ) ), - ), - session_id = self.session_id, + result = cherrypy.root.notebooks.contents( + notebook_id = self.notebook.object_id, + note_id = self.note.object_id, + revision = unicode( self.note.revision ), + user_id = self.user.object_id, ) + self.login() notebook = result[ "notebook" ] startup_notes = result[ "startup_notes" ] @@ -153,54 +216,39 @@ class Test_notebooks( Test_controller ): note = result[ "note" ] assert note.object_id == self.note.object_id + assert note.revision == self.note.revision user = self.database.load( User, self.user.object_id ) assert user.storage_bytes == 0 - def test_contents_with_blank_note( self ): - self.login() - - result = self.http_get( - "/notebooks/contents?notebook_id=%s¬e_id=blank" % self.notebook.object_id , - session_id = self.session_id, + @raises( Access_error ) + def test_contents_without_user_id( self ): + result = cherrypy.root.notebooks.contents( + notebook_id = self.notebook.object_id, ) - notebook = result[ "notebook" ] - 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 ].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_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( - "/notebooks/contents?notebook_id=%s" % self.notebook.object_id, - session_id = self.session_id, + @raises( Access_error ) + def test_contents_with_incorrect_user_id( self ): + result = cherrypy.root.notebooks.contents( + notebook_id = self.notebook.object_id, + user_id = self.anonymous.object_id, ) - assert result.get( "error" ) - user = self.database.load( User, self.user.object_id ) - assert user.storage_bytes == 0 + @raises( Access_error ) + def test_contents_with_unknown_notebook_id( self ): + result = cherrypy.root.notebooks.contents( + notebook_id = self.unknown_notebook_id, + user_id = self.user.object_id, + ) 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, + result = cherrypy.root.notebooks.contents( + notebook_id = self.anon_notebook.object_id, + user_id = self.user.object_id, ) notebook = result[ "notebook" ] startup_notes = result[ "startup_notes" ] + assert result[ "note" ] == None assert notebook.object_id == self.anon_notebook.object_id assert notebook.read_write == False diff --git a/controller/test/Test_users.py b/controller/test/Test_users.py index eca491d..590aa28 100644 --- a/controller/test/Test_users.py +++ b/controller/test/Test_users.py @@ -2,6 +2,7 @@ import re import cherrypy import smtplib from pytz import utc +from nose.tools import raises from datetime import datetime, timedelta from nose.tools import raises from Test_controller import Test_controller @@ -10,6 +11,7 @@ from model.User import User from model.Notebook import Notebook from model.Note import Note from model.Password_reset import Password_reset +from controller.Users import Access_error class Test_users( Test_controller ): @@ -80,7 +82,7 @@ class Test_users( Test_controller ): assert result[ u"redirect" ].startswith( u"/notebooks/" ) - def test_current_after_signup( self, include_startup_notes = False ): + def test_current_after_signup( self ): result = self.http_post( "/users/signup", dict( username = self.new_username, password = self.new_password, @@ -92,12 +94,14 @@ class Test_users( Test_controller ): new_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ] - result = self.http_get( - "/users/current?include_startup_notes=%s" % include_startup_notes, - session_id = session_id, - ) + user = self.database.last_saved_obj + assert isinstance( user, User ) + result = cherrypy.root.users.current( user.object_id ) + assert result[ u"user" ].object_id == user.object_id assert result[ u"user" ].username == self.new_username + assert result[ u"user" ].email_address == self.new_email_address + notebooks = result[ u"notebooks" ] notebook = notebooks[ 0 ] assert notebook.object_id == self.anon_notebook.object_id @@ -120,22 +124,13 @@ class Test_users( Test_controller ): 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 ].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 == [] + assert result.get( u"login_url" ) is None + assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/" rate_plan = result[ u"rate_plan" ] assert rate_plan[ u"name" ] == u"super" assert rate_plan[ u"storage_quota_bytes" ] == 1337 - def test_current_with_startup_notes_after_signup( self ): - self.test_current_after_signup( include_startup_notes = True ) - def test_signup_with_different_passwords( self ): result = self.http_post( "/users/signup", dict( username = self.new_username, @@ -152,18 +147,20 @@ class Test_users( Test_controller ): assert result[ u"redirect" ].startswith( u"/notebooks/" ) - def test_current_after_demo( self, include_startup_notes = False ): + def test_current_after_demo( self ): result = self.http_post( "/users/demo", dict() ) session_id = result[ u"session_id" ] new_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ] - result = self.http_get( - "/users/current?include_startup_notes=%s" % include_startup_notes, - session_id = session_id, - ) + user = self.database.last_saved_obj + assert isinstance( user, User ) + result = cherrypy.root.users.current( user.object_id ) + + assert result[ u"user" ].object_id == user.object_id + assert result[ u"user" ].username is None + assert result[ u"user" ].email_address is None - assert result[ u"user" ].username == None notebooks = result[ u"notebooks" ] assert len( notebooks ) == 3 notebook = notebooks[ 0 ] @@ -187,34 +184,25 @@ class Test_users( Test_controller ): 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 ].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 == [] + assert result.get( u"login_url" ) is None + assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/" rate_plan = result[ u"rate_plan" ] assert rate_plan[ u"name" ] == u"super" assert rate_plan[ u"storage_quota_bytes" ] == 1337 - def test_current_with_startup_notes_after_demo( self ): - self.test_current_after_demo( include_startup_notes = True ) - - def test_current_after_demo_twice( self, include_startup_notes = False ): + def test_current_after_demo_twice( self ): result = self.http_post( "/users/demo", dict() ) session_id = result[ u"session_id" ] new_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ] - result = self.http_get( - "/users/current?include_startup_notes=%s" % include_startup_notes, - session_id = session_id, - ) + user = self.database.last_saved_obj + assert isinstance( user, User ) + result = cherrypy.root.users.current( user.object_id ) user_id = result[ u"user" ].object_id + assert user_id == user.object_id # request a demo for a second time result = self.http_post( "/users/demo", dict(), session_id = session_id ) @@ -224,10 +212,7 @@ class Test_users( Test_controller ): assert notebook_id_again == new_notebook_id - result = self.http_get( - "/users/current?include_startup_notes=%s" % include_startup_notes, - session_id = session_id, - ) + result = cherrypy.root.users.current( user_id ) user_id_again = result[ u"user" ].object_id @@ -235,9 +220,6 @@ class Test_users( Test_controller ): # should just use the same guest user with the same notebook assert user_id_again == user_id - def test_current_with_startup_notes_after_demo_twice( self ): - self.test_current_after_demo_twice( include_startup_notes = True ) - def test_login( self ): result = self.http_post( "/users/login", dict( username = self.username, @@ -270,18 +252,8 @@ class Test_users( Test_controller ): assert result[ u"redirect" ] == self.settings[ u"global" ].get( u"luminotes.http_url" ) + u"/" - def test_current_after_login( self, include_startup_notes = False ): - result = self.http_post( "/users/login", dict( - username = self.username, - password = self.password, - login_button = u"login", - ) ) - session_id = result[ u"session_id" ] - - result = self.http_get( - "/users/current?include_startup_notes=%s" % include_startup_notes, - session_id = session_id, - ) + def test_current( self ): + result = cherrypy.root.users.current( self.user.object_id ) assert result[ u"user" ] assert result[ u"user" ].object_id == self.user.object_id @@ -293,32 +265,22 @@ class Test_users( Test_controller ): 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 + assert result[ u"login_url" ] is None + assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/" - startup_notes = result[ "startup_notes" ] - if include_startup_notes: - assert len( startup_notes ) == 1 - 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 == [] + rate_plan = result[ u"rate_plan" ] + assert rate_plan + assert rate_plan[ u"name" ] == u"super" + assert rate_plan[ u"storage_quota_bytes" ] == 1337 - def test_current_with_startup_notes_after_login( self ): - self.test_current_after_login( include_startup_notes = True ) - - def test_current_without_login( self, include_startup_notes = False ): - result = self.http_get( - "/users/current?include_startup_notes=%s" % include_startup_notes, - ) + def test_current_anonymous( self ): + result = cherrypy.root.users.current( self.anonymous.object_id ) assert result[ u"user" ].username == "anonymous" 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.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" % ( @@ -326,18 +288,12 @@ class Test_users( Test_controller ): self.anon_notebook.object_id, login_note.object_id, ) + assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/" - startup_notes = result[ "startup_notes" ] - if include_startup_notes: - assert len( startup_notes ) == 1 - 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 ) + rate_plan = result[ u"rate_plan" ] + assert rate_plan + assert rate_plan[ u"name" ] == u"super" + assert rate_plan[ u"storage_quota_bytes" ] == 1337 def test_update_storage( self ): previous_revision = self.user.revision @@ -409,11 +365,36 @@ class Test_users( Test_controller ): result = self.http_get( "/users/redeem_reset/%s" % password_reset_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" ] - assert self.user2.username in result[ u"note_contents" ] + assert result[ u"user" ].username == "anonymous" + 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 + + 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, + login_note.object_id, + ) + assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/" + + rate_plan = result[ u"rate_plan" ] + assert rate_plan + assert rate_plan[ u"name" ] == u"super" + assert rate_plan[ u"storage_quota_bytes" ] == 1337 + + assert result[ u"notebook" ].object_id == self.anon_notebook.object_id + assert len( result[ u"startup_notes" ] ) == 1 + assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id + assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title + assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents + assert result[ u"note_read_write" ] is False + assert result[ u"note" ].title == u"complete your password reset" + assert result[ u"note" ].notebook_id == self.anon_notebook.object_id + assert u"password reset" in result[ u"note" ].contents + assert self.user.username in result[ u"note" ].contents + assert self.user2.username in result[ u"note" ].contents def test_redeem_reset_unknown( self ): password_reset_id = u"unknownresetid" diff --git a/static/css/style.css b/static/css/style.css index a00f705..9d56483 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -13,14 +13,6 @@ a:hover { color: #ff6600; } -noscript { - text-align: left; -} - -noscript h3 { - margin-bottom: 0.5em; -} - ul li { margin-top: 0.5em; } @@ -206,6 +198,16 @@ ol li { -webkit-border-radius: 0.5em 0 0 0; } +#static_notes { + text-align: left; + margin: 1em; + line-height: 140%; +} + +#static_notes h3 { + margin-bottom: 0.5em; +} + #notes { text-align: left; margin-top: 1em; @@ -405,3 +407,18 @@ ol li { -moz-border-radius: 0.5em; -webkit-border-radius: 0.5em; } + +.button { + border-style: outset; + border-width: 0px; + background-color: #d0e0f0; + font-size: 100%; + outline: none; + -moz-border-radius: 0.5em; + -webkit-border-radius: 0.5em; +} + +.button:hover { + background-color: #ffcc66; +} + diff --git a/static/html/no javascript.html b/static/html/no javascript.html deleted file mode 100644 index bb92bc4..0000000 --- a/static/html/no javascript.html +++ /dev/null @@ -1,4 +0,0 @@ -

Luminotes requires JavaScript

- -So if you'd like to check out this site any further, please enable JavaScript -in your web browser, and then reload this page. Sorry for the inconvenience. diff --git a/static/js/Wiki.js b/static/js/Wiki.js index 3b14692..4244380 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -1,3 +1,4 @@ + function Wiki( invoker ) { this.next_id = null; this.focused_editor = null; @@ -5,104 +6,52 @@ function Wiki( invoker ) { this.notebook = null; this.notebook_id = getElement( "notebook_id" ).value; this.parent_id = getElement( "parent_id" ).value; // id of the notebook containing this one - this.read_write = false; this.startup_notes = new Array(); // map of startup notes: note id to bool this.open_editors = new Array(); // map of open notes: note title to editor this.all_notes_editor = null; // editor for display of list of all notes this.search_results_editor = null; // editor for display of search results this.invoker = invoker; - this.rate_plan = null; + this.rate_plan = evalJSON( getElement( "rate_plan" ).value ); this.storage_usage_high = false; + // grab the current notebook from the list of available notebooks + var notebooks = evalJSON( getElement( "notebooks" ).value ); + for ( var i in notebooks ) { + if ( notebooks[ i ].object_id == this.notebook_id ) { + this.notebook = notebooks[ i ] + break; + } + } + + // populate the wiki with startup notes + this.populate( + evalJSON( getElement( "startup_notes" ).value || "null" ), + evalJSON( getElement( "note" ).value || "null" ), + evalJSON( getElement( "note_read_write" ).value || "true" ) + ); + + this.display_storage_usage( evalJSON( getElement( "storage_bytes" ).value || "0" ) ); + connect( this.invoker, "error_message", this, "display_error" ); connect( this.invoker, "message", this, "display_message" ); connect( "search_form", "onsubmit", this, "search" ); connect( "html", "onclick", this, "background_clicked" ); - // get info on the requested notebook (if any) var self = this; - if ( this.notebook_id ) { - this.invoker.invoke( - "/notebooks/contents", "GET", { - "notebook_id": this.notebook_id, - "note_id": getElement( "note_id" ).value, - "revision": getElement( "revision" ).value - }, - function( result ) { self.populate( result ); } - ); - var include_startup_notes = false; - } else { - var include_startup_notes = true; + var logout_link = getElement( "logout_link" ); + if ( logout_link ) { + connect( "logout_link", "onclick", function ( event ) { + self.save_editor( null, true ); + self.invoker.invoke( "/users/logout", "POST" ); + event.stop(); + } ); } - - // get info on the current user (logged-in or anonymous) - this.invoker.invoke( "/users/current", "GET", { - "include_startup_notes": include_startup_notes - }, - function( result ) { self.display_user( result ); } - ); } Wiki.prototype.update_next_id = function ( result ) { this.next_id = result.next_id; } -Wiki.prototype.display_user = function ( result ) { - // if no notebook id was requested, then just display the user's default notebook - if ( !this.notebook_id ) { - this.notebook_id = result.notebooks[ 0 ].object_id; - this.populate( { "notebook" : result.notebooks[ 0 ], "startup_notes": result.startup_notes } ); - } - - var user_span = createDOM( "span" ); - replaceChildNodes( "user_area", user_span ); - - // if not logged in, display a login link - if ( result.user.username == "anonymous" && result.login_url ) { - appendChildNodes( user_span, createDOM( "a", { "href": result.login_url, "id": "login_link" }, "login" ) ); - return; - } - - // display links for current notebook and a list of all notebooks that the user has access to - var notebooks_span = createDOM( "span" ); - replaceChildNodes( "notebooks_area", notebooks_span ); - - appendChildNodes( notebooks_span, createDOM( "h4", "notebooks" ) ); - - for ( var i in result.notebooks ) { - var notebook = result.notebooks[ i ]; - - if ( notebook.name == "Luminotes" || notebook.name == "trash" ) - continue; - - var div_class = "link_area_item"; - if ( notebook.object_id == this.notebook_id ) - div_class += " current_notebook_name"; - - appendChildNodes( notebooks_span, createDOM( "div", { - "class": div_class - }, createDOM( "a", { - "href": "/notebooks/" + notebook.object_id, - "id": "notebook_" + notebook.object_id - }, notebook.name ) ) ); - } - - this.rate_plan = result.rate_plan; - this.display_storage_usage( result.user.storage_bytes ); - - // display the name of the logged in user and a logout link - appendChildNodes( user_span, "logged in as " + ( result.user.username || "a guest" ) ); - appendChildNodes( user_span, " | " ); - appendChildNodes( user_span, createDOM( "a", { "href": result.http_url + "/", "id": "logout_link" }, "logout" ) ); - - var self = this; - connect( "logout_link", "onclick", function ( event ) { - self.save_editor( null, true ); - self.invoker.invoke( "/users/logout", "POST" ); - event.stop(); - } ); -} - Wiki.prototype.display_storage_usage = function( storage_bytes ) { if ( !storage_bytes ) return; @@ -113,7 +62,10 @@ Wiki.prototype.display_storage_usage = function( storage_bytes ) { return Math.round( storage_bytes / MEGABYTE ); } - var quota_bytes = this.rate_plan.storage_quota_bytes || 0; + var quota_bytes = this.rate_plan.storage_quota_bytes; + if ( !quota_bytes ) + return; + var usage_percent = Math.round( storage_bytes / quota_bytes * 100.0 ); if ( usage_percent > 90 ) { @@ -136,68 +88,49 @@ Wiki.prototype.display_storage_usage = function( storage_bytes ) { ); } -Wiki.prototype.populate = function ( result ) { - this.notebook = result.notebook; +Wiki.prototype.populate = function ( startup_notes, note, note_read_write ) { + // create an editor for each startup note in the received notebook, focusing the first one + var focus = true; + for ( var i in startup_notes ) { + var startup_note = startup_notes[ i ]; + this.startup_notes[ startup_note.object_id ] = true; + + // don't actually create an editor if a particular note was provided in the result + if ( !note ) { + var editor = this.create_editor( + startup_note.object_id, + // grab this note's contents from the static