diff --git a/NEWS b/NEWS index 3a6485d..c2fa3e8 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,8 @@ * Feature to preview a notebook as a viewer would see it. * Note revisions list now include username of the user who made that revision. + * If you go to luminotes.com when you're logged in, you'll be automatically + redirected to your first notebook. * Fixed bug where passwords with special characters broke password hashing. * Fixed bug that prevented you from opening a note with a title that looked like an external URL. diff --git a/config/Common.py b/config/Common.py index 7113906..324a761 100644 --- a/config/Common.py +++ b/config/Common.py @@ -28,22 +28,32 @@ settings = { "name": "free", "storage_quota_bytes": 30 * MEGABYTE, "notebook_collaboration": False, + "fee": None, }, { "name": "basic", "storage_quota_bytes": 250 * MEGABYTE, "notebook_collaboration": True, + "fee": 5, + "button": + """ + """, }, { "name": "standard", "storage_quota_bytes": 500 * MEGABYTE, "notebook_collaboration": True, + "fee": 9, + "button": + """ + """, }, - { - "name": "premium", - "storage_quota_bytes": 2000 * MEGABYTE, - "notebook_collaboration": True, - }, +# { +# "name": "premium", +# "storage_quota_bytes": 2000 * MEGABYTE, +# "notebook_collaboration": True, +# "fee": 19, +# }, ], }, } diff --git a/controller/Root.py b/controller/Root.py index c0c4e30..ae691ae 100644 --- a/controller/Root.py +++ b/controller/Root.py @@ -2,7 +2,7 @@ import cherrypy from Expose import expose from Expire import strongly_expire -from Validate import validate, Valid_int +from Validate import validate, Valid_int, Valid_string from Notebooks import Notebooks from Users import Users, grab_user_id from Database import Valid_id @@ -11,6 +11,7 @@ from model.Notebook import Notebook from model.User import User from view.Main_page import Main_page from view.Notebook_rss import Notebook_rss +from view.Upgrade_note import Upgrade_note from view.Json import Json from view.Error_page import Error_page from view.Not_found_page import Not_found_page @@ -47,9 +48,10 @@ class Root( object ): @validate( note_title = unicode, invite_id = Valid_id( none_okay = True ), + after_login = Valid_string( min = 0, max = 100 ), user_id = Valid_id( none_okay = True ), ) - def default( self, note_title, invite_id = None, user_id = None ): + def default( self, note_title, invite_id = None, after_login = None, user_id = None ): """ Convenience method for accessing a note in the main notebook by name rather than by note id. @@ -57,6 +59,8 @@ class Root( object ): @param note_title: title of the note to return @type invite_id: unicode @param invite_id: id of the invite used to get to this note (optional) + @type after_login: unicode + @param after_login: URL to redirect to after login (optional, must start with "/") @rtype: unicode @return: rendered HTML page """ @@ -85,6 +89,8 @@ class Root( object ): result.update( self.__notebooks.contents( main_notebook.object_id, user_id = user_id, note_id = note.object_id ) ) if invite_id: result[ "invite_id" ] = invite_id + if after_login and after_login.startswith( u"/" ): + result[ "after_login" ] = after_login return result @@ -144,7 +150,7 @@ class Root( object ): if user: first_notebook = self.__database.select_one( Notebook, user.sql_load_notebooks( parents_only = True, undeleted_only = True ) ) if first_notebook: - return dict( redirect = "%s/notebooks/%s" % ( https_url, first_notebook.object_id ) ) + return dict( redirect = u"%s/notebooks/%s" % ( https_url, first_notebook.object_id ) ) # if the user is logged in and not using https, then redirect to the https version of the page (if available) if https_url and cherrypy.request.remote_addr != https_proxy_ip: @@ -231,6 +237,40 @@ class Root( object ): return result + @expose( view = Main_page ) + @grab_user_id + @validate( + user_id = Valid_id( none_okay = True ), + ) + def upgrade( self, user_id = None ): + """ + Provide the information necessary to display the Luminotes upgrade page. + """ + anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ) ) + if anonymous: + main_notebook = self.__database.select_one( Notebook, anonymous.sql_load_notebooks( undeleted_only = True ) ) + else: + main_notebook = None + + https_url = self.__settings[ u"global" ].get( u"luminotes.https_url" ) + result = self.__users.current( user_id ) + result[ "notebook" ] = main_notebook + result[ "startup_notes" ] = self.__database.select_many( Note, main_notebook.sql_load_startup_notes() ) + result[ "total_notes_count" ] = self.__database.select_one( Note, main_notebook.sql_count_notes() ) + result[ "note_read_write" ] = False + result[ "notes" ] = [ Note.create( + object_id = u"upgrade", + contents = unicode( Upgrade_note( + self.__settings[ u"global" ].get( u"luminotes.rate_plans", [] ), + https_url, + user_id, + ) ), + notebook_id = main_notebook.object_id, + ) ] + result[ "invites" ] = [] + + return result + # TODO: move this method to controller.Notebooks, and maybe give it a more sensible name @expose( view = Json ) def next_id( self ): diff --git a/controller/Users.py b/controller/Users.py index 6cfacdf..699ee0d 100644 --- a/controller/Users.py +++ b/controller/Users.py @@ -331,8 +331,9 @@ class Users( object ): password = Valid_string( min = 1, max = 30 ), login_button = unicode, invite_id = Valid_id( none_okay = True ), + after_login = Valid_string( min = 0, max = 100 ), ) - def login( self, username, password, login_button, invite_id = None ): + def login( self, username, password, login_button, invite_id = None, after_login = None ): """ Attempt to authenticate the user. If successful, associate the given user with the current session. @@ -343,6 +344,8 @@ class Users( object ): @param password: the user's password @type invite_id: unicode @param invite_id: id of invite to redeem upon login (optional) + @type after_login: unicode + @param after_login: URL to redirect to after login (optional, must start with "/") @rtype: json dict @return: { 'redirect': url, 'authenticated': userdict } @raise Authentication_error: invalid username or password @@ -363,6 +366,9 @@ class Users( object ): self.convert_invite_to_access( invite, user.object_id ) redirect = u"/notebooks/%s" % invite.notebook_id + # if there's an after_login URL, redirect to it + elif after_login and after_login.startswith( "/" ): + redirect = after_login # otherwise, just redirect to the user's first notebook (if any) elif first_notebook: redirect = u"/notebooks/%s" % first_notebook.object_id diff --git a/controller/test/Test_controller.py b/controller/test/Test_controller.py index 45079cc..ac1701f 100644 --- a/controller/test/Test_controller.py +++ b/controller/test/Test_controller.py @@ -314,10 +314,16 @@ class Test_controller( object ): { u"name": u"super", u"storage_quota_bytes": 1337, + u"notebook_collaboration": True, + u"fee": 1.99, + u"button": u"[subscribe here user %s!] button", }, { u"name": "extra super", u"storage_quota_bytes": 31337, + u"notebook_collaboration": True, + u"fee": 199.99, + u"button": u"[or here user %s!] button", }, ], }, diff --git a/controller/test/Test_root.py b/controller/test/Test_root.py index e67a977..a077e74 100644 --- a/controller/test/Test_root.py +++ b/controller/test/Test_root.py @@ -117,6 +117,36 @@ class Test_root( Test_controller ): assert result[ u"invite_id" ] == u"whee" assert result[ u"user" ].object_id == self.anonymous.object_id + def test_default_with_after_login( self ): + after_login = "/foo/bar" + + result = self.http_get( + "/my_note?after_login=%s" % after_login, + ) + + assert result + assert result[ u"notes" ] + assert len( result[ u"notes" ] ) == 1 + assert result[ u"notes" ][ 0 ].object_id == self.anon_note.object_id + assert result[ u"notebook" ].object_id == self.anon_notebook.object_id + assert result[ u"after_login" ] == after_login + assert result[ u"user" ].object_id == self.anonymous.object_id + + def test_default_with_after_login_with_full_url( self ): + after_login = "http://example.com/foo/bar" + + result = self.http_get( + "/my_note?after_login=%s" % after_login, + ) + + assert result + assert result[ u"notes" ] + assert len( result[ u"notes" ] ) == 1 + assert result[ u"notes" ][ 0 ].object_id == self.anon_note.object_id + assert result[ u"notebook" ].object_id == self.anon_notebook.object_id + assert result.get( u"after_login" ) is None + assert result[ u"user" ].object_id == self.anonymous.object_id + def test_default_after_login( self ): self.login() @@ -205,6 +235,72 @@ class Test_root( Test_controller ): assert u"error" not in result assert result[ u"notebook" ].object_id == self.privacy_notebook.object_id + def test_upgrade( self ): + result = self.http_get( "/upgrade" ) + + assert result[ u"user" ].username == u"anonymous" + assert len( result[ u"notebooks" ] ) == 4 + 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"notebooks" ][ 0 ].owner == False + + 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" ] ) == 0 + assert result[ u"note_read_write" ] is False + + assert result[ u"notes" ] + assert len( result[ u"notes" ] ) == 1 + assert result[ u"notes" ][ 0 ].title == u"upgrade your wiki" + assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id + + contents = result[ u"notes" ][ 0 ].contents + assert u"upgrade" in contents + assert u"Super" in contents + assert u"Extra super" in contents + + # since the user is not logged in, no subscription buttons should be shown + assert u"button" not in contents + + def test_upgrade_after_login( self ): + self.login() + + result = self.http_get( "/upgrade", session_id = self.session_id ) + + assert result[ u"user" ].username == self.username + assert len( result[ u"notebooks" ] ) == 5 + assert result[ u"notebooks" ][ 0 ].object_id == self.notebook.object_id + assert result[ u"notebooks" ][ 0 ].name == self.notebook.name + assert result[ u"notebooks" ][ 0 ].read_write == False + assert result[ u"notebooks" ][ 0 ].owner == False + + 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" ] ) == 0 + assert result[ u"note_read_write" ] is False + + assert result[ u"notes" ] + assert len( result[ u"notes" ] ) == 1 + assert result[ u"notes" ][ 0 ].title == u"upgrade your wiki" + assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id + + contents = result[ u"notes" ][ 0 ].contents + assert u"upgrade" in contents + assert u"Super" in contents + assert u"Extra super" in contents + + # since the user is logged in, subscription buttons should be shown + assert u"button" in contents + def test_next_id( self ): result = self.http_get( "/next_id" ) diff --git a/controller/test/Test_users.py b/controller/test/Test_users.py index 38dc020..1fdf440 100644 --- a/controller/test/Test_users.py +++ b/controller/test/Test_users.py @@ -457,6 +457,30 @@ class Test_users( Test_controller ): assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].object_id ) assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].trash_id ) + def test_current_after_login_with_after_login( self ): + after_login = u"/foo/bar" + + result = self.http_post( "/users/login", dict( + username = self.username2, + password = self.password2, + after_login = after_login, + login_button = u"login", + ) ) + + assert result[ u"redirect" ] == after_login + + def test_current_after_login_with_after_login_with_full_url( self ): + after_login = u"http://this_url/does/not/start/with/a/slash" + + result = self.http_post( "/users/login", dict( + username = self.username2, + password = self.password2, + after_login = after_login, + login_button = u"login", + ) ) + + assert result[ u"redirect" ] == u"/" + def test_update_storage( self ): previous_revision = self.user.revision diff --git a/static/css/note.css b/static/css/note.css index 33783ec..2657085 100644 --- a/static/css/note.css +++ b/static/css/note.css @@ -138,9 +138,30 @@ ol li { padding-right: 1em; } +#upgrade_table_area { + text-align: center; +} + +#upgrade_login_text { + font-weight: bold; + text-align: center; +} + #upgrade_table { border-collapse: collapse; border: 1px solid #999999; + margin-left: auto; + margin-right: auto; +} + +#upgrade_table th { + padding: 0.5em; +} + +#upgrade_table td { + text-align: center; + background-color: #fafafa; + padding: 0.5em; } #upgrade_table .plan_name { @@ -155,19 +176,41 @@ ol li { background-color: #fafafa; } -#upgrade_table .price_text { +#upgrade_table_small { + border-collapse: collapse; + border: 1px solid #999999; + margin-left: auto; + margin-right: auto; +} + +#upgrade_table_small th { + padding: 0.5em; +} + +#upgrade_table_small td { + text-align: center; + background-color: #fafafa; + padding: 0.5em; +} + +#upgrade_table_small .plan_name { + width: 33%; + text-align: center; + background-color: #d0e0f0; +} + +.price_text { color: #ff6600; } -#upgrade_table .month_text { +.month_text { padding-top: 0.5em; font-size: 75%; } -#upgrade_table td { - text-align: center; - background-color: #fafafa; - padding: 0.5em; +.subscribe_form { + margin-top: 0.5em; + margin-bottom: 0; } .thumbnail_left { diff --git a/static/html/faq.html b/static/html/faq.html index db851dd..1aa8ab0 100644 --- a/static/html/faq.html +++ b/static/html/faq.html @@ -16,10 +16,11 @@ whenever you want.
Does this cost me anything?Nope, use of your personal Luminotes wiki is completely free. Soon you will -also be able to upgrade your Luminotes -account to get notebook sharing features and additional storage space. But the -features you're using now will always remain free.
+Use of your personal Luminotes wiki is completely free. You also have the +option of upgrading your Luminotes +account to get notebook sharing features and additional storage space for a +reasonable subscription fee. But the features you're using now will always +remain free.
What does Luminotes run on?-In a few short weeks, you'll be able to upgrade your Luminotes account to get -notebook sharing features and additional storage space. Here are some of the -features you can look forward to. -
- -- -Most of the time, you want to keep your personal wiki all to yourself. But -sometimes you simply need to share your work with friends and colleagues. When -you upgrade your Luminotes account, you'll be able to invite specific people -to collaborate on your wiki simply by entering their email addresses. You can -even give them full editing capbilities, so several people can contribute to -your wiki notebook. -
- -- -With an upgraded Luminotes wiki, you'll decide exactly how much access to give -people. Collaborators can make changes to your notebook, while viewers can -only read your wiki. And owners have the same complete access to your notebook -that you do. When you're done collaborating, a single click revokes a user's -notebook access. -
- --Your wiki access control works on a per-notebook basis, so you can easily -share one notebook with your friends while keeping your other notebooks -completely private. -
- --An upgraded Luminotes account gets you more than just notebook sharing -features. You'll also be treated to way more room for your personal wiki. That -means you'll have more space for your notes and ideas, and you won't have to -worry about running out of room anytime soon. -
- -