Browse Source

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.
Dan Helfman 11 years ago
parent
commit
613ee8a217

+ 25
- 26
controller/Notebooks.py View File

@@ -46,13 +46,15 @@ class Notebooks( object ):
46 46
     self.__users = users
47 47
 
48 48
   @expose( view = Main_page )
49
+  @grab_user_id
49 50
   @validate(
50 51
     notebook_id = Valid_id(),
51 52
     note_id = Valid_id(),
52 53
     parent_id = Valid_id(),
53 54
     revision = Valid_revision(),
55
+    user_id = Valid_id( none_okay = True ),
54 56
   )
55
-  def default( self, notebook_id, note_id = None, parent_id = None, revision = None ):
57
+  def default( self, notebook_id, note_id = None, parent_id = None, revision = None, user_id = None ):
56 58
     """
57 59
     Provide the information necessary to display the page for a particular notebook. If a
58 60
     particular note id is given without a revision, then the most recent version of that note is
@@ -69,26 +71,18 @@ class Notebooks( object ):
69 71
     @rtype: unicode
70 72
     @return: rendered HTML page
71 73
     """
72
-    return dict(
73
-      notebook_id = notebook_id,
74
-      note_id = note_id,
75
-      parent_id = parent_id,
76
-      revision = revision,
77
-    )
74
+    result = self.__users.current( user_id )
75
+    result.update( self.contents( notebook_id, note_id, revision, user_id ) )
76
+    result[ "parent_id" ] = parent_id
77
+    if revision:
78
+      result[ "note_read_write" ] = False
79
+
80
+    return result
78 81
 
79
-  @expose( view = Json )
80
-  @strongly_expire
81
-  @grab_user_id
82
-  @validate(
83
-    notebook_id = Valid_id(),
84
-    note_id = Valid_id( none_okay = True ),
85
-    revision = Valid_revision( none_okay = True ),
86
-    user_id = Valid_id( none_okay = True ),
87
-  )
88 82
   def contents( self, notebook_id, note_id = None, revision = None, user_id = None ):
89 83
     """
90
-    Return the information on particular notebook, including the contents of its startup notes.
91
-    Optionally include the contents of a single requested note as well.
84
+    Return the startup notes for the given notebook. Optionally include a single requested note as
85
+    well.
92 86
 
93 87
     @type notebook_id: unicode
94 88
     @param notebook_id: id of notebook to return
@@ -97,9 +91,13 @@ class Notebooks( object ):
97 91
     @type revision: unicode or NoneType
98 92
     @param revision: revision timestamp of the provided note (optional)
99 93
     @type user_id: unicode or NoneType
100
-    @param user_id: id of current logged-in user (if any), determined by @grab_user_id
101
-    @rtype: json dict
102
-    @return: { 'notebook': notebookdict, 'note': notedict or None }
94
+    @param user_id: id of current logged-in user (if any)
95
+    @rtype: dict
96
+    @return: {
97
+      'notebook': notebook,
98
+      'startup_notes': notelist,
99
+      'note': note or None,
100
+    }
103 101
     @raise Access_error: the current user doesn't have access to the given notebook or note
104 102
     @raise Validation_error: one of the arguments is invalid
105 103
     """
@@ -108,17 +106,18 @@ class Notebooks( object ):
108 106
 
109 107
     notebook = self.__database.load( Notebook, notebook_id )
110 108
 
109
+    if notebook is None:
110
+      raise Access_error()
111
+
111 112
     if not self.__users.check_access( user_id, notebook_id, read_write = True ):
112 113
       notebook.read_write = False
113 114
 
114
-    if notebook is None:
115
-      note = None
116
-    elif note_id == u"blank":
117
-      note = Note.create( note_id )
118
-    else:
115
+    if note_id:
119 116
       note = self.__database.load( Note, note_id, revision )
120 117
       if note and note.notebook_id != notebook_id:
121 118
         raise Access_error()
119
+    else:
120
+      note = None
122 121
 
123 122
     startup_notes = self.__database.select_many( Note, notebook.sql_load_startup_notes() )
124 123
 

+ 13
- 3
controller/Root.py View File

@@ -1,9 +1,10 @@
1 1
 import cherrypy
2 2
 
3 3
 from Expose import expose
4
+from Expire import strongly_expire
4 5
 from Validate import validate
5 6
 from Notebooks import Notebooks
6
-from Users import Users
7
+from Users import Users, grab_user_id
7 8
 from Database import Valid_id
8 9
 from model.Note import Note
9 10
 from view.Main_page import Main_page
@@ -52,7 +53,12 @@ class Root( object ):
52 53
     )
53 54
 
54 55
   @expose( view = Main_page )
55
-  def index( self ):
56
+  @strongly_expire
57
+  @grab_user_id
58
+  @validate(
59
+    user_id = Valid_id( none_okay = True ),
60
+  )
61
+  def index( self, user_id ):
56 62
     """
57 63
     Provide the information necessary to display the web site's front page, potentially performing
58 64
     a redirect to the https version of the page.
@@ -64,7 +70,11 @@ class Root( object ):
64 70
     if cherrypy.session.get( "user_id" ) and https_url and cherrypy.request.remote_addr != https_proxy_ip:
65 71
       return dict( redirect = https_url )
66 72
 
67
-    return dict()
73
+    result = self.__users.current( user_id )
74
+    first_notebook_id = result[ u"notebooks" ][ 0 ].object_id
75
+    result.update( self.__notebooks.contents( first_notebook_id, user_id = user_id ) )
76
+
77
+    return result
68 78
 
69 79
   # TODO: move this method to controller.Notebooks, and maybe give it a more sensible name
70 80
   @expose( view = Json )

+ 30
- 33
controller/Users.py View File

@@ -68,6 +68,17 @@ class Password_reset_error( Exception ):
68 68
     )
69 69
 
70 70
 
71
+class Access_error( Exception ):
72
+  def __init__( self, message ):
73
+    Exception.__init__( self, message )
74
+    self.__message = message
75
+
76
+  def to_dict( self ):
77
+    return dict(
78
+      error = self.__message
79
+    )
80
+
81
+
71 82
 def grab_user_id( function ):
72 83
   """
73 84
   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 ):
329 340
       deauthenticated = True,
330 341
     )
331 342
 
332
-  @expose( view = Json )
333
-  @strongly_expire
334
-  @grab_user_id
335
-  @validate(
336
-    include_startup_notes = Valid_bool(),
337
-    user_id = Valid_id( none_okay = True ),
338
-  )
339
-  def current( self, include_startup_notes, user_id ):
343
+  def current( self, user_id ):
340 344
     """
341 345
     Return information on the currently logged-in user. If not logged in, default to the anonymous
342 346
     user.
343 347
 
344
-    @type include_startup_notes: bool
345
-    @param include_startup_notes: True to return startup notes for the first notebook
346 348
     @type user_id: unicode
347
-    @param user_id: id of current logged-in user (if any), determined by @grab_user_id
349
+    @param user_id: id of current logged-in user (if any)
348 350
     @rtype: json dict
349 351
     @return: {
350
-      'user': userdict or None,
351
-      'notebooks': notebooksdict,
352
-      'startup_notes': noteslist,
353
-      'http_url': url,
352
+      'user': user or None,
353
+      'notebooks': notebookslist,
354 354
       'login_url': url,
355
+      'logout_url': url,
355 356
       'rate_plan': rateplandict,
356 357
     }
357 358
     @raise Validation_error: one of the arguments is invalid
@@ -364,37 +365,27 @@ class Users( object ):
364 365
       user = anonymous
365 366
 
366 367
     if not user or not anonymous:
367
-      return dict(
368
-        user = None,
369
-        notebooks = None,
370
-        http_url = u"",
371
-      )
368
+      raise Access_error( u"Sorry, you don't have access to do that." )
372 369
 
373 370
     # in addition to this user's own notebooks, add to that list the anonymous user's notebooks
374 371
     login_url = None
375 372
     notebooks = self.__database.select_many( Notebook, anonymous.sql_load_notebooks() )
376 373
 
377
-    if user_id:
374
+    if user_id and user_id != anonymous.object_id:
378 375
       notebooks += self.__database.select_many( Notebook, user.sql_load_notebooks() )
379 376
     # if the user is not logged in, return a login URL
380 377
     else:
381
-      if len( notebooks ) > 0:
378
+      if len( notebooks ) > 0 and notebooks[ 0 ]:
382 379
         main_notebook = notebooks[ 0 ]
383 380
         login_note = self.__database.select_one( Note, main_notebook.sql_load_note_by_title( u"login" ) )
384 381
         if login_note:
385 382
           login_url = "%s/notebooks/%s?note_id=%s" % ( self.__https_url, main_notebook.object_id, login_note.object_id )
386 383
 
387
-    if include_startup_notes and len( notebooks ) > 0:
388
-      startup_notes = self.__database.select_many( Note, notebooks[ 0 ].sql_load_startup_notes() )
389
-    else:
390
-      startup_notes = []
391
-
392 384
     return dict(
393 385
       user = user,
394 386
       notebooks = notebooks,
395
-      startup_notes = startup_notes,
396
-      http_url = self.__http_url,
397 387
       login_url = login_url,
388
+      logout_url = self.__https_url + u"/",
398 389
       rate_plan = ( user.rate_plan < len( self.__rate_plans ) ) and self.__rate_plans[ user.rate_plan ] or {},
399 390
     )
400 391
 
@@ -452,7 +443,7 @@ class Users( object ):
452 443
       # check if the given user has access to this notebook
453 444
       user = self.__database.load( User, user_id )
454 445
 
455
-      if user and self.__database.select_one( bool, user.sql_has_access( notebook_id ) ):
446
+      if user and self.__database.select_one( bool, user.sql_has_access( notebook_id, read_write ) ):
456 447
         return True
457 448
 
458 449
     return False
@@ -551,12 +542,18 @@ class Users( object ):
551 542
     if len( matching_users ) == 0:
552 543
       raise Password_reset_error( u"There are no Luminotes users with the email address %s" % password_reset.email_address )
553 544
 
554
-    return dict(
545
+    result = self.current( anonymous.object_id )
546
+    result[ "notebook" ] = main_notebook
547
+    result[ "startup_notes" ] = self.__database.select_many( Note, main_notebook.sql_load_startup_notes() )
548
+    result[ "note_read_write" ] = False
549
+    result[ "note" ] = Note.create(
550
+      object_id = u"password_reset",
551
+      contents = unicode( Redeem_reset_note( password_reset_id, matching_users ) ),
555 552
       notebook_id = main_notebook.object_id,
556
-      note_id = u"blank",
557
-      note_contents = unicode( Redeem_reset_note( password_reset_id, matching_users ) ),
558 553
     )
559 554
 
555
+    return result
556
+
560 557
   @expose( view = Json )
561 558
   def reset_password( self, password_reset_id, reset_button, **new_passwords ):
562 559
     """

+ 2
- 0
controller/test/Stub_database.py View File

@@ -6,9 +6,11 @@ class Stub_database( object ):
6 6
     # map of object id to list of saved objects (presumably in increasing order of revisions)
7 7
     self.objects = {}
8 8
     self.user_notebook = {} # map of user_id to ( notebook_id, read_write )
9
+    self.last_saved_obj = None
9 10
     self.__next_id = 0
10 11
 
11 12
   def save( self, obj, commit = False ):
13
+    self.last_saved_obj = obj
12 14
     if obj.object_id in self.objects:
13 15
       self.objects[ obj.object_id ].append( copy( obj ) )
14 16
     else:

+ 1
- 1
controller/test/Test_controller.py View File

@@ -29,7 +29,7 @@ class Test_controller( object ):
29 29
       notebooks = []
30 30
       notebook_tuples = database.user_notebook.get( self.object_id )
31 31
 
32
-      if not notebook_tuples: return None
32
+      if not notebook_tuples: return []
33 33
 
34 34
       for notebook_tuple in notebook_tuples:
35 35
         ( notebook_id, read_write ) = notebook_tuple

+ 118
- 70
controller/test/Test_notebooks.py View File

@@ -1,10 +1,12 @@
1 1
 import cherrypy
2 2
 import cgi
3
+from nose.tools import raises
3 4
 from urllib import quote
4 5
 from Test_controller import Test_controller
5 6
 from model.Notebook import Notebook
6 7
 from model.Note import Note
7 8
 from model.User import User
9
+from controller.Notebooks import Access_error
8 10
 
9 11
 
10 12
 class Test_notebooks( Test_controller ):
@@ -53,53 +55,118 @@ class Test_notebooks( Test_controller ):
53 55
     self.database.save( self.anonymous, commit = False )
54 56
     self.database.execute( self.user.sql_save_notebook( self.anon_notebook.object_id, read_write = False ) )
55 57
 
58
+  def test_default_without_login( self ):
59
+    result = self.http_get(
60
+      "/notebooks/%s" % self.notebook.object_id,
61
+    )
62
+    
63
+    assert u"access" in result[ u"error" ]
64
+    user = self.database.load( User, self.user.object_id )
65
+    assert user.storage_bytes == 0
66
+
56 67
   def test_default( self ):
57
-    result = self.http_get( "/notebooks/%s" % self.notebook.object_id )
68
+    self.login()
69
+
70
+    result = self.http_get(
71
+      "/notebooks/%s" % self.notebook.object_id,
72
+      session_id = self.session_id,
73
+    )
58 74
     
59
-    assert result.get( u"notebook_id" ) == self.notebook.object_id
75
+    assert result.get( u"user" ).object_id == self.user.object_id
76
+    assert len( result.get( u"notebooks" ) ) == 3
77
+    assert result.get( u"login_url" ) is None
78
+    assert result.get( u"logout_url" )
79
+    assert result.get( u"rate_plan" )
80
+    assert result.get( u"notebook" ).object_id == self.notebook.object_id
81
+    assert len( result.get( u"startup_notes" ) ) == 1
82
+    assert result.get( u"note" ) is None
83
+    assert result.get( u"parent_id" ) == None
84
+    assert result.get( u"note_read_write" ) in ( None, True )
85
+
60 86
     user = self.database.load( User, self.user.object_id )
61 87
     assert user.storage_bytes == 0
62 88
 
63 89
   def test_default_with_note( self ):
64
-    result = self.http_get( "/notebooks/%s?note_id=%s" % ( self.notebook.object_id, self.note.object_id ) )
90
+    self.login()
91
+
92
+    result = self.http_get(
93
+      "/notebooks/%s?note_id=%s" % ( self.notebook.object_id, self.note.object_id ),
94
+      session_id = self.session_id,
95
+    )
65 96
     
66
-    assert result.get( u"notebook_id" ) == self.notebook.object_id
67
-    assert result.get( u"note_id" ) == self.note.object_id
97
+    assert result.get( u"user" ).object_id == self.user.object_id
98
+    assert len( result.get( u"notebooks" ) ) == 3
99
+    assert result.get( u"login_url" ) is None
100
+    assert result.get( u"logout_url" )
101
+    assert result.get( u"rate_plan" )
102
+    assert result.get( u"notebook" ).object_id == self.notebook.object_id
103
+    assert len( result.get( u"startup_notes" ) ) == 1
104
+    assert result.get( u"note" ).object_id == self.note.object_id
105
+    assert result.get( u"parent_id" ) == None
106
+    assert result.get( u"note_read_write" ) in ( None, True )
107
+
68 108
     user = self.database.load( User, self.user.object_id )
69 109
     assert user.storage_bytes == 0
70 110
 
71 111
   def test_default_with_note_and_revision( self ):
72
-    result = self.http_get( "/notebooks/%s?note_id=%s&revision=%s" % (
73
-      self.notebook.object_id,
74
-      self.note.object_id,
75
-      quote( unicode( self.note.revision ) ),
76
-    ) )
112
+    self.login()
113
+
114
+    result = self.http_get(
115
+      "/notebooks/%s?note_id=%s&revision=%s" % (
116
+        self.notebook.object_id,
117
+        self.note.object_id,
118
+        quote( unicode( self.note.revision ) ),
119
+      ),
120
+      session_id = self.session_id,
121
+    )
77 122
     
78
-    assert result.get( u"notebook_id" ) == self.notebook.object_id
79
-    assert result.get( u"note_id" ) == self.note.object_id
80
-    assert result.get( u"revision" ) == unicode( self.note.revision )
123
+    assert result.get( u"user" ).object_id == self.user.object_id
124
+    assert len( result.get( u"notebooks" ) ) == 3
125
+    assert result.get( u"login_url" ) is None
126
+    assert result.get( u"logout_url" )
127
+    assert result.get( u"rate_plan" )
128
+    assert result.get( u"notebook" ).object_id == self.notebook.object_id
129
+    assert len( result.get( u"startup_notes" ) ) == 1
130
+    assert result.get( u"note" ).object_id == self.note.object_id
131
+    assert result.get( u"note" ).revision == self.note.revision
132
+    assert result.get( u"parent_id" ) == None
133
+    assert result.get( u"note_read_write" ) == False
134
+
81 135
     user = self.database.load( User, self.user.object_id )
82 136
     assert user.storage_bytes == 0
83 137
 
84 138
   def test_default_with_parent( self ):
85
-    parent_id = "foo"
86
-    result = self.http_get( "/notebooks/%s?parent_id=%s" % ( self.notebook.object_id, parent_id ) )
139
+    self.login()
140
+
141
+    parent_id = u"foo"
142
+    result = self.http_get(
143
+      "/notebooks/%s?parent_id=%s" % ( self.notebook.object_id, parent_id ),
144
+      session_id = self.session_id,
145
+    )
87 146
     
88
-    assert result.get( u"notebook_id" ) == self.notebook.object_id
147
+    assert result.get( u"user" ).object_id == self.user.object_id
148
+    assert len( result.get( u"notebooks" ) ) == 3
149
+    assert result.get( u"login_url" ) is None
150
+    assert result.get( u"logout_url" )
151
+    assert result.get( u"rate_plan" )
152
+    assert result.get( u"notebook" ).object_id == self.notebook.object_id
153
+    assert len( result.get( u"startup_notes" ) ) == 1
154
+    assert result.get( u"note" ) is None
89 155
     assert result.get( u"parent_id" ) == parent_id
156
+    assert result.get( u"note_read_write" ) in ( None, True )
157
+
90 158
     user = self.database.load( User, self.user.object_id )
91 159
     assert user.storage_bytes == 0
92 160
 
93 161
   def test_contents( self ):
94
-    self.login()
95
-
96
-    result = self.http_get(
97
-      "/notebooks/contents?notebook_id=%s" % self.notebook.object_id,
98
-      session_id = self.session_id,
162
+    result = cherrypy.root.notebooks.contents(
163
+      notebook_id = self.notebook.object_id,
164
+      user_id = self.user.object_id,
99 165
     )
100 166
 
101 167
     notebook = result[ "notebook" ]
102 168
     startup_notes = result[ "startup_notes" ]
169
+    assert result[ "note" ] == None
103 170
 
104 171
     assert notebook.object_id == self.notebook.object_id
105 172
     assert notebook.read_write == True
@@ -109,11 +176,10 @@ class Test_notebooks( Test_controller ):
109 176
     assert user.storage_bytes == 0
110 177
 
111 178
   def test_contents_with_note( self ):
112
-    self.login()
113
-
114
-    result = self.http_get(
115
-      "/notebooks/contents?notebook_id=%s&note_id=%s" % ( self.notebook.object_id, self.note.object_id ),
116
-      session_id = self.session_id,
179
+    result = cherrypy.root.notebooks.contents(
180
+      notebook_id = self.notebook.object_id,
181
+      note_id = self.note.object_id,
182
+      user_id = self.user.object_id,
117 183
     )
118 184
 
119 185
     notebook = result[ "notebook" ]
@@ -131,16 +197,13 @@ class Test_notebooks( Test_controller ):
131 197
     assert user.storage_bytes == 0
132 198
 
133 199
   def test_contents_with_note_and_revision( self ):
134
-    self.login()
135
-
136
-    result = self.http_get(
137
-      "/notebooks/contents?notebook_id=%s&note_id=%s&revision=%s" % (
138
-        self.notebook.object_id,
139
-        self.note.object_id,
140
-        quote( unicode( self.note.revision ) ),
141
-      ),
142
-      session_id = self.session_id,
200
+    result = cherrypy.root.notebooks.contents(
201
+      notebook_id = self.notebook.object_id,
202
+      note_id = self.note.object_id,
203
+      revision = unicode( self.note.revision ),
204
+      user_id = self.user.object_id,
143 205
     )
206
+    self.login()
144 207
 
145 208
     notebook = result[ "notebook" ]
146 209
     startup_notes = result[ "startup_notes" ]
@@ -153,54 +216,39 @@ class Test_notebooks( Test_controller ):
153 216
     note = result[ "note" ]
154 217
 
155 218
     assert note.object_id == self.note.object_id
219
+    assert note.revision == self.note.revision
156 220
     user = self.database.load( User, self.user.object_id )
157 221
     assert user.storage_bytes == 0
158 222
 
159
-  def test_contents_with_blank_note( self ):
160
-    self.login()
161
-
162
-    result = self.http_get(
163
-      "/notebooks/contents?notebook_id=%s&note_id=blank" % self.notebook.object_id ,
164
-      session_id = self.session_id,
223
+  @raises( Access_error )
224
+  def test_contents_without_user_id( self ):
225
+    result = cherrypy.root.notebooks.contents(
226
+      notebook_id = self.notebook.object_id,
165 227
     )
166 228
 
167
-    notebook = result[ "notebook" ]
168
-    startup_notes = result[ "startup_notes" ]
169
-
170
-    assert notebook.object_id == self.notebook.object_id
171
-    assert notebook.read_write == True
172
-    assert len( startup_notes ) == 1
173
-    assert startup_notes[ 0 ].object_id == self.note.object_id
174
-
175
-    note = result[ "note" ]
176
-
177
-    assert note.object_id == u"blank"
178
-    assert note.contents == None
179
-    assert note.title == None
180
-    assert note.deleted_from_id == None
181
-    user = self.database.load( User, self.user.object_id )
182
-    assert user.storage_bytes == 0
183
-
184
-  def test_contents_without_login( self ):
185
-    result = self.http_get(
186
-      "/notebooks/contents?notebook_id=%s" % self.notebook.object_id,
187
-      session_id = self.session_id,
229
+  @raises( Access_error )
230
+  def test_contents_with_incorrect_user_id( self ):
231
+    result = cherrypy.root.notebooks.contents(
232
+      notebook_id = self.notebook.object_id,
233
+      user_id = self.anonymous.object_id,
188 234
     )
189 235
 
190
-    assert result.get( "error" )
191
-    user = self.database.load( User, self.user.object_id )
192
-    assert user.storage_bytes == 0
236
+  @raises( Access_error )
237
+  def test_contents_with_unknown_notebook_id( self ):
238
+    result = cherrypy.root.notebooks.contents(
239
+      notebook_id = self.unknown_notebook_id,
240
+      user_id = self.user.object_id,
241
+    )
193 242
 
194 243
   def test_contents_with_read_only_notebook( self ):
195
-    self.login()
196
-
197
-    result = self.http_get(
198
-      "/notebooks/contents?notebook_id=%s" % self.anon_notebook.object_id,
199
-      session_id = self.session_id,
244
+    result = cherrypy.root.notebooks.contents(
245
+      notebook_id = self.anon_notebook.object_id,
246
+      user_id = self.user.object_id,
200 247
     )
201 248
 
202 249
     notebook = result[ "notebook" ]
203 250
     startup_notes = result[ "startup_notes" ]
251
+    assert result[ "note" ] == None
204 252
 
205 253
     assert notebook.object_id == self.anon_notebook.object_id
206 254
     assert notebook.read_write == False

+ 74
- 93
controller/test/Test_users.py View File

@@ -2,6 +2,7 @@ import re
2 2
 import cherrypy
3 3
 import smtplib
4 4
 from pytz import utc
5
+from nose.tools import raises
5 6
 from datetime import datetime, timedelta
6 7
 from nose.tools import raises
7 8
 from Test_controller import Test_controller
@@ -10,6 +11,7 @@ from model.User import User
10 11
 from model.Notebook import Notebook
11 12
 from model.Note import Note
12 13
 from model.Password_reset import Password_reset
14
+from controller.Users import Access_error
13 15
 
14 16
 
15 17
 class Test_users( Test_controller ):
@@ -80,7 +82,7 @@ class Test_users( Test_controller ):
80 82
 
81 83
     assert result[ u"redirect" ].startswith( u"/notebooks/" )
82 84
 
83
-  def test_current_after_signup( self, include_startup_notes = False ):
85
+  def test_current_after_signup( self ):
84 86
     result = self.http_post( "/users/signup", dict(
85 87
       username = self.new_username,
86 88
       password = self.new_password,
@@ -92,12 +94,14 @@ class Test_users( Test_controller ):
92 94
 
93 95
     new_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ]
94 96
 
95
-    result = self.http_get(
96
-      "/users/current?include_startup_notes=%s" % include_startup_notes,
97
-      session_id = session_id,
98
-    )
97
+    user = self.database.last_saved_obj
98
+    assert isinstance( user, User )
99
+    result = cherrypy.root.users.current( user.object_id )
99 100
 
101
+    assert result[ u"user" ].object_id == user.object_id
100 102
     assert result[ u"user" ].username == self.new_username
103
+    assert result[ u"user" ].email_address == self.new_email_address
104
+
101 105
     notebooks = result[ u"notebooks" ]
102 106
     notebook = notebooks[ 0 ]
103 107
     assert notebook.object_id == self.anon_notebook.object_id
@@ -120,22 +124,13 @@ class Test_users( Test_controller ):
120 124
     assert notebook.trash_id == None
121 125
     assert notebook.read_write == True
122 126
 
123
-    startup_notes = result[ "startup_notes" ]
124
-    if include_startup_notes:
125
-      assert len( startup_notes ) == 1
126
-      assert startup_notes[ 0 ].object_id == self.startup_note.object_id
127
-      assert startup_notes[ 0 ].title == self.startup_note.title
128
-      assert startup_notes[ 0 ].contents == self.startup_note.contents
129
-    else:
130
-      assert startup_notes == []
127
+    assert result.get( u"login_url" ) is None
128
+    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/"
131 129
 
132 130
     rate_plan = result[ u"rate_plan" ]
133 131
     assert rate_plan[ u"name" ] == u"super"
134 132
     assert rate_plan[ u"storage_quota_bytes" ] == 1337
135 133
 
136
-  def test_current_with_startup_notes_after_signup( self ):
137
-    self.test_current_after_signup( include_startup_notes = True )
138
-
139 134
   def test_signup_with_different_passwords( self ):
140 135
     result = self.http_post( "/users/signup", dict(
141 136
       username = self.new_username,
@@ -152,18 +147,20 @@ class Test_users( Test_controller ):
152 147
 
153 148
     assert result[ u"redirect" ].startswith( u"/notebooks/" )
154 149
 
155
-  def test_current_after_demo( self, include_startup_notes = False ):
150
+  def test_current_after_demo( self ):
156 151
     result = self.http_post( "/users/demo", dict() )
157 152
     session_id = result[ u"session_id" ]
158 153
 
159 154
     new_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ]
160 155
 
161
-    result = self.http_get(
162
-      "/users/current?include_startup_notes=%s" % include_startup_notes,
163
-      session_id = session_id,
164
-    )
156
+    user = self.database.last_saved_obj
157
+    assert isinstance( user, User )
158
+    result = cherrypy.root.users.current( user.object_id )
159
+
160
+    assert result[ u"user" ].object_id == user.object_id
161
+    assert result[ u"user" ].username is None
162
+    assert result[ u"user" ].email_address is None
165 163
 
166
-    assert result[ u"user" ].username == None
167 164
     notebooks = result[ u"notebooks" ]
168 165
     assert len( notebooks ) == 3
169 166
     notebook = notebooks[ 0 ]
@@ -187,34 +184,25 @@ class Test_users( Test_controller ):
187 184
     assert notebook.trash_id == None
188 185
     assert notebook.read_write == True
189 186
 
190
-    startup_notes = result[ "startup_notes" ]
191
-    if include_startup_notes:
192
-      assert len( startup_notes ) == 1
193
-      assert startup_notes[ 0 ].object_id == self.startup_note.object_id
194
-      assert startup_notes[ 0 ].title == self.startup_note.title
195
-      assert startup_notes[ 0 ].contents == self.startup_note.contents
196
-    else:
197
-      assert startup_notes == []
187
+    assert result.get( u"login_url" ) is None
188
+    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/"
198 189
 
199 190
     rate_plan = result[ u"rate_plan" ]
200 191
     assert rate_plan[ u"name" ] == u"super"
201 192
     assert rate_plan[ u"storage_quota_bytes" ] == 1337
202 193
 
203
-  def test_current_with_startup_notes_after_demo( self ):
204
-    self.test_current_after_demo( include_startup_notes = True )
205
-
206
-  def test_current_after_demo_twice( self, include_startup_notes = False ):
194
+  def test_current_after_demo_twice( self ):
207 195
     result = self.http_post( "/users/demo", dict() )
208 196
     session_id = result[ u"session_id" ]
209 197
 
210 198
     new_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ]
211 199
 
212
-    result = self.http_get(
213
-      "/users/current?include_startup_notes=%s" % include_startup_notes,
214
-      session_id = session_id,
215
-    )
200
+    user = self.database.last_saved_obj
201
+    assert isinstance( user, User )
202
+    result = cherrypy.root.users.current( user.object_id )
216 203
 
217 204
     user_id = result[ u"user" ].object_id
205
+    assert user_id == user.object_id
218 206
 
219 207
     # request a demo for a second time
220 208
     result = self.http_post( "/users/demo", dict(), session_id = session_id )
@@ -224,10 +212,7 @@ class Test_users( Test_controller ):
224 212
 
225 213
     assert notebook_id_again == new_notebook_id
226 214
 
227
-    result = self.http_get(
228
-      "/users/current?include_startup_notes=%s" % include_startup_notes,
229
-      session_id = session_id,
230
-    )
215
+    result = cherrypy.root.users.current( user_id )
231 216
 
232 217
     user_id_again = result[ u"user" ].object_id
233 218
 
@@ -235,9 +220,6 @@ class Test_users( Test_controller ):
235 220
     # should just use the same guest user with the same notebook
236 221
     assert user_id_again == user_id
237 222
 
238
-  def test_current_with_startup_notes_after_demo_twice( self ):
239
-    self.test_current_after_demo_twice( include_startup_notes = True )
240
-
241 223
   def test_login( self ):
242 224
     result = self.http_post( "/users/login", dict(
243 225
       username = self.username,
@@ -270,18 +252,8 @@ class Test_users( Test_controller ):
270 252
 
271 253
     assert result[ u"redirect" ] == self.settings[ u"global" ].get( u"luminotes.http_url" ) + u"/"
272 254
 
273
-  def test_current_after_login( self, include_startup_notes = False ):
274
-    result = self.http_post( "/users/login", dict(
275
-      username = self.username,
276
-      password = self.password,
277
-      login_button = u"login",
278
-    ) )
279
-    session_id = result[ u"session_id" ]
280
-
281
-    result = self.http_get(
282
-      "/users/current?include_startup_notes=%s" % include_startup_notes,
283
-      session_id = session_id,
284
-    )
255
+  def test_current( self ):
256
+    result = cherrypy.root.users.current( self.user.object_id )
285 257
 
286 258
     assert result[ u"user" ]
287 259
     assert result[ u"user" ].object_id == self.user.object_id
@@ -293,32 +265,22 @@ class Test_users( Test_controller ):
293 265
     assert result[ u"notebooks" ][ 1 ].read_write == True
294 266
     assert result[ u"notebooks" ][ 2 ].object_id == self.notebooks[ 1 ].object_id
295 267
     assert result[ u"notebooks" ][ 2 ].read_write == True
296
-    assert result[ u"http_url" ] == self.settings[ u"global" ].get( u"luminotes.http_url" )
297
-    assert result[ u"login_url" ] == None
298
-
299
-    startup_notes = result[ "startup_notes" ]
300
-    if include_startup_notes:
301
-      assert len( startup_notes ) == 1
302
-      assert startup_notes[ 0 ].object_id == self.startup_note.object_id
303
-      assert startup_notes[ 0 ].title == self.startup_note.title
304
-      assert startup_notes[ 0 ].contents == self.startup_note.contents
305
-    else:
306
-      assert startup_notes == []
307
-
308
-  def test_current_with_startup_notes_after_login( self ):
309
-    self.test_current_after_login( include_startup_notes = True )
310
-
311
-  def test_current_without_login( self, include_startup_notes = False ):
312
-    result = self.http_get(
313
-      "/users/current?include_startup_notes=%s" % include_startup_notes,
314
-    )
268
+    assert result[ u"login_url" ] is None
269
+    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/"
270
+
271
+    rate_plan = result[ u"rate_plan" ]
272
+    assert rate_plan
273
+    assert rate_plan[ u"name" ] == u"super"
274
+    assert rate_plan[ u"storage_quota_bytes" ] == 1337
275
+
276
+  def test_current_anonymous( self ):
277
+    result = cherrypy.root.users.current( self.anonymous.object_id )
315 278
 
316 279
     assert result[ u"user" ].username == "anonymous"
317 280
     assert len( result[ u"notebooks" ] ) == 1
318 281
     assert result[ u"notebooks" ][ 0 ].object_id == self.anon_notebook.object_id
319 282
     assert result[ u"notebooks" ][ 0 ].name == self.anon_notebook.name
320 283
     assert result[ u"notebooks" ][ 0 ].read_write == False
321
-    assert result[ u"http_url" ] == self.settings[ u"global" ].get( u"luminotes.http_url" )
322 284
 
323 285
     login_note = self.database.select_one( Note, self.anon_notebook.sql_load_note_by_title( u"login" ) )
324 286
     assert result[ u"login_url" ] == u"%s/notebooks/%s?note_id=%s" % (
@@ -326,18 +288,12 @@ class Test_users( Test_controller ):
326 288
       self.anon_notebook.object_id,
327 289
       login_note.object_id,
328 290
     )
291
+    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/"
329 292
 
330
-    startup_notes = result[ "startup_notes" ]
331
-    if include_startup_notes:
332
-      assert len( startup_notes ) == 1
333
-      assert startup_notes[ 0 ].object_id == self.startup_note.object_id
334
-      assert startup_notes[ 0 ].title == self.startup_note.title
335
-      assert startup_notes[ 0 ].contents == self.startup_note.contents
336
-    else:
337
-      assert startup_notes == []
338
-
339
-  def test_current_with_startup_notes_without_login( self ):
340
-    self.test_current_without_login( include_startup_notes = True )
293
+    rate_plan = result[ u"rate_plan" ]
294
+    assert rate_plan
295
+    assert rate_plan[ u"name" ] == u"super"
296
+    assert rate_plan[ u"storage_quota_bytes" ] == 1337
341 297
 
342 298
   def test_update_storage( self ):
343 299
     previous_revision = self.user.revision
@@ -409,11 +365,36 @@ class Test_users( Test_controller ):
409 365
 
410 366
     result = self.http_get( "/users/redeem_reset/%s" % password_reset_id )
411 367
 
412
-    assert result[ u"notebook_id" ] == self.anon_notebook.object_id
413
-    assert result[ u"note_id" ]
414
-    assert u"password reset" in result[ u"note_contents" ]
415
-    assert self.user.username in result[ u"note_contents" ]
416
-    assert self.user2.username in result[ u"note_contents" ]
368
+    assert result[ u"user" ].username == "anonymous"
369
+    assert len( result[ u"notebooks" ] ) == 1
370
+    assert result[ u"notebooks" ][ 0 ].object_id == self.anon_notebook.object_id
371
+    assert result[ u"notebooks" ][ 0 ].name == self.anon_notebook.name
372
+    assert result[ u"notebooks" ][ 0 ].read_write == False
373
+
374
+    login_note = self.database.select_one( Note, self.anon_notebook.sql_load_note_by_title( u"login" ) )
375
+    assert result[ u"login_url" ] == u"%s/notebooks/%s?note_id=%s" % (
376
+      self.settings[ u"global" ][ u"luminotes.https_url" ],
377
+      self.anon_notebook.object_id,
378
+      login_note.object_id,
379
+    )
380
+    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/"
381
+
382
+    rate_plan = result[ u"rate_plan" ]
383
+    assert rate_plan
384
+    assert rate_plan[ u"name" ] == u"super"
385
+    assert rate_plan[ u"storage_quota_bytes" ] == 1337
386
+
387
+    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
388
+    assert len( result[ u"startup_notes" ] ) == 1
389
+    assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
390
+    assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
391
+    assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
392
+    assert result[ u"note_read_write" ] is False
393
+    assert result[ u"note" ].title == u"complete your password reset"
394
+    assert result[ u"note" ].notebook_id == self.anon_notebook.object_id
395
+    assert u"password reset" in result[ u"note" ].contents
396
+    assert self.user.username in result[ u"note" ].contents
397
+    assert self.user2.username in result[ u"note" ].contents
417 398
 
418 399
   def test_redeem_reset_unknown( self ):
419 400
     password_reset_id = u"unknownresetid"

+ 25
- 8
static/css/style.css View File

@@ -13,14 +13,6 @@ a:hover {
13 13
   color: #ff6600;
14 14
 }
15 15
 
16
-noscript {
17
-  text-align: left;
18
-}
19
-
20
-noscript h3 {
21
-  margin-bottom: 0.5em;
22
-}
23
-
24 16
 ul li {
25 17
   margin-top: 0.5em;
26 18
 }
@@ -206,6 +198,16 @@ ol li {
206 198
   -webkit-border-radius: 0.5em 0 0 0;
207 199
 }
208 200
 
201
+#static_notes {
202
+  text-align: left;
203
+  margin: 1em;
204
+  line-height: 140%;
205
+}
206
+
207
+#static_notes h3 {
208
+  margin-bottom: 0.5em;
209
+}
210
+
209 211
 #notes {
210 212
   text-align: left;
211 213
   margin-top: 1em;
@@ -405,3 +407,18 @@ ol li {
405 407
   -moz-border-radius: 0.5em;
406 408
   -webkit-border-radius: 0.5em;
407 409
 }
410
+
411
+.button {
412
+  border-style: outset;
413
+  border-width: 0px;
414
+  background-color: #d0e0f0;
415
+  font-size: 100%;
416
+  outline: none;
417
+  -moz-border-radius: 0.5em;
418
+  -webkit-border-radius: 0.5em;
419
+}
420
+
421
+.button:hover {
422
+  background-color: #ffcc66;
423
+}
424
+

+ 0
- 4
static/html/no javascript.html View File

@@ -1,4 +0,0 @@
1
-<h3>Luminotes requires JavaScript</h3>
2
-
3
-So if you'd like to check out this site any further, please enable JavaScript
4
-in your web browser, and then reload this page. Sorry for the inconvenience.

+ 85
- 183
static/js/Wiki.js View File

@@ -1,3 +1,4 @@
1
+
1 2
 function Wiki( invoker ) {
2 3
   this.next_id = null;
3 4
   this.focused_editor = null;
@@ -5,104 +6,52 @@ function Wiki( invoker ) {
5 6
   this.notebook = null;
6 7
   this.notebook_id = getElement( "notebook_id" ).value;
7 8
   this.parent_id = getElement( "parent_id" ).value; // id of the notebook containing this one
8
-  this.read_write = false;
9 9
   this.startup_notes = new Array();  // map of startup notes: note id to bool
10 10
   this.open_editors = new Array();   // map of open notes: note title to editor
11 11
   this.all_notes_editor = null;      // editor for display of list of all notes
12 12
   this.search_results_editor = null; // editor for display of search results
13 13
   this.invoker = invoker;
14
-  this.rate_plan = null;
14
+  this.rate_plan = evalJSON( getElement( "rate_plan" ).value );
15 15
   this.storage_usage_high = false;
16 16
 
17
+  // grab the current notebook from the list of available notebooks
18
+  var notebooks = evalJSON( getElement( "notebooks" ).value );
19
+  for ( var i in notebooks ) {
20
+    if ( notebooks[ i ].object_id == this.notebook_id ) {
21
+      this.notebook = notebooks[ i ]
22
+      break;
23
+    }
24
+  }
25
+
26
+  // populate the wiki with startup notes
27
+  this.populate(
28
+    evalJSON( getElement( "startup_notes" ).value || "null" ),
29
+    evalJSON( getElement( "note" ).value || "null" ),
30
+    evalJSON( getElement( "note_read_write" ).value || "true" )
31
+  );
32
+
33
+  this.display_storage_usage( evalJSON( getElement( "storage_bytes" ).value || "0" ) );
34
+
17 35
   connect( this.invoker, "error_message", this, "display_error" );
18 36
   connect( this.invoker, "message", this, "display_message" );
19 37
   connect( "search_form", "onsubmit", this, "search" );
20 38
   connect( "html", "onclick", this, "background_clicked" );
21 39
 
22
-  // get info on the requested notebook (if any)
23 40
   var self = this;
24
-  if ( this.notebook_id ) {
25
-    this.invoker.invoke(
26
-      "/notebooks/contents", "GET", {
27
-        "notebook_id": this.notebook_id,
28
-        "note_id": getElement( "note_id" ).value,
29
-        "revision": getElement( "revision" ).value
30
-      },
31
-      function( result ) { self.populate( result ); }
32
-    );
33
-    var include_startup_notes = false;
34
-  } else {
35
-    var include_startup_notes = true;
41
+  var logout_link = getElement( "logout_link" );
42
+  if ( logout_link ) {
43
+    connect( "logout_link", "onclick", function ( event ) {
44
+      self.save_editor( null, true );
45
+      self.invoker.invoke( "/users/logout", "POST" );
46
+      event.stop();
47
+    } );
36 48
   }
37
-
38
-  // get info on the current user (logged-in or anonymous)
39
-  this.invoker.invoke( "/users/current", "GET", {
40
-      "include_startup_notes": include_startup_notes
41
-    },
42
-    function( result ) { self.display_user( result ); }
43
-  );
44 49
 }
45 50
 
46 51
 Wiki.prototype.update_next_id = function ( result ) {
47 52
   this.next_id = result.next_id;
48 53
 }
49 54
 
50
-Wiki.prototype.display_user = function ( result ) {
51
-  // if no notebook id was requested, then just display the user's default notebook
52
-  if ( !this.notebook_id ) {
53
-    this.notebook_id = result.notebooks[ 0 ].object_id;
54
-    this.populate( { "notebook" : result.notebooks[ 0 ], "startup_notes": result.startup_notes } );
55
-  }
56
-
57
-  var user_span = createDOM( "span" );
58
-  replaceChildNodes( "user_area", user_span );
59
-
60
-  // if not logged in, display a login link
61
-  if ( result.user.username == "anonymous" && result.login_url ) {
62
-    appendChildNodes( user_span, createDOM( "a", { "href": result.login_url, "id": "login_link" }, "login" ) );
63
-    return;
64
-  }
65
-
66
-  // display links for current notebook and a list of all notebooks that the user has access to
67
-  var notebooks_span = createDOM( "span" );
68
-  replaceChildNodes( "notebooks_area", notebooks_span );
69
-
70
-  appendChildNodes( notebooks_span, createDOM( "h4", "notebooks" ) );
71
-
72
-  for ( var i in result.notebooks ) {
73
-    var notebook = result.notebooks[ i ];
74
-
75
-    if ( notebook.name == "Luminotes" || notebook.name == "trash" )
76
-      continue;
77
-
78
-    var div_class = "link_area_item";
79
-    if ( notebook.object_id == this.notebook_id )
80
-      div_class += " current_notebook_name";
81
-
82
-    appendChildNodes( notebooks_span, createDOM( "div", {
83
-      "class": div_class
84
-    }, createDOM( "a", {
85
-      "href": "/notebooks/" + notebook.object_id,
86
-      "id": "notebook_" + notebook.object_id
87
-    }, notebook.name ) ) );
88
-  }
89
-
90
-  this.rate_plan = result.rate_plan;
91
-  this.display_storage_usage( result.user.storage_bytes );
92
-
93
-  // display the name of the logged in user and a logout link
94
-  appendChildNodes( user_span, "logged in as " + ( result.user.username || "a guest" ) );
95
-  appendChildNodes( user_span, " | " );
96
-  appendChildNodes( user_span, createDOM( "a", { "href": result.http_url + "/", "id": "logout_link" }, "logout" ) );
97
-
98
-  var self = this;
99
-  connect( "logout_link", "onclick", function ( event ) {
100
-    self.save_editor( null, true );
101
-    self.invoker.invoke( "/users/logout", "POST" );
102
-    event.stop();
103
-  } );
104
-}
105
-
106 55
 Wiki.prototype.display_storage_usage = function( storage_bytes ) {
107 56
   if ( !storage_bytes )
108 57
     return;
@@ -113,7 +62,10 @@ Wiki.prototype.display_storage_usage = function( storage_bytes ) {
113 62
     return Math.round( storage_bytes / MEGABYTE );
114 63
   }
115 64
 
116
-  var quota_bytes = this.rate_plan.storage_quota_bytes || 0;
65
+  var quota_bytes = this.rate_plan.storage_quota_bytes;
66
+  if ( !quota_bytes )
67
+    return;
68
+
117 69
   var usage_percent = Math.round( storage_bytes / quota_bytes * 100.0 );
118 70
 
119 71
   if ( usage_percent > 90 ) {
@@ -136,68 +88,49 @@ Wiki.prototype.display_storage_usage = function( storage_bytes ) {
136 88
   );
137 89
 }
138 90
 
139
-Wiki.prototype.populate = function ( result ) {
140
-  this.notebook = result.notebook;
141
-  var self = this;
142
-
143
-  var header_area = getElement( "notebook_header_area" );
144
-  replaceChildNodes( header_area, createDOM( "b", {}, this.notebook.name ) );
91
+Wiki.prototype.populate = function ( startup_notes, note, note_read_write ) {
92
+  // create an editor for each startup note in the received notebook, focusing the first one
93
+  var focus = true;
94
+  for ( var i in startup_notes ) {
95
+    var startup_note = startup_notes[ i ];
96
+    this.startup_notes[ startup_note.object_id ] = true;
145 97
 
146
-  if ( this.parent_id ) {
147
-    appendChildNodes( header_area, createDOM( "span", {}, ": " ) );
148
-    var empty_trash_link = createDOM( "a", { "href": location.href }, "empty trash" );
149
-    appendChildNodes( header_area, empty_trash_link );
150
-    connect( empty_trash_link, "onclick", function ( event ) { try{ self.delete_all_editors( event ); } catch(e){ alert(e); } } );
98
+    // don't actually create an editor if a particular note was provided in the result
99
+    if ( !note ) {
100
+      var editor = this.create_editor(
101
+        startup_note.object_id,
102
+        // grab this note's contents from the static <noscript> area
103
+        getElement( "static_note_" + startup_note.object_id ).innerHTML,
104
+        startup_note.deleted_from_id,
105
+        startup_note.revision,
106
+        this.notebook.read_write, false, focus
107
+      );
151 108
 
152
-    appendChildNodes( header_area, createDOM( "span", {}, " | " ) );
153
-    appendChildNodes( header_area, createDOM( "a", { "href": "/notebooks/" + this.parent_id }, "return to notebook" ) );
109
+      this.open_editors[ startup_note.title ] = editor;
110
+      focus = false;
111
+    }
154 112
   }
155 113
 
156
-  var span = createDOM( "span" );
157
-  replaceChildNodes( "this_notebook_area", span );
114
+  // if one particular note was provided, then just display an editor for that note
115
+  if ( note )
116
+    this.create_editor(
117
+      note.object_id,
118
+      getElement( "static_note_" + note.object_id ).innerHTML,
119
+      note.deleted_from_id,
120
+      note.revision,
121
+      this.notebook.read_write && note_read_write, false, true
122
+    );
158 123
 
159
-  appendChildNodes( span, createDOM( "h4", "this notebook" ) );
160
-  if ( !this.parent_id ) {
161
-    appendChildNodes( span, createDOM( "div", { "class": "link_area_item" },
162
-      createDOM( "a", { "href": location.href, "id": "all_notes_link", "title": "View a list of all notes in this notebook." }, "all notes" )
163
-    ) );
164
-  }
165
-  if ( this.notebook.name != "Luminotes" ) {
166
-    appendChildNodes( span, createDOM( "div", { "class": "link_area_item" },
167
-      createDOM( "a", { "href": "/notebooks/download_html/" + this.notebook.object_id, "id": "download_html_link", "title": "Download a stand-alone copy of the entire wiki notebook." }, "download as html" )
168
-    ) );
169
-  }
124
+  if ( startup_notes.length == 0 && !note )
125
+    this.display_empty_message();
170 126
 
171
-  if ( this.notebook.read_write ) {
172
-    this.read_write = true;
173
-    removeElementClass( "toolbar", "undisplayed" );
174
-
175
-    if ( this.notebook.trash_id ) {
176
-      appendChildNodes( span, createDOM( "div", { "class": "link_area_item" },
177
-        createDOM( "a", {
178
-          "href": "/notebooks/" + this.notebook.trash_id + "?parent_id=" + this.notebook.object_id,
179
-          "id": "trash_link",
180
-          "title": "Look here for notes you've deleted."
181
-        }, "trash" )
182
-      ) );
183
-    } else if ( this.notebook.name == "trash" ) {
184
-      appendChildNodes( span, createDOM( "div", { "class": "link_area_item current_trash_notebook_name" },
185
-        createDOM( "a", {
186
-          "href": location.href,
187
-          "id": "trash_link",
188
-          "title": "Look here for notes you've deleted."
189
-        }, "trash" )
190
-      ) );
191
-
192
-      var header_area = getElement( "notebook_header_area" )
193
-      removeElementClass( header_area, "current_notebook_name" );
194
-      addElementClass( header_area, "current_trash_notebook_name" );
195
-
196
-      var border = getElement( "notebook_border" )
197
-      removeElementClass( border, "current_notebook_name" );
198
-      addElementClass( border, "current_trash_notebook_name" );
199
-    }
127
+  var self = this;
128
+
129
+  var empty_trash_link = getElement( "empty_trash_link" );
130
+  if ( empty_trash_link )
131
+    connect( empty_trash_link, "onclick", function ( event ) { self.delete_all_editors( event ); } );
200 132
 
133
+  if ( this.notebook.read_write ) {
201 134
     connect( window, "onunload", function ( event ) { self.editor_focused( null, true ); } );
202 135
     connect( "bold", "onclick", function ( event ) { self.toggle_button( event, "bold" ); } );
203 136
     connect( "italic", "onclick", function ( event ) { self.toggle_button( event, "italic" ); } );
@@ -215,51 +148,20 @@ Wiki.prototype.populate = function ( result ) {
215 148
     );
216 149
   }
217 150
 
218
-  var self = this;
219
-  if ( !this.parent_id ) {
220
-    connect( "all_notes_link", "onclick", function ( event ) {
151
+  var all_notes_link = getElement( "all_notes_link" );
152
+  if ( all_notes_link ) {
153
+    connect( all_notes_link, "onclick", function ( event ) {
221 154
       self.load_editor( "all notes", "null" );
222 155
       event.stop();
223 156
     } );
224 157
   }
225 158
 
226
-  if ( this.notebook.name != "Luminotes" ) {
227
-    connect( "download_html_link", "onclick", function ( event ) {
159
+  var download_html_link = getElement( "download_html_link" );
160
+  if ( download_html_link ) {
161
+    connect( download_html_link, "onclick", function ( event ) {
228 162
       self.save_editor( null, true );
229 163
     } );
230 164
   }
231
-
232
-  // create an editor for each startup note in the received notebook, focusing the first one
233
-  var focus = true;
234
-  for ( var i in result.startup_notes ) {
235
-    var note = result.startup_notes[ i ];
236
-    if ( !note ) continue;
237
-    this.startup_notes[ note.object_id ] = true;
238
-
239
-    // don't actually create an editor if a particular note was provided in the result
240
-    if ( !result.note ) {
241
-      var editor = this.create_editor( note.object_id, note.contents, note.deleted_from_id, note.revision, this.read_write, false, focus );
242
-      this.open_editors[ note.title ] = editor;
243
-      focus = false;
244
-    }
245
-  }
246
-
247
-  // if one particular note was provided, then just display an editor for that note
248
-  var read_write = this.read_write;
249
-  var revision_element = getElement( "revision" );
250
-  if ( revision_element && revision_element.value ) read_write = false;
251
-
252
-  if ( result.note )
253
-    this.create_editor(
254
-      result.note.object_id,
255
-      result.note.contents || getElement( "note_contents" ).value,
256
-      result.note.deleted_from_id,
257
-      result.note.revision,
258
-      read_write, false, true
259
-    );
260
-
261
-  if ( result.startup_notes.length == 0 && !result.note )
262
-    this.display_empty_message();
263 165
 }
264 166
 
265 167
 Wiki.prototype.background_clicked = function ( event ) {
@@ -288,7 +190,7 @@ Wiki.prototype.create_blank_editor = function ( event ) {
288 190
     }
289 191
   }
290 192
 
291
-  var editor = this.create_editor( undefined, undefined, undefined, undefined, this.read_write, true, true );
193
+  var editor = this.create_editor( undefined, undefined, undefined, undefined, this.notebook.read_write, true, true );
292 194
   this.blank_editor_id = editor.id;
293 195
 }
294 196
 
@@ -472,7 +374,7 @@ Wiki.prototype.parse_loaded_editor = function ( result, note_title, requested_re
472 374
   if ( requested_revision )
473 375
     var read_write = false; // show previous revisions as read-only
474 376
   else
475
-    var read_write = this.read_write;
377
+    var read_write = this.notebook.read_write;
476 378
 
477 379
   var editor = this.create_editor( id, note_text, deleted_from_id, actual_revision, read_write, true, false );
478 380
   id = editor.id;
@@ -485,7 +387,7 @@ Wiki.prototype.parse_loaded_editor = function ( result, note_title, requested_re
485 387
 Wiki.prototype.create_editor = function ( id, note_text, deleted_from_id, revision, read_write, highlight, focus ) {
486 388
   var self = this;
487 389
   if ( isUndefinedOrNull( id ) ) {
488
-    if ( this.read_write ) {
390
+    if ( this.notebook.read_write ) {
489 391
       id = this.next_id;
490 392
       this.invoker.invoke( "/next_id", "POST", null,
491 393
         function( result ) { self.update_next_id( result ); }
@@ -496,7 +398,7 @@ Wiki.prototype.create_editor = function ( id, note_text, deleted_from_id, revisi
496 398
   }
497 399
 
498 400
   // for read-only notes within read-write notebooks, tack the revision timestamp onto the start of the note text
499
-  if ( !read_write && this.read_write && revision ) {
401
+  if ( !read_write && this.notebook.read_write && revision ) {
500 402
     var short_revision = this.brief_revision( revision );
501 403
     note_text = "<p>Previous revision from " + short_revision + "</p>" + note_text;
502 404
   }
@@ -504,7 +406,7 @@ Wiki.prototype.create_editor = function ( id, note_text, deleted_from_id, revisi
504 406
   var startup = this.startup_notes[ id ];
505 407
   var editor = new Editor( id, this.notebook_id, note_text, deleted_from_id, revision, read_write, startup, highlight, focus );
506 408
 
507
-  if ( this.read_write ) {
409
+  if ( this.notebook.read_write ) {
508 410
     connect( editor, "state_changed", this, "editor_state_changed" );
509 411
     connect( editor, "title_changed", this, "editor_title_changed" );
510 412
     connect( editor, "key_pressed", this, "editor_key_pressed" );
@@ -698,7 +600,7 @@ Wiki.prototype.hide_editor = function ( event, editor ) {
698 600
 
699 601
   if ( editor ) {
700 602
     // before hiding an editor, save it
701
-    if ( this.read_write )
603
+    if ( this.notebook.read_write )
702 604
       this.save_editor( editor );
703 605
 
704 606
     editor.shutdown();
@@ -724,7 +626,7 @@ Wiki.prototype.delete_editor = function ( event, editor ) {
724 626
     this.save_editor( editor, true );
725 627
 
726 628
     var self = this;
727
-    if ( this.read_write && editor.read_write ) {
629
+    if ( this.notebook.read_write && editor.read_write ) {
728 630
       this.invoker.invoke( "/notebooks/delete_note", "POST", { 
729 631
         "notebook_id": this.notebook_id,
730 632
         "note_id": editor.id
@@ -771,7 +673,7 @@ Wiki.prototype.undelete_editor_via_trash = function ( event, editor ) {
771 673
 
772 674
     this.save_editor( editor, true );
773 675
 
774
-    if ( this.read_write && editor.read_write ) {
676
+    if ( this.notebook.read_write && editor.read_write ) {
775 677
       var self = this;
776 678
       this.invoker.invoke( "/notebooks/undelete_note", "POST", { 
777 679
         "notebook_id": editor.deleted_from_id,
@@ -794,7 +696,7 @@ Wiki.prototype.undelete_editor_via_undo = function( event, editor ) {
794 696
   this.clear_pulldowns();
795 697
 
796 698
   if ( editor ) {
797
-    if ( this.read_write && editor.read_write ) {
699
+    if ( this.notebook.read_write && editor.read_write ) {
798 700
       var self = this;
799 701
       this.invoker.invoke( "/notebooks/undelete_note", "POST", { 
800 702
         "notebook_id": this.notebook_id,
@@ -908,7 +810,7 @@ Wiki.prototype.display_search_results = function ( result ) {
908 810
     }
909 811
 
910 812
     // otherwise, create an editor for the one note
911
-    this.create_editor( note.object_id, note.contents, note.deleted_from_id, note.revision, this.read_write, true, true );
813
+    this.create_editor( note.object_id, note.contents, note.deleted_from_id, note.revision, this.notebook.read_write, true, true );
912 814
     return;
913 815
   }
914 816
 
@@ -1077,7 +979,7 @@ Wiki.prototype.delete_all_editors = function ( event ) {
1077 979
 
1078 980
   this.startup_notes = new Array();
1079 981
 
1080
-  if ( this.read_write ) {
982
+  if ( this.notebook.read_write ) {
1081 983
     var self = this;
1082 984
     this.invoker.invoke( "/notebooks/delete_all_notes", "POST", { 
1083 985
       "notebook_id": this.notebook_id
@@ -1291,7 +1193,7 @@ function Changes_pulldown( wiki, notebook_id, invoker, editor ) {
1291 1193
   revisions_list.reverse();
1292 1194
 
1293 1195
   var self = this;
1294
-  for ( var i = 0; i < revisions_list.length; ++i ) {
1196
+  for ( var i = 0; i < revisions_list.length - 1; ++i ) { // -1 to skip the oldest revision
1295 1197
     var revision = revisions_list[ i ];
1296 1198
     var short_revision = this.wiki.brief_revision( revision );
1297 1199
     var href = "/notebooks/" + this.notebook_id + "?" + queryString(

+ 11
- 7
view/Error_page.py View File

@@ -1,5 +1,5 @@
1 1
 from Page import Page
2
-from Tags import Div, H2, P, A, Ul, Li, Strong
2
+from Tags import Div, H2, P, A, Ul, Li, Strong, Noscript
3 3
 
4 4
 
5 5
 class Error_page( Page ):
@@ -20,6 +20,16 @@ class Error_page( Page ):
20 20
       title,
21 21
       Div(
22 22
         H2( title ),
23
+        Noscript(
24
+          P(
25
+            Strong(
26
+              u"""
27
+              Please enable JavaScript in your web browser. JavaScript is necessary for many Luminotes
28
+              features to work properly.
29
+              """,
30
+            ),
31
+          ),
32
+        ),
23 33
         P(
24 34
           u"Something went wrong! If you care, please",
25 35
           A( "let me know about it.", href = "mailto:%s" % support_email ),
@@ -34,12 +44,6 @@ class Error_page( Page ):
34 44
         P(
35 45
           u"Thanks!",
36 46
         ),
37
-        P(
38
-          Strong( u"P.S." ),
39
-          u"""
40
-          If JavaScript isn't enabled in your browser, please enable it.
41
-          """,
42
-        ),
43 47
         class_ = u"error_box",
44 48
       ),
45 49
       include_js = False,

+ 11
- 1
view/Json.py View File

@@ -5,11 +5,21 @@ from datetime import datetime, date
5 5
 
6 6
 
7 7
 class Json( JSONEncoder ):
8
-  def __init__( self, **kwargs ):
8
+  def __init__( self, *args, **kwargs ):
9 9
     JSONEncoder.__init__( self )
10
+
11
+    if args and kwargs:
12
+      raise ValueError( "Please provide either args or kwargs, not both." )
13
+
14
+    self.__args = args
10 15
     self.__kwargs = kwargs
11 16
 
12 17
   def __str__( self ):
18
+    if self.__args:
19
+      if len( self.__args ) == 1:
20
+        return self.encode( self.__args[ 0 ] )
21
+      return self.encode( self.__args )
22
+
13 23
     return self.encode( self.__kwargs )
14 24
 
15 25
   def default( self, obj ):

+ 58
- 2
view/Link_area.py View File

@@ -1,17 +1,73 @@
1
-from Tags import Div, H3, A
1
+from Tags import Div, Span, H4, A
2 2
 
3 3
 
4 4
 class Link_area( Div ):
5
-  def __init__( self, notebook_id ):
5
+  def __init__( self, notebooks, notebook, parent_id ):
6 6
     Div.__init__(
7 7
       self,
8 8
       Div(
9
+        H4( u"this notebook" ),
10
+        ( parent_id is None ) and Div(
11
+          A(
12
+            u"all notes",
13
+            href = u"/notebooks/%s" % notebook.object_id,
14
+            id = u"all_notes_link",
15
+            title = u"View a list of all notes in this notebook.",
16
+          ),
17
+          class_ = u"link_area_item",
18
+        ) or None,
19
+
20
+        ( notebook.name != u"Luminotes" ) and Div(
21
+          A(
22
+            u"download as html",
23
+            href = u"/notebooks/download_html/%s" % notebook.object_id,
24
+            id = u"download_html_link",
25
+            title = u"Download a stand-alone copy of the entire wiki notebook.",
26
+          ),
27
+          class_ = u"link_area_item",
28
+        ) or None,
29
+
30
+        notebook.read_write and Span(
31
+          notebook.trash_id and Div(
32
+            A(
33
+              u"trash",
34
+              href = u"/notebooks/%s?parent_id=%s" % ( notebook.trash_id, notebook.object_id ),
35
+              id = u"trash_link",
36
+              title = u"Look here for notes you've deleted.",
37
+            ),
38
+            class_ = u"link_area_item",
39
+          ) or None,
40
+
41
+          ( notebook.name == u"trash" ) and Div(
42
+            A(
43
+              u"trash",
44
+              href = u"#",
45
+              id = u"trash_link",
46
+              title = u"Look here for notes you've deleted.",
47
+            ),
48
+            class_ = u"link_area_item current_trash_notebook_name",
49
+          ) or None,
50
+        ) or None,
51
+
9 52
         id = u"this_notebook_area",
10 53
       ),
54
+
11 55
       Div(
56
+        [ Span(
57
+          ( index == 0 ) and H4( u"notebooks" ) or None,
58
+          Div(
59
+          A(
60
+            nb.name,
61
+            href = u"/notebooks/%s" % nb.object_id,
62
+            id = u"notebook_%s" % nb.object_id,
63
+          ),
64
+          class_ = ( nb.object_id == notebook.object_id ) and u"link_area_item current_notebook_name" or u"link_area_item",
65
+        ) ) for ( index, nb ) in enumerate( notebooks ) if nb.name not in ( u"Luminotes", u"trash" ) ],
12 66
         id = u"notebooks_area",
13 67
       ),
68
+
14 69
       Div(
15 70
         id = u"storage_usage_area",
16 71
       ),
72
+      id = u"link_area",
17 73
     )

+ 77
- 23
view/Main_page.py View File

@@ -1,43 +1,86 @@
1 1
 from cgi import escape
2 2
 from Page import Page
3
-from Tags import Input, Div, Noscript, H2, H4, A, Br
3
+from Tags import Input, Div, Span, H2, H4, A, Br, Strong, Script
4 4
 from Search_form import Search_form
5
+from User_area import User_area
5 6
 from Link_area import Link_area
6 7
 from Toolbar import Toolbar
8
+from Json import Json
7 9
 
8 10
 
9 11
 class Main_page( Page ):
10
-  def __init__( self, notebook_id = None, note_id = None, parent_id = None, revision = None, note_contents = None ):
11
-    title = None
12
-    note_contents = note_contents and escape( note_contents, quote = True ) or ""
12
+  def __init__(
13
+    self,
14
+    user,
15
+    rate_plan,
16
+    notebooks,
17
+    notebook,
18
+    parent_id = None,
19
+    login_url = None,
20
+    logout_url = None,
21
+    startup_notes = None,
22
+    note = None,
23
+    note_read_write = True,
24
+  ):
25
+    startup_note_ids = [ startup_note.object_id for startup_note in startup_notes ]
26
+
27
+    static_notes = Div(
28
+      note and Div(
29
+        note.contents,
30
+        id = "static_note_%s" % note.object_id,
31
+      ) or
32
+      [ Div(
33
+        startup_note.contents,
34
+        id = "static_note_%s" % startup_note.object_id
35
+      ) for startup_note in startup_notes ],
36
+      id = "static_notes",
37
+    )
38
+
39
+    # Since the contents of these notes are included in the static_notes section below, don't
40
+    # include them again in the hidden fields here. Accomplish this by making custom dicts for
41
+    # sending to the client.
42
+    startup_note_dicts = [ {
43
+      u"object_id" : startup_note.object_id,
44
+      u"revision" : startup_note.revision,
45
+      u"deleted_from_id" : startup_note.deleted_from_id,
46
+    } for startup_note in startup_notes ]
13 47
 
48
+    if note:
49
+      note_dict = {
50
+        u"object_id" : note.object_id,
51
+        u"revision" : note.revision,
52
+        u"deleted_from_id" : note.deleted_from_id,
53
+      }
54
+
55
+    def json( string ):
56
+      return escape( unicode( Json( string ) ), quote = True )
57
+
58
+    title = None
14 59
     Page.__init__(
15 60
       self,
16 61
       title,
17
-      Input( type = u"hidden", name = u"notebook_id", id = u"notebook_id", value = notebook_id or "" ),
18
-      Input( type = u"hidden", name = u"note_id", id = u"note_id", value = note_id or "" ),
62
+      Input( type = u"hidden", name = u"storage_bytes", id = u"storage_bytes", value = user.storage_bytes ),
63
+      Input( type = u"hidden", name = u"rate_plan", id = u"rate_plan", value = json( rate_plan ) ),
64
+      Input( type = u"hidden", name = u"notebooks", id = u"notebooks", value = json( notebooks ) ),
65
+      Input( type = u"hidden", name = u"notebook_id", id = u"notebook_id", value = notebook.object_id ),
19 66
       Input( type = u"hidden", name = u"parent_id", id = u"parent_id", value = parent_id or "" ),
20
-      Input( type = u"hidden", name = u"revision", id = u"revision", value = revision or "" ),
21
-      Input( type = u"hidden", name = u"note_contents", id = u"note_contents", value = note_contents ),
67
+      Input( type = u"hidden", name = u"startup_notes", id = u"startup_notes", value = json( startup_note_dicts ) ),
68
+      Input( type = u"hidden", name = u"note", id = u"note", value = note and json( note_dict ) or "" ),
69
+      Input( type = u"hidden", name = u"note_read_write", id = u"note_read_write", value = json( note_read_write ) ),
22 70
       Div(
23 71
         id = u"status_area",
24 72
       ),
25 73
       Div(
26 74
         Div(
27 75
           Br(),
28
-          Toolbar(),
76
+          Toolbar( hide_toolbar = not notebook.read_write ),
29 77
           id = u"toolbar_area",
30 78
         ),
31
-        Div(
32
-          Link_area( notebook_id ),
33
-          id = u"link_area",
34
-        ),
79
+        Link_area( notebooks, notebook, parent_id ),
35 80
         Div(
36 81
           Div(
37 82
             Div(
38
-              Div(
39
-                id = u"user_area",
40
-              ),
83
+              User_area( user, login_url, logout_url ),
41 84
               Div(
42 85
                 Search_form(),
43 86
                 id = u"search_area",
@@ -52,23 +95,34 @@ class Main_page( Page ):
52 95
             id = u"top_area",
53 96
           ),
54 97
           Div(
98
+            Strong( notebook.name ),
99
+            parent_id and Span(
100
+              u" | ",
101
+              A( u"empty trash", href = u"/notebooks/%s" % notebook.object_id, id = u"empty_trash_link" ),
102
+              u" | ",
103
+              A( u"return to notebook", href = u"/notebooks/%s" % parent_id ),
104
+            ) or None,
55 105
             id = u"notebook_header_area",
56
-            class_ = u"current_notebook_name",
106
+            class_ = ( notebook.name == u"trash" ) and u"current_trash_notebook_name" or u"current_notebook_name",
57 107
           ),
58 108
           Div(
59 109
             Div(
60 110
               Div(
61 111
                 id = u"notes",
62 112
               ),
113
+              static_notes,
114
+              # Sort of simulate the <noscript> tag by hiding the static version of the notes.
115
+              # This code won't be executed if JavaScript is disabled. I'm not actually using
116
+              # <noscript> because I want to be able to programmatically read the hidden static
117
+              # notes when JavaScript is enabled.
118
+              Script(
119
+                u"document.getElementById( 'static_notes' ).style.display = 'none';",
120
+                type = u"text/javascript",
121
+              ),
63 122
               id = u"notebook_background",
64 123
             ),
65 124
             id = u"notebook_border",
66
-            class_ = u"current_notebook_name",
67
-          ),
68
-          Noscript(
69
-            Div( file( u"static/html/about.html" ).read() ),
70
-            Div( file( u"static/html/features.html" ).read().replace( u"href=", u"disabled=" ) ),
71
-            Div( file( u"static/html/no javascript.html" ).read() ),
125
+            class_ = ( notebook.name == u"trash" ) and u"current_trash_notebook_name" or u"current_notebook_name",
72 126
           ),
73 127
           id = u"center_area",
74 128
         ),

+ 2
- 2
view/Toolbar.py View File

@@ -2,7 +2,7 @@ from Tags import P, Div, A, Input, Span, Br
2 2
 
3 3
 
4 4
 class Toolbar( Div ):
5
-  def __init__( self ):
5
+  def __init__( self, hide_toolbar = False ):
6 6
     Div.__init__(
7 7
       self,
8 8
       Div(
@@ -47,5 +47,5 @@ class Toolbar( Div ):
47 47
         class_ = u"button_wrapper",
48 48
       ),
49 49
       id = u"toolbar",
50
-      class_ = u"undisplayed", # start out as hidden, and then shown in the browser if the current notebook is read-write
50
+      class_ = hide_toolbar and u"undisplayed" or None,
51 51
     )

+ 24
- 0
view/User_area.py View File

@@ -0,0 +1,24 @@
1
+from Tags import Div, H4, A
2
+
3
+
4
+class User_area( Div ):
5
+  def __init__( self, user, login_url, logout_url ):
6
+    Div.__init__(
7
+      self,
8
+      ( login_url and user.username == u"anonymous" ) and Div(
9
+        A(
10
+          u"login",
11
+          href = login_url,
12
+          id = u"login_link",
13
+        ),
14
+      ) or Div(
15
+        u"logged in as %s" % ( user.username or u"a guest" ),
16
+        " | ",
17
+        A(
18
+          u"logout",
19
+          href = logout_url,
20
+          id = u"logout_link",
21
+        ),
22
+      ),
23
+      id = u"user_area",
24
+    )

Loading…
Cancel
Save