From 4d736d48210671674ca0916952221823607aca9e Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Thu, 20 Sep 2007 20:36:19 +0000 Subject: [PATCH] Lots of work on user storage quotas: * Each model.User now has a current storage bytes and a rate plan. * model.User.to_dict() updated accordingly. * Minor Scheduler.add() change to bail of the given thread is None. * controller.Users.current() returns current user's rate plan details. * controller.Users.update_storage() now takes an optional callback. * Various methods in controller.Notebooks responsible for calling controller.Users.update_storage(). * Added rate plan details to config/Common.py. * Added quota utilization colors to style.css. * Implemented quota utilization calculation and display in Wiki.js. Still to-do: Return updated storage bytes where appropriate in controller.Notebook and update the client accordingly. --- config/Common.py | 17 +++++++++++++ controller/Notebooks.py | 41 +++++++++++++++++++++++++----- controller/Root.py | 3 ++- controller/Scheduler.py | 3 +++ controller/Users.py | 32 ++++++++++++++++++----- controller/test/Test_controller.py | 10 ++++++++ controller/test/Test_notebooks.py | 41 ++++++++++++++++++++++++++++++ controller/test/Test_users.py | 33 +++++++++++++++++++++++- model/User.py | 12 ++++++++- model/test/Test_user.py | 16 ++++++++++++ static/css/style.css | 21 +++++++++++++++ static/js/Wiki.js | 28 ++++++++++++++++++++ view/Link_area.py | 3 +++ 13 files changed, 243 insertions(+), 17 deletions(-) diff --git a/config/Common.py b/config/Common.py index bffe914..575d68f 100644 --- a/config/Common.py +++ b/config/Common.py @@ -1,6 +1,9 @@ import cherrypy +MEGABYTE = 1024 * 1024 + + settings = { "global": { "server.socket_port": 8081, @@ -20,5 +23,19 @@ settings = { "luminotes.http_proxy_ip": "127.0.0.1", "luminotes.https_proxy_ip": "127.0.0.2", "luminotes.support_email": "support@luminotes.com", + "luminotes.rate_plans": [ + { + "name": "basic", + "storage_quota_bytes": 30 * MEGABYTE, + }, + { + "name": "standard", + "storage_quota_bytes": 100 * MEGABYTE, + }, + { + "name": "professional", + "storage_quota_bytes": 300 * MEGABYTE, + }, + ], }, } diff --git a/controller/Notebooks.py b/controller/Notebooks.py index 26c61e8..c3982c6 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -33,7 +33,7 @@ class Notebooks( object ): """ Controller for dealing with notebooks and their notes, corresponding to the "/notebooks" URL. """ - def __init__( self, scheduler, database ): + def __init__( self, scheduler, database, users ): """ Create a new Notebooks object. @@ -41,11 +41,14 @@ class Notebooks( object ): @param scheduler: scheduler to use for asynchronous calls @type database: controller.Database @param database: database that notebooks are stored in + @type users: controller.Users + @param users: controller for all users, used here for updating storage utilization @rtype: Notebooks @return: newly constructed Notebooks """ self.__scheduler = scheduler self.__database = database + self.__users = users @expose( view = Main_page ) @validate( @@ -363,7 +366,11 @@ class Notebooks( object ): startup_changed = notebook.remove_startup_note( note ) if new_revision or startup_changed: - self.__database.save( notebook ) + self.__database.save( notebook, self.__scheduler.thread ) + yield Scheduler.SLEEP + self.__users.update_storage( user_id, self.__scheduler.thread ) + user = ( yield Scheduler.SLEEP ) + self.__database.save( user ) yield dict( new_revision = new_revision, @@ -411,7 +418,11 @@ class Notebooks( object ): if note: notebook.add_startup_note( note ) - self.__database.save( notebook ) + self.__database.save( notebook, self.__scheduler.thread ) + yield Scheduler.SLEEP + self.__users.update_storage( user_id, self.__scheduler.thread ) + user = ( yield Scheduler.SLEEP ) + self.__database.save( user ) yield dict() @@ -456,7 +467,11 @@ class Notebooks( object ): if note: notebook.remove_startup_note( note ) - self.__database.save( notebook ) + self.__database.save( notebook, self.__scheduler.thread ) + yield Scheduler.SLEEP + self.__users.update_storage( user_id, self.__scheduler.thread ) + user = ( yield Scheduler.SLEEP ) + self.__database.save( user ) yield dict() @@ -508,7 +523,11 @@ class Notebooks( object ): notebook.trash.add_note( note ) notebook.trash.add_startup_note( note ) - self.__database.save( notebook ) + self.__database.save( notebook, self.__scheduler.thread ) + yield Scheduler.SLEEP + self.__users.update_storage( user_id, self.__scheduler.thread ) + user = ( yield Scheduler.SLEEP ) + self.__database.save( user ) yield dict() @@ -567,7 +586,11 @@ class Notebooks( object ): notebook.add_note( note ) notebook.add_startup_note( note ) - self.__database.save( notebook ) + self.__database.save( notebook, self.__scheduler.thread ) + yield Scheduler.SLEEP + self.__users.update_storage( user_id, self.__scheduler.thread ) + user = ( yield Scheduler.SLEEP ) + self.__database.save( user ) yield dict() @@ -613,7 +636,11 @@ class Notebooks( object ): notebook.trash.add_note( note ) notebook.trash.add_startup_note( note ) - self.__database.save( notebook ) + self.__database.save( notebook, self.__scheduler.thread ) + yield Scheduler.SLEEP + self.__users.update_storage( user_id, self.__scheduler.thread ) + user = ( yield Scheduler.SLEEP ) + self.__database.save( user ) yield dict() diff --git a/controller/Root.py b/controller/Root.py index 95df45f..031f80c 100644 --- a/controller/Root.py +++ b/controller/Root.py @@ -33,13 +33,14 @@ class Root( object ): self.__scheduler = scheduler self.__database = database self.__settings = settings - self.__notebooks = Notebooks( scheduler, database ) self.__users = Users( scheduler, database, settings[ u"global" ].get( u"luminotes.http_url", u"" ), settings[ u"global" ].get( u"luminotes.https_url", u"" ), + settings[ u"global" ].get( u"luminotes.rate_plans", [] ), ) + self.__notebooks = Notebooks( scheduler, database, self.__users ) @expose( view = Main_page ) def index( self ): diff --git a/controller/Scheduler.py b/controller/Scheduler.py index 637ca62..16df6a8 100644 --- a/controller/Scheduler.py +++ b/controller/Scheduler.py @@ -125,6 +125,9 @@ class Scheduler( object ): @type args: tuple @param args: arguments to send() to the given thread when it is executed """ + if thread is None: + return + self.__idle.release() if thread in self.__sleeping: diff --git a/controller/Users.py b/controller/Users.py index 5dceff2..fa07782 100644 --- a/controller/Users.py +++ b/controller/Users.py @@ -106,7 +106,7 @@ class Users( object ): """ Controller for dealing with users, corresponding to the "/users" URL. """ - def __init__( self, scheduler, database, http_url, https_url ): + def __init__( self, scheduler, database, http_url, https_url, rate_plans ): """ Create a new Users object. @@ -118,6 +118,8 @@ class Users( object ): @param http_url: base URL to use for non-SSL http requests, or an empty string @type https_url: unicode @param https_url: base URL to use for SSL http requests, or an empty string + @type rate_plans: [ { "name": unicode, "storage_quota_bytes": int } ] + @param rate_plans: list of configured rate plans @rtype: Users @return: newly constructed Users """ @@ -125,6 +127,7 @@ class Users( object ): self.__database = database self.__http_url = http_url self.__https_url = https_url + self.__rate_plans = rate_plans @expose( view = Json ) @update_auth @@ -275,7 +278,14 @@ class Users( object ): @type user_id: unicode @param user_id: id of current logged-in user (if any), determined by @grab_user_id @rtype: json dict - @return: { 'user': userdict or None, 'notebooks': notebooksdict, 'http_url': url } + @return: { + 'user': userdict or None, + 'notebooks': notebooksdict, + 'startup_notes': noteslist, + 'http_url': url, + 'login_url': url, + 'rate_plan': rateplandict, + } """ # if there's no logged-in user, default to the anonymous user self.__database.load( user_id or u"User anonymous", self.__scheduler.thread ) @@ -312,6 +322,7 @@ class Users( object ): startup_notes = include_startup_notes and len( notebooks ) > 0 and notebooks[ 0 ].startup_notes or [], http_url = self.__http_url, login_url = login_url, + rate_plan = ( user.rate_plan < len( self.__rate_plans ) ) and self.__rate_plans[ user.rate_plan ] or {}, ) def calculate_storage( self, user ): @@ -344,13 +355,20 @@ class Users( object ): return total_bytes @async - def update_storage( self, user ): + def update_storage( self, user_id, callback = None ): """ Calculate and record total storage utilization for the given user. - @type user: User - @param user: user for which to calculate storage utilization + @type user_id: unicode or NoneType + @param user_id: id of user for which to calculate storage utilization + @type callback: generator or NoneType + @param callback: generator to wakeup when the update is complete (optional) """ - user.storage_bytes = self.calculate_storage( user ) - yield None + self.__database.load( user_id, self.__scheduler.thread ) + user = ( yield Scheduler.SLEEP ) + + if user: + user.storage_bytes = self.calculate_storage( user ) + + yield callback, user scheduler = property( lambda self: self.__scheduler ) diff --git a/controller/test/Test_controller.py b/controller/test/Test_controller.py index b976ef3..483bcb8 100644 --- a/controller/test/Test_controller.py +++ b/controller/test/Test_controller.py @@ -19,6 +19,16 @@ class Test_controller( object ): u"luminotes.https_url" : u"https://luminotes.com", u"luminotes.http_proxy_ip" : u"127.0.0.1", u"luminotes.https_proxy_ip" : u"127.0.0.2", + "luminotes.rate_plans": [ + { + "name": "super", + "storage_quota_bytes": 1337, + }, + { + "name": "extra super", + "storage_quota_bytes": 31337, + }, + ], }, } diff --git a/controller/test/Test_notebooks.py b/controller/test/Test_notebooks.py index f07a37f..1ee0fda 100644 --- a/controller/test/Test_notebooks.py +++ b/controller/test/Test_notebooks.py @@ -66,12 +66,14 @@ class Test_notebooks( Test_controller ): result = self.http_get( "/notebooks/%s" % self.notebook.object_id ) assert result.get( u"notebook_id" ) == self.notebook.object_id + assert self.user.storage_bytes == 0 def test_default_with_note( self ): result = self.http_get( "/notebooks/%s?note_id=%s" % ( self.notebook.object_id, self.note.object_id ) ) assert result.get( u"notebook_id" ) == self.notebook.object_id assert result.get( u"note_id" ) == self.note.object_id + assert self.user.storage_bytes == 0 def test_default_with_note_and_revision( self ): result = self.http_get( "/notebooks/%s?note_id=%s&revision=%s" % ( @@ -83,6 +85,7 @@ class Test_notebooks( Test_controller ): assert result.get( u"notebook_id" ) == self.notebook.object_id assert result.get( u"note_id" ) == self.note.object_id assert result.get( u"revision" ) == unicode( self.note.revision ) + assert self.user.storage_bytes == 0 def test_default_with_parent( self ): parent_id = "foo" @@ -90,6 +93,7 @@ class Test_notebooks( Test_controller ): assert result.get( u"notebook_id" ) == self.notebook.object_id assert result.get( u"parent_id" ) == parent_id + assert self.user.storage_bytes == 0 def test_contents( self ): self.login() @@ -105,6 +109,7 @@ class Test_notebooks( Test_controller ): assert notebook.object_id == self.notebook.object_id assert len( startup_notes ) == 1 assert startup_notes[ 0 ] == self.note + assert self.user.storage_bytes == 0 def test_contents_with_note( self ): self.login() @@ -124,6 +129,7 @@ class Test_notebooks( Test_controller ): note = result[ "note" ] assert note.object_id == self.note.object_id + assert self.user.storage_bytes == 0 def test_contents_with_note_and_revision( self ): self.login() @@ -147,6 +153,7 @@ class Test_notebooks( Test_controller ): note = result[ "note" ] assert note.object_id == self.note.object_id + assert self.user.storage_bytes == 0 def test_contents_without_login( self ): result = self.http_get( @@ -155,6 +162,7 @@ class Test_notebooks( Test_controller ): ) assert result.get( "error" ) + assert self.user.storage_bytes == 0 def test_load_note( self ): self.login() @@ -169,6 +177,7 @@ class Test_notebooks( Test_controller ): assert note.object_id == self.note.object_id assert note.title == self.note.title assert note.contents == self.note.contents + assert self.user.storage_bytes == 0 def test_load_note_with_revision( self ): self.login() @@ -198,6 +207,7 @@ class Test_notebooks( Test_controller ): assert note.revision == previous_revision assert note.title == previous_title assert note.contents == previous_contents + assert self.user.storage_bytes == 0 def test_load_note_without_login( self ): result = self.http_post( "/notebooks/load_note/", dict( @@ -216,6 +226,7 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) + assert self.user.storage_bytes == 0 def test_load_unknown_note( self ): self.login() @@ -227,6 +238,7 @@ class Test_notebooks( Test_controller ): note = result[ "note" ] assert note == None + assert self.user.storage_bytes == 0 def test_load_note_by_title( self ): self.login() @@ -241,6 +253,7 @@ class Test_notebooks( Test_controller ): assert note.object_id == self.note.object_id assert note.title == self.note.title assert note.contents == self.note.contents + assert self.user.storage_bytes == 0 def test_load_note_by_title_without_login( self ): result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -249,6 +262,7 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) + assert self.user.storage_bytes == 0 def test_load_note_by_title_with_unknown_notebook( self ): self.login() @@ -259,6 +273,7 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) + assert self.user.storage_bytes == 0 def test_load_unknown_note_by_title( self ): self.login() @@ -270,6 +285,7 @@ class Test_notebooks( Test_controller ): note = result[ "note" ] assert note == None + assert self.user.storage_bytes == 0 def test_lookup_note_id( self ): self.login() @@ -280,6 +296,7 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "note_id" ) == self.note.object_id + assert self.user.storage_bytes == 0 def test_lookup_note_id_without_login( self ): result = self.http_post( "/notebooks/lookup_note_id/", dict( @@ -288,6 +305,7 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) + assert self.user.storage_bytes == 0 def test_lookup_note_id_with_unknown_notebook( self ): self.login() @@ -298,6 +316,7 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) + assert self.user.storage_bytes == 0 def test_lookup_unknown_note_id( self ): self.login() @@ -308,6 +327,7 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "note_id" ) == None + assert self.user.storage_bytes == 0 def test_save_note( self, startup = False ): self.login() @@ -325,6 +345,7 @@ class Test_notebooks( Test_controller ): assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision assert result[ "previous_revision" ] == previous_revision + assert self.user.storage_bytes > 0 # make sure the old title can no longer be loaded result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -367,6 +388,7 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) + assert self.user.storage_bytes == 0 def test_save_startup_note_without_login( self ): self.test_save_note_without_login( startup = True ) @@ -393,6 +415,7 @@ class Test_notebooks( Test_controller ): assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision assert result[ "previous_revision" ] == previous_revision + assert self.user.storage_bytes > 0 # make sure the old title can no longer be loaded result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -446,6 +469,7 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) # now attempt to save over that note again without changing the contents + previous_storage_bytes = self.user.storage_bytes previous_revision = self.note.revision result = self.http_post( "/notebooks/save_note/", dict( notebook_id = self.notebook.object_id, @@ -458,6 +482,7 @@ class Test_notebooks( Test_controller ): # assert that the note wasn't actually updated the second time assert result[ "new_revision" ] == None assert result[ "previous_revision" ] == previous_revision + assert self.user.storage_bytes == previous_storage_bytes result = self.http_post( "/notebooks/load_note_by_title/", dict( notebook_id = self.notebook.object_id, @@ -491,6 +516,7 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) # now attempt to save over that note again without changing the contents + previous_storage_bytes = self.user.storage_bytes previous_revision = self.note.revision result = self.http_post( "/notebooks/save_note/", dict( notebook_id = self.notebook.object_id, @@ -503,6 +529,7 @@ class Test_notebooks( Test_controller ): # assert that the note wasn't actually updated the second time assert result[ "new_revision" ] == None assert result[ "previous_revision" ] == previous_revision + assert self.user.storage_bytes == previous_storage_bytes result = self.http_post( "/notebooks/load_note_by_title/", dict( notebook_id = self.notebook.object_id, @@ -553,6 +580,7 @@ class Test_notebooks( Test_controller ): # assert that the note wasn't actually updated the second time assert result[ "new_revision" ] == None assert result[ "previous_revision" ] == previous_revision + assert self.user.storage_bytes > 0 result = self.http_post( "/notebooks/load_note_by_title/", dict( notebook_id = self.notebook.object_id, @@ -602,6 +630,7 @@ class Test_notebooks( Test_controller ): assert result[ "new_revision" ] assert result[ "new_revision" ] not in ( first_revision, second_revision ) assert result[ "previous_revision" ] == second_revision + assert self.user.storage_bytes > 0 # make sure the first title can no longer be loaded result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -646,6 +675,7 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) + assert self.user.storage_bytes == 0 def test_save_new_note( self, startup = False ): self.login() @@ -663,6 +693,7 @@ class Test_notebooks( Test_controller ): assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision assert result[ "previous_revision" ] == None + assert self.user.storage_bytes > 0 # make sure the new title is now loadable result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -705,6 +736,7 @@ class Test_notebooks( Test_controller ): assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision assert result[ "previous_revision" ] == None + assert self.user.storage_bytes > 0 # make sure the new title is now loadable result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -738,6 +770,7 @@ class Test_notebooks( Test_controller ): assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision assert result[ "previous_revision" ] == None + assert self.user.storage_bytes > 0 # make sure the new title is now loadable result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -770,6 +803,7 @@ class Test_notebooks( Test_controller ): assert len( notebook.startup_notes ) == 2 assert notebook.startup_notes[ 0 ] == self.note assert notebook.startup_notes[ 1 ] == self.note2 + assert self.user.storage_bytes > 0 def test_add_startup_note_without_login( self ): result = self.http_post( "/notebooks/add_startup_note/", dict( @@ -778,6 +812,7 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) + assert self.user.storage_bytes == 0 def test_add_startup_note_with_unknown_notebook( self ): self.login() @@ -797,6 +832,7 @@ class Test_notebooks( Test_controller ): assert len( notebook.startup_notes ) == 1 assert notebook.startup_notes[ 0 ] == self.note + assert self.user.storage_bytes == 0 def test_add_startup_unknown_note( self ): self.login() @@ -816,6 +852,7 @@ class Test_notebooks( Test_controller ): assert len( notebook.startup_notes ) == 1 assert notebook.startup_notes[ 0 ] == self.note + assert self.user.storage_bytes == 0 def test_remove_startup_note( self ): self.login() @@ -834,6 +871,7 @@ class Test_notebooks( Test_controller ): notebook = result[ "notebook" ] assert len( notebook.startup_notes ) == 0 + assert self.user.storage_bytes > 0 def test_remove_startup_note_without_login( self ): result = self.http_post( "/notebooks/remove_startup_note/", dict( @@ -842,6 +880,7 @@ class Test_notebooks( Test_controller ): ), session_id = self.session_id ) assert result.get( "error" ) + assert self.user.storage_bytes == 0 def test_remove_startup_note_with_unknown_notebook( self ): self.login() @@ -861,6 +900,7 @@ class Test_notebooks( Test_controller ): assert len( notebook.startup_notes ) == 1 assert notebook.startup_notes[ 0 ] == self.note + assert self.user.storage_bytes == 0 def test_remove_startup_unknown_note( self ): self.login() @@ -880,6 +920,7 @@ class Test_notebooks( Test_controller ): assert len( notebook.startup_notes ) == 1 assert notebook.startup_notes[ 0 ] == self.note + assert self.user.storage_bytes == 0 def test_is_startup_note( self ): self.login() diff --git a/controller/test/Test_users.py b/controller/test/Test_users.py index e93b5fd..a5641c7 100644 --- a/controller/test/Test_users.py +++ b/controller/test/Test_users.py @@ -98,6 +98,10 @@ class Test_users( Test_controller ): else: assert startup_notes == [] + rate_plan = result[ u"rate_plan" ] + assert rate_plan[ u"name" ] == u"super" + assert rate_plan[ u"storage_quota_bytes" ] == 1337 + def test_current_with_startup_notes_after_signup( self ): self.test_current_after_signup( include_startup_notes = True ) @@ -229,10 +233,37 @@ class Test_users( Test_controller ): def test_update_storage( self ): previous_revision = self.user.revision - cherrypy.root.users.update_storage( self.user ) + cherrypy.root.users.update_storage( self.user.object_id ) self.scheduler.wait_until_idle() expected_size = cherrypy.root.users.calculate_storage( self.user ) assert self.user.storage_bytes == expected_size assert self.user.revision > previous_revision + + def test_update_storage_with_unknown_user_id( self ): + original_revision = self.user.revision + + cherrypy.root.users.update_storage( 77 ) + self.scheduler.wait_until_idle() + + expected_size = cherrypy.root.users.calculate_storage( self.user ) + + assert self.user.storage_bytes == 0 + assert self.user.revision == original_revision + + def test_update_storage_with_callback( self ): + def gen(): + previous_revision = self.user.revision + + cherrypy.root.users.update_storage( self.user.object_id, self.scheduler.thread ) + user = ( yield Scheduler.SLEEP ) + + expected_size = cherrypy.root.users.calculate_storage( self.user ) + assert user == self.user + assert self.user.storage_bytes == expected_size + assert self.user.revision > previous_revision + + g = gen() + self.scheduler.add( g ) + self.scheduler.wait_for( g ) diff --git a/model/User.py b/model/User.py index a00b77e..2ef3f13 100644 --- a/model/User.py +++ b/model/User.py @@ -13,7 +13,9 @@ class User( Persistent ): def __setstate__( self, state ): if "_User__storage_bytes" not in state: - state[ "_User__storage_bytes" ] = False + state[ "_User__storage_bytes" ] = 0 + if "_User__rate_plan" not in state: + state[ "_User__rate_plan" ] = 0 self.__dict__.update( state ) @@ -40,6 +42,7 @@ class User( Persistent ): self.__email_address = email_address self.__notebooks = notebooks or [] self.__storage_bytes = 0 # total storage bytes for this user's notebooks, notes, and revisions + self.__rate_plan = 0 # each rate plan is an integer index into the array in config/Common.py def __create_salt( self ): return "".join( [ random.choice( self.SALT_CHARS ) for i in range( self.SALT_SIZE ) ] ) @@ -82,6 +85,8 @@ class User( Persistent ): d = Persistent.to_dict( self ) d.update( dict( username = self.username, + storage_bytes = self.__storage_bytes, + rate_plan = self.__rate_plan, ) ) return d @@ -99,10 +104,15 @@ class User( Persistent ): self.update_revision() self.__storage_bytes = storage_bytes + def __set_rate_plan( self, rate_plan ): + self.update_revision() + self.__rate_plan = rate_plan + username = property( lambda self: self.secondary_id ) email_address = property( lambda self: self.__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 ) # the notebooks (read-only and read-write) that this user has access to notebooks = property( lambda self: copy( self.__notebooks ), __set_notebooks ) diff --git a/model/test/Test_user.py b/model/test/Test_user.py index 532ff25..6522caa 100644 --- a/model/test/Test_user.py +++ b/model/test/Test_user.py @@ -17,6 +17,7 @@ class Test_user( object ): assert self.user.email_address == self.email_address assert self.user.notebooks == [] assert self.user.storage_bytes == 0 + assert self.user.rate_plan == 0 def test_check_correct_password( self ): assert self.user.check_password( self.password ) == True @@ -60,6 +61,14 @@ class Test_user( object ): assert self.user.storage_bytes == storage_bytes assert self.user.revision > previous_revision + def test_set_rate_plan( self ): + previous_revision = self.user.revision + rate_plan = 2 + self.user.rate_plan = rate_plan + + assert self.user.rate_plan == rate_plan + assert self.user.revision > previous_revision + class Test_user_with_notebooks( object ): def setUp( self ): @@ -116,3 +125,10 @@ class Test_user_with_notebooks( object ): trash = Notebook( trash_id, u"trash" ) notebook = Notebook( notebook_id, u"my new notebook", trash ) assert self.user.has_access( notebook.object_id ) == False + + def test_to_dict( self ): + d = self.user.to_dict() + + assert d.get( "username" ) == self.username + assert d.get( "storage_bytes" ) == self.user.storage_bytes + assert d.get( "rate_plan" ) == self.user.rate_plan diff --git a/static/css/style.css b/static/css/style.css index 7d2b90c..1218b54 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -346,6 +346,27 @@ ol li { padding: 0.25em 0.25em 0.25em 0.5em; } +.storage_usage_area { +} + +.storage_usage_low { + color: green; + font-weight: bold; + margin-top: 1.5em; +} + +.storage_usage_medium { + color: orange; + font-weight: bold; + margin-top: 1.5em; +} + +.storage_usage_high { + color: red; + font-weight: bold; + margin-top: 1.5em; +} + .current_notebook_name { background-color: #b0d0ff; -moz-border-radius: 0.5em; diff --git a/static/js/Wiki.js b/static/js/Wiki.js index 1bf51f6..e5d2a3e 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -12,6 +12,7 @@ function Wiki( invoker ) { this.search_results_editor = null; // editor for display of search results this.invoker = invoker; this.search_titles_only = true; + this.rate_plan = null; connect( this.invoker, "error_message", this, "display_error" ); connect( "search_form", "onsubmit", this, "search" ); @@ -86,6 +87,9 @@ Wiki.prototype.display_user = function ( result ) { }, notebook.name ) ) ); } + this.rate_plan = result.rate_plan; + this.display_storage_usage( result.user.storage_bytes ); + // display the name of the logged in user and a logout link appendChildNodes( user_span, "logged in as " + result.user.username ); appendChildNodes( user_span, " | " ); @@ -99,6 +103,30 @@ Wiki.prototype.display_user = function ( result ) { } ); } +Wiki.prototype.display_storage_usage = function( storage_bytes ) { + // display the user's current storage usage + var MEGABYTE = 1024 * 1024; + function bytes_to_megabytes( storage_bytes ) { + return Math.round( storage_bytes / MEGABYTE ); + } + + var quota_bytes = this.rate_plan.storage_quota_bytes || 0; + var usage_percent = Math.round( storage_bytes / quota_bytes * 100.0 ); + + if ( usage_percent > 90 ) + var storage_usage_class = "storage_usage_high"; + else if ( usage_percent > 75 ) + var storage_usage_class = "storage_usage_medium"; + else + var storage_usage_class = "storage_usage_low"; + + replaceChildNodes( + "storage_usage_area", + createDOM( "div", { "class": storage_usage_class }, + bytes_to_megabytes( storage_bytes ) + " MB (" + usage_percent + "%) of " + bytes_to_megabytes( quota_bytes ) + " MB used" ) + ); +} + Wiki.prototype.populate = function ( result ) { this.notebook = result.notebook; var self = this; diff --git a/view/Link_area.py b/view/Link_area.py index a02e9a1..de684a1 100644 --- a/view/Link_area.py +++ b/view/Link_area.py @@ -11,4 +11,7 @@ class Link_area( Div ): Div( id = u"notebooks_area", ), + Div( + id = u"storage_usage_area", + ), )