From 18982dc129daf9631a7ce9e5718c3fc2b1f212d0 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Thu, 8 May 2008 03:05:35 +0000 Subject: [PATCH] Implemented basic user account settings. Now you can change your email address. --- NEWS | 6 +++ controller/Users.py | 43 +++++++++++++++++++++ controller/test/Test_users.py | 73 +++++++++++++++++++++++++++++++++++ model/User.py | 6 ++- model/test/Test_user.py | 16 ++++++++ static/js/Editor.js | 11 ++++++ static/js/Wiki.js | 71 +++++++++++++++++++++++++++++++++- view/Header.py | 9 +++++ view/Main_page.py | 1 + 9 files changed, 233 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index 66bd57c..50f09ab 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,9 @@ +1.3.17: May 7, 2008 + * Implemented basic user account settings. Now you can change your email + address. + * Fixed a bug where if you load a particular note in its own window, and + that note is a startup note, it shows up in the note tree twice. + 1.3.16: May 6, 2008 * Fixed a bug where an invite sent for a notebook with an accented unicode name would cause a UnicodeEncodeError upon sending the invite email. Now diff --git a/controller/Users.py b/controller/Users.py index ba47cf1..a52911d 100644 --- a/controller/Users.py +++ b/controller/Users.py @@ -1208,3 +1208,46 @@ class Users( object ): def rate_plan( self, plan_index ): return self.__rate_plans[ plan_index ] + + @expose( view = Json ) + @end_transaction + @grab_user_id + @validate( + email_address = ( Valid_string( min = 0, max = 60 ) ), + settings_button = unicode, + user_id = Valid_id( none_okay = True ), + ) + def update_settings( self, email_address, settings_button, user_id ): + """ + Update the settings for a particular user. + + @type email_address: unicode + @param email_address: new email address + @type settings_button: unicode + @param settings_button: ignored + @type user_id: unicode + @param user_id: id of current logged-in user (if any), determined by @grab_user_id + @rtype: json dict + @return: { "email_address": new_email_address } + @raise Validation_error: one of the arguments is invalid + @raise Access_error: the given user id is unknown + """ + if len( email_address ) > 0: + try: + email_address = valid_email_address( email_address ) + except ValueError: + raise Validation_error( "email_address", email_address, valid_email_address ) + else: + email_address = None + + user = self.__database.load( User, user_id ) + if not user: + raise Access_error() + + if email_address != user.email_address: + user.email_address = email_address + self.__database.save( user ) + + return dict( + email_address = email_address, + ) diff --git a/controller/test/Test_users.py b/controller/test/Test_users.py index 7a08f40..c2a89f3 100644 --- a/controller/test/Test_users.py +++ b/controller/test/Test_users.py @@ -3630,6 +3630,79 @@ class Test_users( Test_controller ): assert rate_plan assert rate_plan == self.settings[ u"global" ][ u"luminotes.rate_plans" ][ plan_index ] + def test_update_settings( self ): + self.login() + previous_revision = self.user.revision + + result = self.http_post( "/users/update_settings", dict( + email_address = self.new_email_address, + settings_button = u"save settings", + ), session_id = self.session_id ) + + assert result[ u"email_address" ] == self.new_email_address + + user = self.database.load( User, self.user.object_id ) + assert user.email_address == self.new_email_address + assert user.revision > previous_revision + + def test_update_settings_without_login( self ): + original_revision = self.user.revision + + result = self.http_post( "/users/update_settings", dict( + email_address = self.new_email_address, + settings_button = u"save settings", + ) ) + + assert u"access" in result[ u"error" ] + + user = self.database.load( User, self.user.object_id ) + assert user.email_address == self.email_address + assert user.revision == original_revision + + def test_update_settings_with_same_email_address( self ): + self.login() + original_revision = self.user.revision + + result = self.http_post( "/users/update_settings", dict( + email_address = self.email_address, + settings_button = u"save settings", + ), session_id = self.session_id ) + + assert result[ u"email_address" ] == self.email_address + + user = self.database.load( User, self.user.object_id ) + assert user.email_address == self.email_address + assert user.revision == original_revision + + def test_update_settings_with_invalid_email_address( self ): + original_revision = self.user.revision + + result = self.http_post( "/users/update_settings", dict( + email_address = u"foo@bar@com", + settings_button = u"save settings", + ) ) + + assert u"invalid" in result[ u"error" ] + + user = self.database.load( User, self.user.object_id ) + assert user.email_address == self.email_address + assert user.revision == original_revision + + def test_update_settings_with_blank_email_address( self ): + self.login() + previous_revision = self.user.revision + + result = self.http_post( "/users/update_settings", dict( + email_address = u"", + settings_button = u"save settings", + ), session_id = self.session_id ) + + assert result[ u"email_address" ] == None + + user = self.database.load( User, self.user.object_id ) + assert user.email_address == None + assert user.revision > previous_revision + def login( self ): result = self.http_post( "/users/login", dict( username = self.username, diff --git a/model/User.py b/model/User.py index 1d5a9b5..14e11a1 100644 --- a/model/User.py +++ b/model/User.py @@ -281,6 +281,10 @@ class User( Persistent ): return d + def __set_email_address( self, email_address ): + self.update_revision() + self.__email_address = email_address + def __set_password( self, password ): self.update_revision() self.__salt = User.__create_salt() @@ -295,7 +299,7 @@ class User( Persistent ): self.__rate_plan = rate_plan username = property( lambda self: self.__username ) - email_address = property( lambda self: self.__email_address ) + email_address = property( lambda self: self.__email_address, __set_email_address ) password = property( None, __set_password ) storage_bytes = property( lambda self: self.__storage_bytes, __set_storage_bytes ) rate_plan = property( lambda self: self.__rate_plan, __set_rate_plan ) diff --git a/model/test/Test_user.py b/model/test/Test_user.py index c2b0f58..24eb8d5 100644 --- a/model/test/Test_user.py +++ b/model/test/Test_user.py @@ -27,6 +27,22 @@ class Test_user( object ): def test_check_incorrect_password( self ): assert self.user.check_password( u"wrong" ) == False + def test_set_email_address( self ): + previous_revision = self.user.revision + email_address = u"alice@example.com" + self.user.email_address = email_address + + assert self.user.email_address == email_address + assert self.user.revision > previous_revision + + def test_set_none_email_address( self ): + previous_revision = self.user.revision + email_address = None + self.user.email_address = email_address + + assert self.user.email_address == email_address + assert self.user.revision > previous_revision + def test_set_password( self ): previous_revision = self.user.revision new_password = u"newpass" diff --git a/static/js/Editor.js b/static/js/Editor.js index e2e4f69..5caa1a7 100644 --- a/static/js/Editor.js +++ b/static/js/Editor.js @@ -213,6 +213,17 @@ Editor.prototype.finish_init = function () { connect_button( revoke_button, invite_id ); } } + + var settings_button = getElement( "settings_button" ); + if ( settings_button ) { + var settings_form = getElement( "settings_form" ); + connect( settings_button, "onclick", function ( event ) { + signal( self, "submit_form", "/users/update_settings", settings_form, function ( result ) { + signal( self, "settings_updated", result ); + } ); + event.stop(); + } ); + } } ); // browsers such as Firefox, but not Opera diff --git a/static/js/Wiki.js b/static/js/Wiki.js index 7b7b3a2..4b5cf2c 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -17,6 +17,7 @@ function Wiki( invoker ) { this.invite_id = getElement( "invite_id" ).value; this.after_login = getElement( "after_login" ).value; this.signup_plan = getElement( "signup_plan" ).value; + this.email_address = getElement( "email_address" ).value || ""; this.font_size = null; var total_notes_count_node = getElement( "total_notes_count" ); @@ -361,6 +362,14 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri } ); } + var settings_link = getElement( "settings_link" ); + if ( settings_link ) { + connect( settings_link, "onclick", function ( event ) { + self.load_editor( "account settings", "null", null, null, null, getElement( "notes_top" ) ); + event.stop(); + } ); + } + var declutter_link = getElement( "declutter_link" ); if ( declutter_link ) { connect( declutter_link, "onclick", function ( event ) { @@ -468,6 +477,16 @@ Wiki.prototype.load_editor = function ( note_title, note_id, revision, previous_ this.share_notebook(); return; } + if ( note_title == "account settings" ) { + var editor = this.open_editors[ note_title ]; + if ( editor ) { + editor.highlight(); + return; + } + + this.display_settings(); + return; + } // but if the note corresponding to the link's title is already open, highlight it and bail if ( !revision ) { @@ -512,7 +531,7 @@ Wiki.prototype.resolve_link = function ( note_title, link, callback ) { if ( link && link.target ) link.removeAttribute( "target" ); - if ( note_title == "search results" || note_title == "share this notebook" ) { + if ( note_title == "search results" || note_title == "share this notebook" || note_title == "account settings" ) { link.href = "/notebooks/" + this.notebook_id + "?" + queryString( [ "title", "note_id" ], [ note_title, "null" ] @@ -520,8 +539,10 @@ Wiki.prototype.resolve_link = function ( note_title, link, callback ) { if ( callback ) { if ( note_title == "search results" ) callback( "current search results" ); - else + else if ( note_title == "share this notebook" ) callback( "share this notebook with others" ); + else + callback( "account settings" ); } return; } @@ -694,6 +715,11 @@ Wiki.prototype.create_editor = function ( id, note_text, deleted_from_id, revisi connect( editor, "load_editor", this, "load_editor" ); connect( editor, "hide_clicked", function ( event ) { self.hide_editor( event, editor ) } ); connect( editor, "invites_updated", function ( invites ) { self.invites = invites; self.share_notebook(); } ); + connect( editor, "settings_updated", function ( result ) { + self.email_address = result.email_address || ""; + self.display_message( "Your account settings have been updated." ); + } ); + connect( editor, "submit_form", function ( url, form, callback ) { var args = {} if ( url == "/users/signup" ) { @@ -1556,6 +1582,41 @@ Wiki.prototype.display_invites = function ( invite_area ) { replaceChildNodes( invite_area, div ); } +Wiki.prototype.display_settings = function () { + this.clear_pulldowns(); + + var settings_frame = getElement( "note_settings" ); + if ( settings_frame ) { + settings_frame.editor.highlight(); + return; + } + + var div = createDOM( "div", {}, + createDOM( "form", { "id": "settings_form" }, + createDOM( "p", {}, + createDOM( "b", {}, "email address" ), + createDOM( "br", {} ), + createDOM( "input", + { "type": "text", "name": "email_address", "id": "email_address", "class": "text_field", + "size": "30", "maxlength": "60", "value": this.email_address || "" } + ) + ), + createDOM( "p", {}, + createDOM( "input", + { "type": "submit", "name": "settings_button", "id": "settings_button", "class": "button", "value": "save settings" } + ) + ), + createDOM( "p", {}, + "Your email address will ", + createDOM( "a", { "href": "/privacy", "target": "_new" }, "never be shared" ), + ". It will only be used for password resets, contacting you about account problems, and the from address in any invite emails you send." + ) + ) + ); + + this.create_editor( "settings", "

account settings

" + div.innerHTML, undefined, undefined, undefined, false, true, true, getElement( "notes_top" ) ); +} + Wiki.prototype.declutter_clicked = function () { var header = getElement( "header" ); if ( header ) @@ -2269,6 +2330,12 @@ function Link_pulldown( wiki, notebook_id, invoker, editor, link ) { return; } + if ( title == "account settings" ) { + this.title_field.value = title; + this.display_summary( title, "account settings" ); + return; + } + this.invoker.invoke( "/notebooks/load_note_by_title", "GET", { "notebook_id": this.notebook_id, diff --git a/view/Header.py b/view/Header.py index a1884a6..7c78d92 100644 --- a/view/Header.py +++ b/view/Header.py @@ -32,6 +32,15 @@ class Header( Div ): ), u" | ", ) or None, + user.username and Span( + A( + u"settings", + href = u"#", + title = u"Update your account settings.", + id = u"settings_link", + ), + " | ", + ) or None, user.username and Span( A( u"upgrade", diff --git a/view/Main_page.py b/view/Main_page.py index d7ae718..8704c51 100644 --- a/view/Main_page.py +++ b/view/Main_page.py @@ -141,6 +141,7 @@ class Main_page( Page ): Input( type = u"hidden", name = u"invite_id", id = u"invite_id", value = invite_id ), Input( type = u"hidden", name = u"after_login", id = u"after_login", value = after_login ), Input( type = u"hidden", name = u"signup_plan", id = u"signup_plan", value = signup_plan ), + Input( type = u"hidden", name = u"email_address", id = u"email_address", value = user.email_address ), Div( id = u"status_area", ),