witten
/
luminotes
Archived
1
0
Fork 0

Pulled in and merged most recent revision with iframes-on-demand changes.

This commit is contained in:
Dan Helfman 2009-01-03 15:56:54 -08:00
commit e043cf1e0c
48 changed files with 782 additions and 496 deletions

41
NEWS
View File

@ -1,6 +1,45 @@
1.5.9: 1.5.13: January ??, 2009
* Fixed various bugs related to the subscription page.
* Switching between notebooks and loading notebooks is now even faster. * Switching between notebooks and loading notebooks is now even faster.
1.5.12: December 30, 2008
* Fixed a bug in which clicking on the notebook rename text field ended the
renaming prematurely.
* Potential fix for a bug in which product downloads and attached file
downloads occasionally did not complete in Internet Explorer.
* Added a 30-day free trial to all Luminotes subscription plans, and updated
the pricing page accordingly.
1.5.11: December 27, 2008
* Added a font selection button to the toolbar.
* Decreased the default note text font size, so now you can see more of your
note text at once.
* Added rounded corners to several display elements.
* Improved the layout on low-resolution displays (1024x768 and below).
* Fixed a Luminotes Desktop bug in which creating and then clicking on a new
note link sometimes caused a red error message.
* Fixed a bug in which yellow pulldowns that were opened towards the bottom
of the page appeared partially off the page.
* Fixed a bug in which forum post permalinks didn't work on posts after the
first ten in a particular thread.
1.5.10: December 4, 2008
* Fixed a bug in which certain new installations of Luminotes Desktop
on Windows yielded an "uh oh" error on initial launch. This bug did
not occur during upgrades. It only affected new installations.
1.5.9: December 3, 2008
* When you hover the mouse over a link and a link pulldown appears, that
pulldown will now automatically disappear soon after you move the mouse
away.
* Changed the "new note" key from ctrl-N to ctrl-M so as not to conflict with
the "new browser window" key used in most web browsers.
* Fixed a Chrome/Safari bug in which ending a link didn't always work.
* Fixed a rare Chrome/Safari bug in which pressing backspace sometimes made
the text cursor vanish.
* Fixed an Internet Explorer bug in which backspace sometimes didn't work,
such as when backspacing an empty list element.
1.5.8: November 24, 2008 1.5.8: November 24, 2008
* Fixed a bug that prevented notes from being automatically saved in certain * Fixed a bug that prevented notes from being automatically saved in certain
notebooks. notebooks.

3
README
View File

@ -27,7 +27,8 @@ click "Keep Blocking".
* File storage: In case you're curious, your notes are stored within the * File storage: In case you're curious, your notes are stored within the
%APPDATA%\Luminotes folder in a database file. Attached files are stored with %APPDATA%\Luminotes folder in a database file. Attached files are stored with
the %APPDATA\Luminotes\files folder. the %APPDATA\Luminotes\files folder. (It is not recommended that you modify
or copy these files.)
* USB drive: If you'd like to run Luminotes from a USB drive, then when the * USB drive: If you'd like to run Luminotes from a USB drive, then when the
Luminotes Desktop installer prompts you to select the installation destination Luminotes Desktop installer prompts you to select the installation destination

View File

@ -26,7 +26,7 @@ settings = {
"session_filter.storage_type": "ram", "session_filter.storage_type": "ram",
"session_filter.timeout": 60 * 24 * 365, # one year "session_filter.timeout": 60 * 24 * 365, # one year
"static_filter.root": os.getcwd(), "static_filter.root": os.getcwd(),
"server.log_to_screen": False, "server.log_to_screen": True,
"server.log_file": os.path.join( gettempdir(), "luminotes_error%s.log" % username_postfix ), "server.log_file": os.path.join( gettempdir(), "luminotes_error%s.log" % username_postfix ),
"server.log_access_file": os.path.join( gettempdir(), "luminotes%s.log" % username_postfix ), "server.log_access_file": os.path.join( gettempdir(), "luminotes%s.log" % username_postfix ),
"server.log_tracebacks": True, "server.log_tracebacks": True,

View File

@ -1 +1 @@
VERSION = u"1.5.9" VERSION = u"1.5.13"

View File

@ -69,11 +69,17 @@ class Database( object ):
from pytz import utc from pytz import utc
TIMESTAMP_PATTERN = re.compile( "^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d).(\d+)(?:\+\d\d:\d\d$)?" ) TIMESTAMP_PATTERN = re.compile( "^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d).(\d+)(?:\+\d\d:\d\d$)?" )
MICROSECONDS_PER_SECOND = 1000000
def convert_timestamp( value ): def convert_timestamp( value ):
( year, month, day, hours, minutes, seconds, fractional_seconds ) = \ ( year, month, day, hours, minutes, seconds, fractional_seconds ) = \
TIMESTAMP_PATTERN.search( value ).groups( 0 ) TIMESTAMP_PATTERN.search( value ).groups( 0 )
microseconds = int( float ( "0." + fractional_seconds ) * 1000000 )
# convert fractional seconds (with an arbitrary number of decimal places) to microseconds
microseconds = int( fractional_seconds )
while microseconds > MICROSECONDS_PER_SECOND:
fractional_seconds = fractional_seconds[ : -1 ]
microseconds = int( fractional_seconds or 0 )
# ignore time zone in timestamp and assume UTC # ignore time zone in timestamp and assume UTC
return datetime( return datetime(

View File

@ -15,7 +15,7 @@ from Expose import expose
from Validate import validate, Valid_int, Valid_bool, Validation_error from Validate import validate, Valid_int, Valid_bool, Validation_error
from Database import Valid_id, end_transaction from Database import Valid_id, end_transaction
from Users import grab_user_id, Access_error from Users import grab_user_id, Access_error
from Expire import strongly_expire from Expire import strongly_expire, weakly_expire
from model.File import File from model.File import File
from model.User import User from model.User import User
from model.Notebook import Notebook from model.Notebook import Notebook
@ -262,6 +262,7 @@ class Files( object ):
self.__download_products = download_products self.__download_products = download_products
@expose() @expose()
@weakly_expire
@end_transaction @end_transaction
@grab_user_id @grab_user_id
@validate( @validate(
@ -323,6 +324,7 @@ class Files( object ):
return stream() return stream()
@expose() @expose()
@weakly_expire
@end_transaction @end_transaction
@validate( @validate(
access_id = Valid_id(), access_id = Valid_id(),
@ -409,6 +411,7 @@ class Files( object ):
) )
@expose() @expose()
@weakly_expire
@end_transaction @end_transaction
@grab_user_id @grab_user_id
@validate( @validate(
@ -471,6 +474,7 @@ class Files( object ):
return stream( image_buffer ) return stream( image_buffer )
@expose() @expose()
@weakly_expire
@end_transaction @end_transaction
@grab_user_id @grab_user_id
@validate( @validate(

View File

@ -212,7 +212,11 @@ class Forum( object ):
# if a single note was requested, just return that one note # if a single note was requested, just return that one note
if note_id: if note_id:
result[ "notes" ] = [ note for note in result[ "notes" ] if note.object_id == note_id ] note = self.__database.load( Note, note_id )
if note:
result[ "notes" ] = [ note ]
else:
result[ "notes" ] = []
return result return result

View File

@ -463,7 +463,7 @@ class Users( object ):
) )
return dict( return dict(
form = button % user_id, form = button % ( user_id, 0 ) # 0 = new subscription, 1 = modify an existing subscription
) )
@expose() @expose()
@ -1450,7 +1450,12 @@ class Users( object ):
if mc_gross and mc_gross not in ( fee, yearly_fee ): if mc_gross and mc_gross not in ( fee, yearly_fee ):
raise Payment_error( u"invalid mc_gross", params ) raise Payment_error( u"invalid mc_gross", params )
# verify mc_amount3 # verify mc_amount1 (free 30-day trial)
mc_amount1 = params.get( u"mc_amount1" )
if mc_amount1 and mc_amount1 != "0.00":
raise Payment_error( u"invalid mc_amount1", params )
# verify mc_amount3 (actual payment)
mc_amount3 = params.get( u"mc_amount3" ) mc_amount3 = params.get( u"mc_amount3" )
if mc_amount3 and mc_amount3 not in ( fee, yearly_fee ): if mc_amount3 and mc_amount3 not in ( fee, yearly_fee ):
raise Payment_error( u"invalid mc_amount3", params ) raise Payment_error( u"invalid mc_amount3", params )
@ -1460,9 +1465,14 @@ class Users( object ):
if item_name and item_name.lower() != u"luminotes " + rate_plan[ u"name" ].lower(): if item_name and item_name.lower() != u"luminotes " + rate_plan[ u"name" ].lower():
raise Payment_error( u"invalid item_name", params ) raise Payment_error( u"invalid item_name", params )
# verify period1 and period2 (should not be present) # verify period1 (free 30-day trial)
if params.get( u"period1" ) or params.get( u"period2" ): period1 = params.get( u"period1" )
raise Payment_error( u"invalid period", params ) if period1 and period1 != "30 D":
raise Payment_error( u"invalid period1", params )
# verify period2 (should not be present)
if params.get( u"period2" ):
raise Payment_error( u"invalid period2", params )
# verify period3 # verify period3
period3 = params.get( u"period3" ) period3 = params.get( u"period3" )
@ -1509,7 +1519,7 @@ class Users( object ):
self.__database.save( user, commit = False ) self.__database.save( user, commit = False )
self.update_groups( user ) self.update_groups( user )
self.__database.commit() self.__database.commit()
elif txn_type in ( u"subscr_payment", u"subscr_failed" ): elif txn_type in ( u"subscr_payment", u"subscr_failed", "subscr_eot" ):
pass # for now, ignore payments and let paypal handle them pass # for now, ignore payments and let paypal handle them
else: else:
raise Payment_error( "unknown txn_type", params ) raise Payment_error( "unknown txn_type", params )
@ -1588,15 +1598,18 @@ class Users( object ):
# if there's no rate plan or we've retried too many times, give up and display an error # if there's no rate plan or we've retried too many times, give up and display an error
RETRY_TIMEOUT = 15 RETRY_TIMEOUT = 15
if rate_plan is None or retry_count > RETRY_TIMEOUT: if retry_count > RETRY_TIMEOUT:
note = Thanks_error_note() note = Thanks_error_note()
# if the rate plan of the subscription matches the user's current rate plan, success # if the rate plan of the subscription matches the user's current rate plan, success
elif rate_plan == result[ u"user" ].rate_plan: elif rate_plan == result[ u"user" ].rate_plan:
note = Thanks_note( self.__rate_plans[ rate_plan ][ u"name" ].capitalize() ) note = Thanks_note( self.__rate_plans[ rate_plan ][ u"name" ].capitalize() )
result[ "conversion" ] = "subscribe_%s" % rate_plan result[ "conversion" ] = "subscribe_%s" % rate_plan
# otherwise, display an auto-reloading "processing..." page # if a rate plan is given, display an auto-reloading "processing..." page
else: elif rate_plan is not None:
note = Processing_note( rate_plan, retry_count ) note = Processing_note( rate_plan, retry_count )
# otherwise, assume that this is a free trial and default to a generic thanks page
else:
note = Thanks_note()
result[ "notebook" ] = main_notebook result[ "notebook" ] = main_notebook
result[ "startup_notes" ] = self.__database.select_many( Note, main_notebook.sql_load_startup_notes() ) result[ "startup_notes" ] = self.__database.select_many( Note, main_notebook.sql_load_startup_notes() )

View File

@ -80,8 +80,8 @@ class Test_controller( object ):
u"included_users": 1, u"included_users": 1,
u"fee": 1.99, u"fee": 1.99,
u"yearly_fee": 19.90, u"yearly_fee": 19.90,
u"button": u"[subscribe here user %s!] button", u"button": u"[subscribe here user %s!] button (modify=%s)",
u"yearly_button": u"[yearly subscribe here user %s!] button", u"yearly_button": u"[yearly subscribe here user %s!] button (modify=%s)",
}, },
{ {
u"name": "extra super", u"name": "extra super",
@ -91,8 +91,8 @@ class Test_controller( object ):
u"included_users": 3, u"included_users": 3,
u"fee": 9.00, u"fee": 9.00,
u"yearly_fee": 90.00, u"yearly_fee": 90.00,
u"button": u"[or here user %s!] button", u"button": u"[or here user %s!] button (modify=%s)",
u"yearly_button": u"[yearly or here user %s!] button", u"yearly_button": u"[yearly or here user %s!] button (modify=%s)",
}, },
], ],
"luminotes.download_products": [ "luminotes.download_products": [

View File

@ -66,6 +66,40 @@ class Test_database( object ):
assert obj.revision.replace( tzinfo = utc ) == original_revision assert obj.revision.replace( tzinfo = utc ) == original_revision
assert obj.value == basic_obj.value assert obj.value == basic_obj.value
def test_select_datetime( self ):
# this revision (with .504099) happens to test for a bug caused by floating point rounding errors
original_revision = "2008-01-01 01:00:42.504099+00:00"
basic_obj = Stub_object( object_id = "5", revision = original_revision, value = 1 )
self.database.save( basic_obj )
obj = self.database.select_one( Stub_object, Stub_object.sql_load( basic_obj.object_id ) )
assert obj.object_id == basic_obj.object_id
assert str( obj.revision.replace( tzinfo = utc ) ) == original_revision
assert obj.value == basic_obj.value
def test_select_datetime_with_many_fractional_digits( self ):
original_revision = "2008-01-01 01:00:42.5032429489284+00:00"
basic_obj = Stub_object( object_id = "5", revision = original_revision, value = 1 )
self.database.save( basic_obj )
obj = self.database.select_one( Stub_object, Stub_object.sql_load( basic_obj.object_id ) )
assert obj.object_id == basic_obj.object_id
assert str( obj.revision.replace( tzinfo = utc ) ) == "2008-01-01 01:00:42.503242+00:00"
assert obj.value == basic_obj.value
def test_select_datetime_with_zero_fractional_seconds( self ):
original_revision = "2008-01-01 01:00:42.0+00:00"
basic_obj = Stub_object( object_id = "5", revision = original_revision, value = 1 )
self.database.save( basic_obj )
obj = self.database.select_one( Stub_object, Stub_object.sql_load( basic_obj.object_id ) )
assert obj.object_id == basic_obj.object_id
assert str( obj.revision.replace( tzinfo = utc ) ) == "2008-01-01 01:00:42+00:00"
assert obj.value == basic_obj.value
def test_select_one_tuple( self ): def test_select_one_tuple( self ):
obj = self.database.select_one( tuple, Stub_object.sql_tuple() ) obj = self.database.select_one( tuple, Stub_object.sql_tuple() )
@ -185,7 +219,7 @@ class Test_database( object ):
self.connection.rollback() self.connection.rollback()
assert self.database.load( Stub_object, next_id ) == None assert self.database.load( Stub_object, next_id ) == None
def test_next_id_with_explit_commit( self ): def test_next_id_with_explicit_commit( self ):
next_id = self.database.next_id( Stub_object, commit = False ) next_id = self.database.next_id( Stub_object, commit = False )
self.database.commit() self.database.commit()
assert next_id assert next_id

View File

@ -268,6 +268,54 @@ class Test_forums( Test_controller ):
user = self.database.load( User, self.user.object_id ) user = self.database.load( User, self.user.object_id )
assert user.storage_bytes == 0 assert user.storage_bytes == 0
def test_general_thread_default_with_unknown_note_id( self ):
result = self.http_get( "/forums/general/%s?note_id=unknownid" % self.general_thread.object_id )
assert result.get( u"user" ).object_id == self.anonymous.object_id
assert len( result.get( u"notebooks" ) ) == 4
assert result.get( u"notebooks" )[ 0 ].object_id == self.anon_notebook.object_id
assert result.get( u"login_url" )
assert result.get( u"logout_url" )
assert result.get( u"rate_plan" )
assert result.get( u"notebook" ).object_id == self.general_thread.object_id
assert len( result.get( u"startup_notes" ) ) == 0
assert result.get( u"notes" ) == []
assert result.get( u"parent_id" ) == None
assert result.get( u"note_read_write" ) in ( None, True )
assert result.get( u"total_notes_count" ) == 0
invites = result[ "invites" ]
assert len( invites ) == 0
user = self.database.load( User, self.user.object_id )
assert user.storage_bytes == 0
def test_general_thread_default_with_note_id( self ):
self.__make_notes()
result = self.http_get( "/forums/general/%s?note_id=%s" % ( self.general_thread.object_id, self.note.object_id ) )
assert result.get( u"user" ).object_id == self.anonymous.object_id
assert len( result.get( u"notebooks" ) ) == 4
assert result.get( u"notebooks" )[ 0 ].object_id == self.anon_notebook.object_id
assert result.get( u"login_url" )
assert result.get( u"logout_url" )
assert result.get( u"rate_plan" )
assert result.get( u"notebook" ).object_id == self.general_thread.object_id
assert len( result.get( u"startup_notes" ) ) == 3
assert len( result.get( u"notes" ) ) == 1
assert result.get( u"notes" )[ 0 ].object_id == self.note.object_id
assert result[ u"notes" ][ 0 ].title == u"foo"
assert result.get( u"parent_id" ) == None
assert result.get( u"note_read_write" ) in ( None, True )
assert result.get( u"total_notes_count" ) == 3
invites = result[ "invites" ]
assert len( invites ) == 0
user = self.database.load( User, self.user.object_id )
assert user.storage_bytes == 0
def test_general_thread_default_with_login( self ): def test_general_thread_default_with_login( self ):
self.login() self.login()

View File

@ -542,7 +542,7 @@ class Test_users( Test_controller ):
form = result.get( u"form" ) form = result.get( u"form" )
plan = self.settings[ u"global" ][ u"luminotes.rate_plans" ][ 1 ] plan = self.settings[ u"global" ][ u"luminotes.rate_plans" ][ 1 ]
assert form == plan[ u"button" ] % self.user.object_id assert form == plan[ u"button" ] % ( self.user.object_id, 0 )
def test_subscribe_yearly( self ): def test_subscribe_yearly( self ):
self.login() self.login()
@ -555,7 +555,7 @@ class Test_users( Test_controller ):
form = result.get( u"form" ) form = result.get( u"form" )
plan = self.settings[ u"global" ][ u"luminotes.rate_plans" ][ 1 ] plan = self.settings[ u"global" ][ u"luminotes.rate_plans" ][ 1 ]
assert form == plan[ u"yearly_button" ] % self.user.object_id assert form == plan[ u"yearly_button" ] % ( self.user.object_id, 0 )
def test_subscribe_with_free_rate_plan( self ): def test_subscribe_with_free_rate_plan( self ):
self.login() self.login()
@ -3351,6 +3351,43 @@ class Test_users( Test_controller ):
user = self.database.load( User, self.user.object_id ) user = self.database.load( User, self.user.object_id )
assert user.rate_plan == 0 assert user.rate_plan == 0
def test_paypal_notify_payment_with_trial( self ):
data = dict( self.PAYMENT_DATA )
data[ u"custom" ] = self.user.object_id
data[ u"mc_amount1" ] = u"0.00"
data[ u"period1" ] = u"30 D"
result = self.http_post( "/users/paypal_notify", data );
assert len( result ) == 1
assert result.get( u"session_id" )
assert Stub_urllib2.result == u"VERIFIED"
assert Stub_urllib2.headers.get( u"Content-type" ) == u"application/x-www-form-urlencoded"
assert Stub_urllib2.url.startswith( "https://" )
assert u"paypal.com" in Stub_urllib2.url
assert Stub_urllib2.encoded_params
# being notified of a mere payment should not change the user's rate plan
user = self.database.load( User, self.user.object_id )
assert user.rate_plan == 0
def test_paypal_notify_payment_with_trial_invalid_period( self ):
data = dict( self.PAYMENT_DATA )
data[ u"custom" ] = self.user.object_id
data[ u"mc_amount1" ] = u"0.00"
data[ u"period1" ] = u"31 D"
result = self.http_post( "/users/paypal_notify", data );
assert result.get( u"error" )
def test_paypal_notify_payment_with_trial_invalid_amount( self ):
data = dict( self.PAYMENT_DATA )
data[ u"custom" ] = self.user.object_id
data[ u"mc_amount1" ] = u"2.50"
data[ u"period1" ] = u"30 D"
result = self.http_post( "/users/paypal_notify", data );
assert result.get( u"error" )
def test_paypal_notify_payment_invalid( self ): def test_paypal_notify_payment_invalid( self ):
data = dict( self.PAYMENT_DATA ) data = dict( self.PAYMENT_DATA )
data[ u"custom" ] = self.user.object_id data[ u"custom" ] = self.user.object_id
@ -4962,7 +4999,7 @@ class Test_users( Test_controller ):
assert result[ u"notes" ][ 0 ].title == u"thank you" assert result[ u"notes" ][ 0 ].title == u"thank you"
assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
assert u"Thank you" in result[ u"notes" ][ 0 ].contents assert u"Thank you" in result[ u"notes" ][ 0 ].contents
assert u"confirmation" in result[ u"notes" ][ 0 ].contents assert u"confirmation" not in result[ u"notes" ][ 0 ].contents
def test_thanks_download( self ): def test_thanks_download( self ):
access_id = u"wheeaccessid" access_id = u"wheeaccessid"

3
model/delta/1.5.10.sql Normal file
View File

@ -0,0 +1,3 @@
DROP VIEW notebook_current;
CREATE VIEW notebook_current AS
SELECT id, revision, name, trash_id, deleted, user_id FROM notebook WHERE (notebook.revision IN (SELECT max(sub_notebook.revision) AS max FROM notebook sub_notebook WHERE (sub_notebook.id = notebook.id))) and notebook.name is not null;

View File

@ -104,7 +104,7 @@ CREATE TABLE notebook (
); );
CREATE VIEW notebook_current AS CREATE VIEW notebook_current AS
SELECT id, revision, name, trash_id, deleted, user_id FROM notebook WHERE (notebook.revision IN (SELECT max(sub_notebook.revision) AS max FROM notebook sub_notebook WHERE (sub_notebook.id = notebook.id))); SELECT id, revision, name, trash_id, deleted, user_id FROM notebook WHERE (notebook.revision IN (SELECT max(sub_notebook.revision) AS max FROM notebook sub_notebook WHERE (sub_notebook.id = notebook.id))) and notebook.name is not null;
CREATE TABLE password_reset ( CREATE TABLE password_reset (
id text NOT NULL, id text NOT NULL,

View File

@ -1,5 +1,6 @@
body { body {
padding: 1em; padding: 1em;
font-size: 90%;
background-color: #fafafa; background-color: #fafafa;
line-height: 140%; line-height: 140%;
font-family: sans-serif; font-family: sans-serif;
@ -13,6 +14,7 @@ body {
} }
.note_frame { .note_frame {
-moz-border-radius: 5px;
text-align: left; text-align: left;
margin: 0em; margin: 0em;
padding: 1.5em; padding: 1.5em;

View File

@ -71,3 +71,7 @@
margin-bottom: 0.25em; margin-bottom: 0.25em;
padding: 0.25em 0.25em 0 0.5em; padding: 0.25em 0.25em 0 0.5em;
} }
#current_notebook_wrapper {
margin-right: 1em;
}

View File

@ -1,5 +1,6 @@
body { body {
padding: 1em; padding: 1em;
font-size: 90%;
line-height: 140%; line-height: 140%;
font-family: sans-serif; font-family: sans-serif;
} }
@ -128,7 +129,7 @@ ol li {
.small_text { .small_text {
padding-top: 0.5em; padding-top: 0.5em;
font-size: 72%; font-size: 90%;
} }
.radio_label { .radio_label {

View File

@ -40,10 +40,10 @@
background-image: url(/static/images/toolbar/strikethrough_button.png); background-image: url(/static/images/toolbar/strikethrough_button.png);
} }
#title_button_preload { #font_button_preload {
height: 0; height: 0;
overflow: hidden; overflow: hidden;
background-image: url(/static/images/toolbar/title_button.png); background-image: url(/static/images/toolbar/font_button.png);
} }
#bullet_list_button_preload { #bullet_list_button_preload {
@ -65,6 +65,8 @@
} }
.hook_area { .hook_area {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
padding-top: 1.5em; padding-top: 1.5em;
padding-bottom: 1.5em; padding-bottom: 1.5em;
width: 100%; width: 100%;
@ -93,6 +95,8 @@
background-color: #ffff99; background-color: #ffff99;
font-weight: bold; font-weight: bold;
padding: 1em; padding: 1em;
margin-top: 1em;
margin-bottom: 1em;
} }
.hook_action { .hook_action {
@ -156,6 +160,8 @@
} }
.thumbnail_area { .thumbnail_area {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
background-color: #fffece; background-color: #fffece;
padding-bottom: 0.5em; padding-bottom: 0.5em;
} }
@ -301,6 +307,10 @@
padding-bottom: 0.5em; padding-bottom: 0.5em;
} }
.lighter_text {
color: #267dff;
}
.upgrade_question { .upgrade_question {
text-align: left; text-align: left;
} }
@ -322,6 +332,12 @@
.upgrade_table_area { .upgrade_table_area {
text-align: center; text-align: center;
margin-bottom: 1em;
}
.luminotes_online_link_area {
margin-top: 1em;
clear: both;
} }
#upgrade_table { #upgrade_table {
@ -337,43 +353,80 @@
} }
#upgrade_table th { #upgrade_table th {
padding: 0.5em; padding: 0.5em 1.5em 0.5em 1.5em;
border: 1px solid #999999; border: 1px solid #999999;
border-bottom: 1px solid #cccccc;
}
.plan_width {
width: 20%;
}
.download_plan_width {
width: 400px;
} }
#upgrade_table td { #upgrade_table td {
text-align: center; text-align: center;
background-color: #fafafa; background-color: #fafafa;
padding: 0.5em; padding: 0.25em;
border: 1px solid #999999; border: none;
border-left: 1px solid #999999;
border-right: 1px solid #999999;
}
#upgrade_table .plan_name_area {
text-align: center;
background-color: #d0e0f0;
}
#upgrade_table .focused_plan_name_area {
text-align: center;
background-color: #dde6f0;
border-top: 2px solid #000000;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
}
.plan_name_area a {
color: #000000;
text-decoration: none;
}
.spacer_row {
height: 0.5em;
} }
#upgrade_table .plan_name { #upgrade_table .plan_name {
text-align: center; font-size: 125%;
background-color: #d0e0f0;
} }
#upgrade_table ul { #upgrade_table ul {
margin-top: 0; margin-top: 0;
} }
#upgrade_table .feature_name {
font-size: 82%;
text-align: left;
background-color: #fafafa;
border-bottom: 0px;
}
#upgrade_table .feature_description {
font-size: 82%;
text-align: left;
background-color: #fafafa;
padding: 0.25em;
border-width: 0px;
}
#upgrade_table .feature_value { #upgrade_table .feature_value {
font-size: 82%; font-size: 95%;
cursor: pointer;
}
#upgrade_table .focused_feature_value {
background-color: #ffffff;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
}
#upgrade_table .focused_border_bottom {
border-bottom: 2px solid #000000;
}
#upgrade_table .focused_text {
margin-top: 0.5em;
}
.highlight {
color: #ff6600;
font-weight: bold;
} }
#upgrade_table_small { #upgrade_table_small {
@ -386,26 +439,68 @@
} }
#upgrade_table_small th { #upgrade_table_small th {
padding: 0.5em; padding: 0.5em 1.5em 0.5em 1.5em;
border: 1px solid #999999;
border-bottom: 1px solid #cccccc;
}
#upgrade_table_small tr {
border: 0px solid #999999;
} }
#upgrade_table_small td { #upgrade_table_small td {
text-align: center; text-align: center;
background-color: #fafafa; background-color: #fafafa;
padding: 0.5em; padding: 0.25em;
border: none;
border-left: 1px solid #999999;
border-right: 1px solid #999999;
} }
#upgrade_table_small .plan_name { #upgrade_table_small .plan_name_area {
text-align: center; text-align: center;
background-color: #d0e0f0; background-color: #d0e0f0;
} }
#upgrade_table_small .focused_plan_name_area {
text-align: center;
background-color: #dde6f0;
border-top: 2px solid #000000;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
}
#upgrade_table_small .plan_name {
font-size: 125%;
}
#upgrade_table_small .feature_value { #upgrade_table_small .feature_value {
font-size: 82%; font-size: 95%;
cursor: pointer;
}
#upgrade_table_small .focused_feature_value {
background-color: #ffffff;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
}
#upgrade_table_small .focused_border_bottom {
border-bottom: 2px solid #000000;
}
#upgrade_table_small .focused_text {
margin-top: 0.5em;
}
.subscribe_button_area {
padding-top: 0.5em;
padding-bottom: 0.5em;
} }
.download_button_area { .download_button_area {
padding-top: 0.5em; padding-top: 1em;
padding-bottom: 0.5em;
} }
.yearly_link { .yearly_link {
@ -413,7 +508,8 @@
} }
.price_text { .price_text {
color: #ff6600; margin-top: 0.25em;
color: #267dff;
} }
.month_text { .month_text {
@ -422,7 +518,7 @@
} }
.version_text { .version_text {
padding-top: 0.25em; padding-top: 0.5em;
font-size: 72%; font-size: 72%;
font-weight: normal; font-weight: normal;
} }
@ -432,10 +528,6 @@
margin-bottom: 0; margin-bottom: 0;
} }
.sign_up_button_area {
margin-top: 0.5em;
}
.thumbnail_left { .thumbnail_left {
float: left; float: left;
margin: 0.5em; margin: 0.5em;

View File

@ -107,10 +107,10 @@ h1 {
background-image: url(/static/images/toolbar/strikethrough_button_hover.png); background-image: url(/static/images/toolbar/strikethrough_button_hover.png);
} }
#title_button_hover_preload { #font_button_hover_preload {
height: 0; height: 0;
overflow: hidden; overflow: hidden;
background-image: url(/static/images/toolbar/title_button_hover.png); background-image: url(/static/images/toolbar/font_button_hover.png);
} }
#bullet_list_button_hover_preload { #bullet_list_button_hover_preload {
@ -167,10 +167,10 @@ h1 {
background-image: url(/static/images/toolbar/strikethrough_button_down_hover.png); background-image: url(/static/images/toolbar/strikethrough_button_down_hover.png);
} }
#title_button_down_hover_preload { #font_button_down_hover_preload {
height: 0; height: 0;
overflow: hidden; overflow: hidden;
background-image: url(/static/images/toolbar/title_button_down_hover.png); background-image: url(/static/images/toolbar/font_button_down_hover.png);
} }
#bullet_list_button_down_hover_preload { #bullet_list_button_down_hover_preload {
@ -227,10 +227,10 @@ h1 {
background-image: url(/static/images/toolbar/strikethrough_button_down.png); background-image: url(/static/images/toolbar/strikethrough_button_down.png);
} }
#title_button_down_preload { #font_button_down_preload {
height: 0; height: 0;
overflow: hidden; overflow: hidden;
background-image: url(/static/images/toolbar/title_button_down.png); background-image: url(/static/images/toolbar/font_button_down.png);
} }
#bullet_list_button_down_preload { #bullet_list_button_down_preload {
@ -427,6 +427,12 @@ h1 {
color: #ff6600; color: #ff6600;
} }
#notebook_header_links {
position: absolute;
top: 1.7em;
right: 1em;
}
#rename_form { #rename_form {
margin: 0; margin: 0;
} }
@ -493,6 +499,9 @@ h1 {
} }
.note_button { .note_button {
-moz-border-radius-topleft: 4px;
-moz-border-radius-topright: 4px;
-webkit-border-radius: 4px;
border-style: outset; border-style: outset;
border-width: 0px; border-width: 0px;
background-color: #d0e0f0; background-color: #d0e0f0;
@ -511,9 +520,12 @@ h1 {
#save_button { #save_button {
margin-left: 0.5em; margin-left: 0.5em;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
} }
.note_frame { .note_frame {
-moz-border-radius: 5px;
margin: 0em; margin: 0em;
padding: 0em; padding: 0em;
overflow: hidden; overflow: hidden;
@ -560,6 +572,7 @@ h1 {
} }
.pulldown { .pulldown {
-moz-border-radius: 4px;
position: absolute; position: absolute;
font-size: 72%; font-size: 72%;
text-align: left; text-align: left;
@ -581,6 +594,10 @@ h1 {
text-decoration: none; text-decoration: none;
} }
.selected_mark {
vertical-align: top;
}
.suggestion { .suggestion {
padding: 0.25em 0.5em 0.25em 0.5em; padding: 0.25em 0.5em 0.25em 0.5em;
} }
@ -590,16 +607,31 @@ h1 {
} }
.pulldown_label { .pulldown_label {
-moz-user-select: none;
color: #000000; color: #000000;
text-decoration: none; text-decoration: none;
} }
.font_label_button {
font-size: 125%;
border-style: none;
border-width: 0px;
text-align: left;
background-color: #ffff99;
outline: none;
cursor: pointer;
padding: 0;
margin: 0;
}
.pulldown_label:hover { .pulldown_label:hover {
color: #ff6600; color: #ff6600;
cursor: pointer; cursor: pointer;
} }
.message { .message {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
padding: 0.5em; padding: 0.5em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
font-weight: bold; font-weight: bold;
@ -607,12 +639,16 @@ h1 {
} }
.message_inner { .message_inner {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
padding: 0.5em; padding: 0.5em;
line-height: 140%; line-height: 140%;
background-color: #ffaa44; background-color: #ffaa44;
} }
.error { .error {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
padding: 0.5em; padding: 0.5em;
border: 1px solid #550000; border: 1px solid #550000;
margin-bottom: 0.5em; margin-bottom: 0.5em;
@ -622,6 +658,8 @@ h1 {
} }
.error_inner { .error_inner {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
padding: 0.5em; padding: 0.5em;
line-height: 140%; line-height: 140%;
color: #ffffff; color: #ffffff;
@ -629,6 +667,8 @@ h1 {
} }
.message_button { .message_button {
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
margin-left: 0.5em; margin-left: 0.5em;
border-style: outset; border-style: outset;
border-width: 0px; border-width: 0px;
@ -676,7 +716,7 @@ h1 {
.link_area_item { .link_area_item {
font-size: 75%; font-size: 75%;
padding: 0.25em 0.25em 0.25em 0.5em; padding: 0.15em 0.25em 0.15em 0.5em;
} }
.note_tree_item { .note_tree_item {
@ -872,6 +912,8 @@ h1 {
} }
.hook_action_area { .hook_action_area {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
background-color: #ffff99; background-color: #ffff99;
font-weight: bold; font-weight: bold;
padding: 1em; padding: 1em;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

View File

@ -393,6 +393,10 @@ Editor.prototype.insert_html = function ( html ) {
} }
} }
Editor.prototype.query_command_value = function ( command ) {
return this.document.queryCommandValue( command );
}
// resize the editor's frame to fit the dimensions of its content // resize the editor's frame to fit the dimensions of its content
Editor.prototype.resize = function () { Editor.prototype.resize = function () {
if ( !this.document ) return; if ( !this.document ) return;
@ -437,10 +441,14 @@ Editor.prototype.key_released = function ( event ) {
Editor.prototype.cleanup_html = function ( key_code ) { Editor.prototype.cleanup_html = function ( key_code ) {
if ( WEBKIT ) { if ( WEBKIT ) {
// if enter is pressed while in a title, end title mode, since WebKit doesn't do that for us // if enter is pressed while in a title, end title mode, since WebKit doesn't do that for us
var ENTER = 13; var ENTER = 13; BACKSPACE = 8;
if ( key_code == ENTER && this.state_enabled( "h3" ) ) if ( key_code == ENTER && this.state_enabled( "h3" ) )
this.exec_command( "h3" ); this.exec_command( "h3" );
// if backspace is pressed, skip WebKit style scrubbing since it can cause problems
if ( key_code == BACKSPACE )
return null;
// as of this writing, WebKit doesn't support execCommand( "styleWithCSS" ). for more info, see // as of this writing, WebKit doesn't support execCommand( "styleWithCSS" ). for more info, see
// https://bugs.webkit.org/show_bug.cgi?id=13490 // https://bugs.webkit.org/show_bug.cgi?id=13490
// so to make up for this shortcoming, manually scrub WebKit style spans and other nodes, // so to make up for this shortcoming, manually scrub WebKit style spans and other nodes,
@ -462,8 +470,6 @@ Editor.prototype.cleanup_html = function ( key_code ) {
continue; continue;
var replacement = withDocument( this.document, function () { var replacement = withDocument( this.document, function () {
if ( style == undefined )
return createDOM( "span" );
// font-size is set when ending title mode // font-size is set when ending title mode
if ( style.indexOf( "font-size: " ) != -1 ) if ( style.indexOf( "font-size: " ) != -1 )
return null; return null;
@ -712,7 +718,9 @@ Editor.prototype.end_link = function () {
// end of the link if it's not already there // end of the link if it's not already there
if ( link && WEBKIT ) { if ( link && WEBKIT ) {
var selection = this.iframe.contentWindow.getSelection(); var selection = this.iframe.contentWindow.getSelection();
selection.collapse( link, 1 ); var sentinel = this.document.createTextNode( Editor.title_placeholder_char );
insertSiblingNodesAfter( link, sentinel );
selection.collapse( sentinel, 1 );
} }
} else if ( this.document.selection ) { // browsers such as IE } else if ( this.document.selection ) { // browsers such as IE
// if some text is already selected, unlink it and bail // if some text is already selected, unlink it and bail

View File

@ -330,6 +330,7 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri
connect( "italic", "onclick", function ( event ) { self.toggle_button( event, "italic" ); } ); connect( "italic", "onclick", function ( event ) { self.toggle_button( event, "italic" ); } );
connect( "underline", "onclick", function ( event ) { self.toggle_button( event, "underline" ); } ); connect( "underline", "onclick", function ( event ) { self.toggle_button( event, "underline" ); } );
connect( "strikethrough", "onclick", function ( event ) { self.toggle_button( event, "strikethrough" ); } ); connect( "strikethrough", "onclick", function ( event ) { self.toggle_button( event, "strikethrough" ); } );
connect( "font", "onclick", this, "toggle_font_button" );
connect( "title", "onclick", function ( event ) { self.toggle_button( event, "title" ); } ); connect( "title", "onclick", function ( event ) { self.toggle_button( event, "title" ); } );
connect( "insertUnorderedList", "onclick", function ( event ) { self.toggle_button( event, "insertUnorderedList" ); } ); connect( "insertUnorderedList", "onclick", function ( event ) { self.toggle_button( event, "insertUnorderedList" ); } );
connect( "insertOrderedList", "onclick", function ( event ) { self.toggle_button( event, "insertOrderedList" ); } ); connect( "insertOrderedList", "onclick", function ( event ) { self.toggle_button( event, "insertOrderedList" ); } );
@ -342,6 +343,7 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri
this.make_image_button( "italic" ); this.make_image_button( "italic" );
this.make_image_button( "underline" ); this.make_image_button( "underline" );
this.make_image_button( "strikethrough" ); this.make_image_button( "strikethrough" );
this.make_image_button( "font" );
this.make_image_button( "title" ); this.make_image_button( "title" );
this.make_image_button( "insertUnorderedList", "bullet_list" ); this.make_image_button( "insertUnorderedList", "bullet_list" );
this.make_image_button( "insertOrderedList", "numbered_list" ); this.make_image_button( "insertOrderedList", "numbered_list" );
@ -1069,8 +1071,8 @@ Wiki.prototype.key_pressed = function ( event ) {
var code = event.key().code; var code = event.key().code;
if ( event.modifier().ctrl ) { if ( event.modifier().ctrl ) {
// ctrl-n: new note // ctrl-m: make a new note
if ( code == 78 ) if ( code == 77 )
this.create_blank_editor( event ); this.create_blank_editor( event );
} }
} }
@ -1112,8 +1114,8 @@ Wiki.prototype.editor_key_pressed = function ( editor, event ) {
// ctrl-l: link // ctrl-l: link
} else if ( code == 76 ) { } else if ( code == 76 ) {
this.toggle_link_button( event ); this.toggle_link_button( event );
// ctrl-n: new note // ctrl-m: make a new note
} else if ( code == 78 ) { } else if ( code == 77 ) {
this.create_blank_editor( event ); this.create_blank_editor( event );
// ctrl-h: hide note // ctrl-h: hide note
} else if ( code == 72 ) { } else if ( code == 72 ) {
@ -1150,8 +1152,10 @@ Wiki.prototype.editor_key_pressed = function ( editor, event ) {
} else if ( code == 8 && editor.document.selection ) { } else if ( code == 8 && editor.document.selection ) {
var range = editor.document.selection.createRange(); var range = editor.document.selection.createRange();
range.moveStart( "character", -1 ); range.moveStart( "character", -1 );
range.text = ""; if ( range.text != "" ) {
event.stop(); range.text = "";
event.stop();
}
} }
} }
@ -1330,6 +1334,7 @@ Wiki.prototype.update_toolbar = function() {
this.update_button( "italic", "i", node_names ); this.update_button( "italic", "i", node_names );
this.update_button( "underline", "u", node_names ); this.update_button( "underline", "u", node_names );
this.update_button( "strikethrough", "strike", node_names ); this.update_button( "strikethrough", "strike", node_names );
this.update_button( "font", "font", node_names );
this.update_button( "title", "h3", node_names ); this.update_button( "title", "h3", node_names );
this.update_button( "insertUnorderedList", "ul", node_names ); this.update_button( "insertUnorderedList", "ul", node_names );
this.update_button( "insertOrderedList", "ol", node_names ); this.update_button( "insertOrderedList", "ol", node_names );
@ -1405,6 +1410,30 @@ Wiki.prototype.toggle_attach_button = function ( event ) {
event.stop(); event.stop();
} }
Wiki.prototype.toggle_font_button = function ( event ) {
if ( this.focused_editor && this.focused_editor.read_write ) {
this.focused_editor.focus();
// if a pulldown is already open, then just close it
var existing_div = getElement( "font_pulldown" );
if ( existing_div ) {
this.up_image_button( "font" );
existing_div.pulldown.shutdown();
existing_div.pulldown = null;
return;
}
this.down_image_button( "font" );
this.clear_messages();
this.clear_pulldowns();
new Font_pulldown( this, this.notebook.object_id, this.invoker, event.target(), this.focused_editor );
}
event.stop();
}
Wiki.prototype.hide_editor = function ( event, editor ) { Wiki.prototype.hide_editor = function ( event, editor ) {
this.clear_messages(); this.clear_messages();
this.clear_pulldowns(); this.clear_pulldowns();
@ -2790,7 +2819,7 @@ Wiki.prototype.start_notebook_rename = function () {
"form", { "id": "rename_form" }, notebook_name_field, ok_button "form", { "id": "rename_form" }, notebook_name_field, ok_button
); );
replaceChildNodes( "notebook_header_area", rename_form ); replaceChildNodes( "notebook_header_name", rename_form );
var self = this; var self = this;
connect( rename_form, "onsubmit", function ( event ) { connect( rename_form, "onsubmit", function ( event ) {
@ -2801,6 +2830,9 @@ Wiki.prototype.start_notebook_rename = function () {
self.end_notebook_rename(); self.end_notebook_rename();
event.stop(); event.stop();
} ); } );
connect( notebook_name_field, "onclick", function ( event ) {
event.stop();
} );
notebook_name_field.focus(); notebook_name_field.focus();
notebook_name_field.select(); notebook_name_field.select();
@ -2971,11 +3003,15 @@ function Pulldown( wiki, notebook_id, pulldown_id, anchor, relative_to, ephemera
if ( this.ephemeral ) { if ( this.ephemeral ) {
// when the mouse cursor is moved into the pulldown, it becomes non-ephemeral (in other words, // when the mouse cursor is moved into the pulldown, it becomes non-ephemeral (in other words,
// it will no longer disappear in a few seconds) // it will no longer disappear in a few seconds). but as soon as the mouse leaves, it becomes
// ephemeral again
var self = this; var self = this;
connect( this.div, "onmouseover", function ( event ) { connect( this.div, "onmouseover", function ( event ) {
self.ephemeral = false; self.ephemeral = false;
} ); } );
connect( this.div, "onmouseout", function ( event ) {
self.ephemeral = true;
} );
} }
} }
@ -3051,6 +3087,13 @@ function calculate_position( node, anchor, relative_to, always_left_align ) {
Pulldown.prototype.update_position = function ( always_left_align ) { Pulldown.prototype.update_position = function ( always_left_align ) {
var position = calculate_position( this.div, this.anchor, this.relative_to, always_left_align ); var position = calculate_position( this.div, this.anchor, this.relative_to, always_left_align );
setElementPosition( this.div, position ); setElementPosition( this.div, position );
var div_height = getElementDimensions( this.div ).h;
var viewport_bottom = getViewportPosition().y + getViewportDimensions().h;
// if the pulldown is now partially off the bottom of the window, move it up until it isn't
if ( position.y + div_height > viewport_bottom )
new Move( this.div, { "x": position.x, "y": viewport_bottom - div_height, "mode": "absolute", "duration": 0.25 } );
} }
Pulldown.prototype.shutdown = function () { Pulldown.prototype.shutdown = function () {
@ -3302,10 +3345,16 @@ Link_pulldown.prototype.display_summary = function ( title, summary ) {
} }
Link_pulldown.prototype.title_field_clicked = function ( event ) { Link_pulldown.prototype.title_field_clicked = function ( event ) {
disconnectAll( this.div );
this.ephemeral = false;
event.stop(); event.stop();
} }
Link_pulldown.prototype.title_field_focused = function ( event ) { Link_pulldown.prototype.title_field_focused = function ( event ) {
disconnectAll( this.div );
this.ephemeral = false;
this.title_field.select(); this.title_field.select();
} }
@ -4184,6 +4233,89 @@ Suggest_pulldown.prototype.shutdown = function () {
} }
function Font_pulldown( wiki, notebook_id, invoker, anchor, editor ) {
anchor.pulldown = this;
this.anchor = anchor;
this.editor = editor;
this.initial_selected_mark = null;
Pulldown.call( this, wiki, notebook_id, "font_pulldown", anchor );
this.invoker = invoker;
var fonts = [
[ "Arial", "arial,sans-serif" ],
[ "Times New Roman", "times new roman,serif" ],
[ "Courier", "courier new,monospace" ],
[ "Comic Sans", "comic sans ms,sans-serif" ],
[ "Garamond", "garamond,serif" ],
[ "Georgia", "georgia,serif" ],
[ "Tahoma", "tahoma,sans-serif" ],
[ "Trebuchet", "trebuchet ms,sans-serif" ],
[ "Verdana", "verdana,sans-serif" ]
];
var self = this;
var current_font_family = editor.query_command_value( "fontname" );
if ( current_font_family ) {
current_font_family = current_font_family.toLowerCase();
current_font_family = current_font_family.replace( /'/g, "" ).replace( /-webkit-/, "" );
current_font_family = current_font_family.split( ',' )[ 0 ];
}
for ( var i in fonts ) {
var font = fonts[ i ];
var font_name = font[ 0 ];
var font_family = font[ 1 ];
// using a button here instead of a <label> to make IE happy: when a <label> is used, clicking
// on the label steals focus from the editor iframe and prevents the font from being changed
var label = createDOM( "input", { "type": "button", "value": font_name, "class": "pulldown_label font_label_button", "style": "font-family: " + font_family + ";" } );
var selected_mark_char = document.createTextNode( "\u25cf" );
if ( current_font_family && font_family.search( current_font_family ) == 0 ) {
var selected_mark = createDOM( "span", { "class": "selected_mark" }, selected_mark_char );
this.initial_selected_mark = selected_mark;
} else {
var selected_mark = createDOM( "span", { "class": "selected_mark invisible" }, selected_mark_char );
}
var div = createDOM( "div", {}, selected_mark, " ", label );
label.font_family = font_family;
label.selected_mark = selected_mark;
appendChildNodes( this.div, div );
connect( label, "onclick", function ( event ) { self.font_name_clicked( event ); } );
}
Pulldown.prototype.finish_init.call( this );
}
Font_pulldown.prototype = new function () { this.prototype = Pulldown.prototype; };
Font_pulldown.prototype.constructor = Font_pulldown;
Font_pulldown.prototype.font_name_clicked = function ( event ) {
var label = event.src();
if ( this.initial_selected_mark )
addElementClass( this.initial_selected_mark, "invisible" );
removeElementClass( label.selected_mark, "invisible" );
var self = this;
setTimeout( function () {
self.editor.focus();
self.editor.exec_command( "fontname", label.font_family );
self.shutdown();
}, 100 );
}
Font_pulldown.prototype.shutdown = function () {
Pulldown.prototype.shutdown.call( this );
this.anchor.pulldown = null;
disconnectAll( this );
}
function Note_tree( wiki, notebook_id, invoker ) { function Note_tree( wiki, notebook_id, invoker ) {
this.wiki = wiki; this.wiki = wiki;
this.notebook_id = notebook_id; this.notebook_id = notebook_id;

View File

@ -31,9 +31,8 @@ class Download_page( Product_page ):
), ),
), ),
P( P(
""" u"Install Luminotes on your computer.",
Install Luminotes on your computer. 60-day money-back guarantee. Span( u"60-day money-back guarantee.", class_ = u"lighter_text" ),
""",
class_ = u"upgrade_subtitle", class_ = u"upgrade_subtitle",
), ),
Div( Div(
@ -60,167 +59,66 @@ class Download_page( Product_page ):
P( P(
Table( Table(
Tr( Tr(
Th( u"&nbsp;" ),
Th( Th(
u"Luminotes Desktop", Span( u"Luminotes Desktop", class_ = u"plan_name" ),
Div( Div(
A( "version", VERSION, href = news_url ), A( "version", VERSION, href = news_url ),
class_ = u"version_text", class_ = u"version_text",
), ),
class_ = u"plan_name_area download_plan_width",
)
),
Tr( Td(), class_ = u"spacer_row" ),
Tr(
Td(
Span( u"Solo", class_ = u"highlight" ), u"note taking",
title = u"Luminotes Desktop is designed for individuals.",
class_ = u"feature_value",
),
),
Tr(
Td(
u"Runs on your", Span( u"own computer", class_ = u"highlight" ),
title = u"All of your notes are stored privately on your own computer or on a USB drive.",
class_ = u"feature_value",
),
),
Tr(
Td(
Span( u"Unlimited", class_ = u"highlight" ), u"storage",
title = u"Add as many notes, documents, and files as you want.",
class_ = u"feature_value",
),
),
Tr(
Td(
u"Works", Span( "offline", class_ = u"highlight" ),
title = u"Take notes in meetings, in class, or while on the go. Runs in a web browser, but doesn't need an internet connection.",
class_ = u"feature_value",
),
),
Tr( Td(), class_ = u"spacer_row" ),
Tr(
Td(
u"Windows XP/Vista,", A( u"Linux source", href = u"/source_code" ),
class_ = u"small_text",
),
),
Tr(
Td(
u"Firefox, Internet Explorer, Chrome, Safari",
class_ = u"small_text",
),
),
Tr(
Td(
Div( Div(
download_button, download_button,
class_ = u"download_button_area", class_ = u"download_button_area",
), ),
class_ = u"plan_name",
)
),
Tr(
Td(
A( u"Unlimited storage space", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'storage_description' ); return false;" ),
class_ = u"feature_name",
),
Td(
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ),
),
),
Tr(
Td(
Ul(
Li( u"More space for your wiki notes." ),
Li( u"More space for your documents and files." ),
),
colspan = u"2",
id = u"storage_description",
class_ = u"feature_description undisplayed",
),
),
Tr(
Td(
A( u"Unlimited wiki notebooks", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'notebooks_description' ); return false;" ),
class_ = u"feature_name",
),
Td(
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ),
),
),
Tr(
Td(
Ul(
Li( u"Create a unique notebook for each subject." ),
Li( u"Keep work and personal notebooks separate." ),
),
colspan = u"2",
id = u"notebooks_description",
class_ = u"feature_description undisplayed",
),
),
Tr(
Td(
A( u"Friendly email support", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'support_description' ); return false;" ),
class_ = u"feature_name",
),
Td(
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ),
),
),
Tr(
Td(
Ul(
Li( u"Fast email responses to your support questions. From a real live human." ),
Li( u"No waiting on hold with a call center." ),
),
colspan = u"2",
id = u"support_description",
class_ = u"feature_description undisplayed",
),
),
Tr(
Td(
A( u"Notes stored on your own computer", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'local_storage' ); return false;" ),
class_ = u"feature_name",
),
Td(
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ),
),
),
Tr(
Td(
Ul(
Li( u"All of your notes are stored privately on your own computer." ),
Li( u"You can also run Luminotes Desktop from a USB drive." ),
Li( u"A future release will support optional online syncing." ),
),
colspan = u"2",
id = u"local_storage",
class_ = u"feature_description undisplayed",
),
),
Tr(
Td(
A( u"Works without an internet connection", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'works_offline' ); return false;" ),
class_ = u"feature_name",
),
Td(
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ),
),
),
Tr(
Td(
Ul(
Li( u"Take notes in meetings, in class, or while on the go." ),
Li( u"Runs in a web browser, but no internet connection is needed." ),
Li( u'Absolutely no DRM. Does not "phone home".' ),
),
colspan = u"2",
id = u"works_offline",
class_ = u"feature_description undisplayed",
),
),
Tr(
Td(
A( u"Supported operating systems", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'supported_oses' ); return false;" ),
class_ = u"feature_name",
),
Td(
u"Windows XP/Vista, Linux",
class_ = u"small_text",
),
),
Tr(
Td(
Ul(
Li( u"Fully supports Windows XP and Windows Vista." ),
Li( u"Linux", A( u"source code", href = u"/source_code" ), "is available." ),
Li( u"Future releases will include Mac OS X support." ),
),
colspan = u"2",
id = u"supported_oses",
class_ = u"feature_description undisplayed",
),
),
Tr(
Td(
A( u"Supported web browsers", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'supported_browsers' ); return false;" ),
class_ = u"feature_name",
),
Td(
u"Firefox, Internet Explorer,", Br(),
U"Chrome, Safari",
class_ = u"small_text",
),
),
Tr(
Td(
Ul(
Li( u"Fully supports Firefox and Internet Explorer." ),
Li( u"Beta support for Chrome and Safari." ),
Li( u"Future upgrades will support Opera." ),
),
colspan = u"2",
id = u"supported_browsers",
class_ = u"feature_description undisplayed",
), ),
), ),
Tr( Td(), class_ = u"spacer_row" ),
border = u"1", border = u"1",
id = u"upgrade_table", id = u"upgrade_table",
), ),
@ -232,7 +130,7 @@ class Download_page( Product_page ):
u"Don't want to install anything? Need collaboration features? ", u"Don't want to install anything? Need collaboration features? ",
A( u"Use Luminotes online", href = u"/pricing" ), A( u"Use Luminotes online", href = u"/pricing" ),
u".", u".",
class_ = u"small_text", class_ = u"small_text luminotes_online_link_area",
separator = u"", separator = u"",
), ),
@ -321,13 +219,17 @@ class Download_page( Product_page ):
Table( Table(
Tr( Tr(
Th( Th(
u"Luminotes Desktop", Span( u"Luminotes Desktop", class_ = u"plan_name" ),
class_ = u"plan_name_area",
)
),
Tr(
Td(
Div( Div(
download_button, download_button,
class_ = u"download_button_area", class_ = u"download_button_area",
), ),
class_ = u"plan_name", ),
)
), ),
id = u"upgrade_table_small", id = u"upgrade_table_small",
), ),

View File

@ -367,6 +367,7 @@ class Front_page( Product_page ):
Span( id = u"italic_button_preload" ), Span( id = u"italic_button_preload" ),
Span( id = u"underline_button_preload" ), Span( id = u"underline_button_preload" ),
Span( id = u"strikethrough_button_preload" ), Span( id = u"strikethrough_button_preload" ),
Span( id = u"font_button_preload" ),
Span( id = u"title_button_preload" ), Span( id = u"title_button_preload" ),
Span( id = u"bullet_list_button_preload" ), Span( id = u"bullet_list_button_preload" ),
Span( id = u"numbered_list_button_preload" ), Span( id = u"numbered_list_button_preload" ),

View File

@ -18,8 +18,8 @@ class Header( Div ):
Div( Div(
u"version", VERSION, u" | ", u"version", VERSION, u" | ",
A( u"upgrade", href = u"http://luminotes.com/download?upgrade=True", target = "_new" ), u" | ", A( u"upgrade", href = u"http://luminotes.com/download?upgrade=True", target = "_new" ), u" | ",
A( u"support", href = u"http://luminotes.com/support", target = "_new" ), u" | ", A( u"community", href = u"http://luminotes.com/community", target = "_new" ), u" | ",
A( u"blog", href = u"http://luminotes.com/blog", target = "_new" ), A( u"blog", href = u"http://luminotes.com/blog/", target = "_new" ),
class_ = u"header_links", class_ = u"header_links",
), ),
class_ = u"wide_center_area", class_ = u"wide_center_area",

View File

@ -39,7 +39,7 @@ class Link_area( Div ):
( rate_plan.get( u"notebook_sharing" ) and notebook.name == u"Luminotes blog" ) and Div( ( rate_plan.get( u"notebook_sharing" ) and notebook.name == u"Luminotes blog" ) and Div(
A( A(
u"subscribe to rss", u"follow",
href = u"%s?rss" % notebook_path, href = u"%s?rss" % notebook_path,
id = u"blog_rss_link", id = u"blog_rss_link",
title = u"Subscribe to the RSS feed for the Luminotes blog.", title = u"Subscribe to the RSS feed for the Luminotes blog.",
@ -52,7 +52,7 @@ class Link_area( Div ):
class_ = u"link_area_item", class_ = u"link_area_item",
) or ( updates_path and rate_plan.get( u"notebook_sharing" ) and ( not forum_tag ) and Div( ) or ( updates_path and rate_plan.get( u"notebook_sharing" ) and ( not forum_tag ) and Div(
A( A(
u"subscribe to rss", u"follow",
href = updates_path, href = updates_path,
id = u"notebook_rss_link", id = u"notebook_rss_link",
title = u"Subscribe to the RSS feed for this %s." % notebook_word, title = u"Subscribe to the RSS feed for this %s." % notebook_word,
@ -87,13 +87,13 @@ class Link_area( Div ):
( notebook.read_write == Notebook.READ_WRITE ) and Span( ( notebook.read_write == Notebook.READ_WRITE ) and Span(
Div( Div(
A( ( notebook.name != u"trash" ) and A(
u"import", u"import",
href = u"#", href = u"#",
id = u"import_link", id = u"import_link",
title = u"Import %ss from other software into Luminotes." % note_word, title = u"Import %ss from other software into Luminotes." % note_word,
), ) or None,
u"|", ( notebook.name != u"trash" ) and u"|" or None,
A( A(
u"export", u"export",
href = u"#", href = u"#",
@ -103,27 +103,35 @@ class Link_area( Div ):
class_ = u"link_area_item", class_ = u"link_area_item",
) or None, ) or None,
( notebook.name != u"trash" ) and Div(
notebook.trash_id and A(
u"trash",
href = u"/notebooks/%s?parent_id=%s" % ( notebook.trash_id, notebook.object_id ),
id = u"trash_link",
title = u"Look here for %ss you've deleted." % note_word,
) or None,
( notebook.owner and notebook.name != u"trash" and notebook.trash_id ) and u"|" or None,
( notebook.owner and notebook.name != u"trash" ) and A(
u"delete",
href = u"#",
id = u"delete_notebook_link",
title = u"Move this %s to the trash." % notebook_word,
) or None,
class_ = u"link_area_item",
) or None,
( notebook.owner and notebook.name != u"trash" ) and Div( ( notebook.owner and notebook.name != u"trash" ) and Div(
A( A(
u"rename", u"rename",
href = u"#", href = u"#",
id = u"rename_notebook_link", id = u"rename_notebook_link",
title = u"Change the name of this %s." % notebook_word, title = u"Change the name of this %s." % notebook_word,
), ),
class_ = u"link_area_item", class_ = u"link_area_item",
) or None, ) or None,
( notebook.owner and notebook.name != u"trash" ) and Div( ( notebook.owner and notebook.name != u"trash" and
A( user.username and rate_plan.get( u"notebook_sharing" ) ) and Div(
u"delete",
href = u"#",
id = u"delete_notebook_link",
title = u"Move this %s to the trash." % notebook_word,
),
class_ = u"link_area_item",
) or None,
( notebook.owner and user.username and rate_plan.get( u"notebook_sharing" ) ) and Div(
A( A(
u"share", u"share",
href = u"#", href = u"#",
@ -133,16 +141,6 @@ class Link_area( Div ):
class_ = u"link_area_item", class_ = u"link_area_item",
) or None, ) or None,
notebook.trash_id and Div(
A(
u"trash",
href = u"/notebooks/%s?parent_id=%s" % ( notebook.trash_id, notebook.object_id ),
id = u"trash_link",
title = u"Look here for %ss you've deleted." % note_word,
),
class_ = u"link_area_item",
) or None,
( notebook.name == u"trash" ) and Rounded_div( ( notebook.name == u"trash" ) and Rounded_div(
u"trash_notebook", u"trash_notebook",
A( A(

View File

@ -231,6 +231,12 @@ class Main_page( Page ):
) or None, ) or None,
Rounded_div( Rounded_div(
( notebook.name == u"trash" ) and u"trash_notebook" or u"current_notebook", ( notebook.name == u"trash" ) and u"trash_notebook" or u"current_notebook",
parent_id and Span(
A( u"empty", href = u"/notebooks/%s" % notebook.object_id, id = u"empty_trash_link" ),
u" | ",
A( u"go back", href = u"/notebooks/%s" % parent_id ),
id = u"notebook_header_links",
) or None,
( notebook.name == u"Luminotes" and title == u"source code" ) and \ ( notebook.name == u"Luminotes" and title == u"source code" ) and \
Strong( "%s %s" % ( notebook.name, VERSION ) ) or \ Strong( "%s %s" % ( notebook.name, VERSION ) ) or \
Span( Span(
@ -238,12 +244,6 @@ class Main_page( Page ):
and Strong( notebook.name ) \ and Strong( notebook.name ) \
or Span( Strong( notebook.name ), id = u"notebook_header_name", title = "Rename this notebook." ), or Span( Strong( notebook.name ), id = u"notebook_header_name", title = "Rename this notebook." ),
), ),
parent_id and Span(
u" | ",
A( u"empty trash", href = u"/notebooks/%s" % notebook.object_id, id = u"empty_trash_link" ),
u" | ",
A( u"return to notebook", href = u"/notebooks/%s" % parent_id ),
) or None,
id = u"notebook_header_area", id = u"notebook_header_area",
corners = ( u"tl", u"tr", u"br" ), corners = ( u"tl", u"tr", u"br" ),
), ),
@ -267,6 +267,8 @@ class Main_page( Page ):
) or None, ) or None,
( forum_tag and user.username and user.username != u"anonymous" ) and \ ( forum_tag and user.username and user.username != u"anonymous" ) and \
P( u"To write a comment, click that large \"+\" button to the left. To publish your comment, click the save button.", class_ = u"small_text" ) or None, P( u"To write a comment, click that large \"+\" button to the left. To publish your comment, click the save button.", class_ = u"small_text" ) or None,
( forum_tag and ( not user.username or user.username == u"anonymous" ) ) and \
P( u"To write a comment, please login first. No account?", A( u"Sign up", href = u"/pricing" ), u"to get a free account.", class_ = "small_text" ) or None,
Page_navigation( Page_navigation(
notebook_path, len( notes ), total_notes_count, start, count, notebook_path, len( notes ), total_notes_count, start, count,
return_text = u"return to the discussion", return_text = u"return to the discussion",

View File

@ -21,13 +21,12 @@ class Product_page( Page ):
Div( Div(
Div( Div(
# TODO make into a table kinda like on the footer of change.gov?
Div( Div(
Div( Div(
Ul( Ul(
Li( u"About", class_ = u"footer_category" ), Li( u"About", class_ = u"footer_category" ),
Li( A( u"tour", href = u"/tour" ) ), Li( A( u"tour", href = u"/tour" ) ),
Li( A( u"demo", href = u"/demo" ) ), Li( A( u"demo", href = u"/users/demo" ) ),
Li( A( u"faq", href = u"/faq" ) ), Li( A( u"faq", href = u"/faq" ) ),
Li( A( u"team", href = u"/meet_the_team" ) ), Li( A( u"team", href = u"/meet_the_team" ) ),
Li( A( u"user guide", href = u"/guide" ) ), Li( A( u"user guide", href = u"/guide" ) ),

View File

@ -2,7 +2,7 @@ from Tags import Span, H3, P, A
class Thanks_note( Span ): class Thanks_note( Span ):
def __init__( self, rate_plan_name ): def __init__( self, rate_plan_name = None ):
Span.__init__( Span.__init__(
self, self,
H3( u"thank you" ), H3( u"thank you" ),
@ -13,9 +13,9 @@ class Thanks_note( Span ):
), ),
P( P(
u""" u"""
You are now subscribed to Luminotes %s. Please click on one of your You are now subscribed to Luminotes%s. Please click on one of your
notebooks to the right to get started with your newly upgraded wiki. notebooks to the left to get started with your newly upgraded wiki.
""" % rate_plan_name, """ % ( rate_plan_name and u" %s" % rate_plan_name or u"" ),
), ),
P( P(
u""" u"""

View File

@ -10,7 +10,7 @@ class Toolbar( Div ):
P( P(
Div( Input( Div( Input(
type = u"image", type = u"image",
id = u"newNote", title = u"new %s [ctrl-N]" % ( note_word or u"note" ), id = u"newNote", title = u"make a new %s [ctrl-M]" % ( note_word or u"note" ),
src = u"/static/images/toolbar/new_note_button.png", src = u"/static/images/toolbar/new_note_button.png",
width = u"40", height = u"40", width = u"40", height = u"40",
class_ = "image_button", class_ = "image_button",
@ -60,6 +60,13 @@ class Toolbar( Div ):
width = u"40", height = u"40", width = u"40", height = u"40",
class_ = "image_button", class_ = "image_button",
) ), ) ),
Div( Input(
type = u"image",
id = u"font", title = u"font",
src = u"/static/images/toolbar/font_button.png",
width = u"40", height = u"40",
class_ = "image_button",
) ),
Div( Input( Div( Input(
type = u"image", type = u"image",
id = u"title", title = u"title", id = u"title", title = u"title",
@ -94,7 +101,7 @@ class Toolbar( Div ):
Span( id = "italic_button_hover_preload" ), Span( id = "italic_button_hover_preload" ),
Span( id = "underline_button_hover_preload" ), Span( id = "underline_button_hover_preload" ),
Span( id = "strikethrough_button_hover_preload" ), Span( id = "strikethrough_button_hover_preload" ),
Span( id = "title_button_hover_preload" ), Span( id = "font_button_hover_preload" ),
Span( id = "bullet_list_button_hover_preload" ), Span( id = "bullet_list_button_hover_preload" ),
Span( id = "numbered_list_button_hover_preload" ), Span( id = "numbered_list_button_hover_preload" ),
@ -105,7 +112,7 @@ class Toolbar( Div ):
Span( id = "italic_button_down_hover_preload" ), Span( id = "italic_button_down_hover_preload" ),
Span( id = "underline_button_down_hover_preload" ), Span( id = "underline_button_down_hover_preload" ),
Span( id = "strikethrough_button_down_hover_preload" ), Span( id = "strikethrough_button_down_hover_preload" ),
Span( id = "title_button_down_hover_preload" ), Span( id = "font_button_down_hover_preload" ),
Span( id = "bullet_list_button_down_hover_preload" ), Span( id = "bullet_list_button_down_hover_preload" ),
Span( id = "numbered_list_button_down_hover_preload" ), Span( id = "numbered_list_button_down_hover_preload" ),
@ -116,7 +123,7 @@ class Toolbar( Div ):
Span( id = "italic_button_down_preload" ), Span( id = "italic_button_down_preload" ),
Span( id = "underline_button_down_preload" ), Span( id = "underline_button_down_preload" ),
Span( id = "strikethrough_button_down_preload" ), Span( id = "strikethrough_button_down_preload" ),
Span( id = "title_button_down_preload" ), Span( id = "font_button_down_preload" ),
Span( id = "bullet_list_button_down_preload" ), Span( id = "bullet_list_button_down_preload" ),
Span( id = "numbered_list_button_down_preload" ), Span( id = "numbered_list_button_down_preload" ),

View File

@ -3,7 +3,7 @@ from Tags import Div, H1, Img, A, P, Table, Th, Tr, Td, Li, Span, I, Br, Ul, Li,
class Upgrade_page( Product_page ): class Upgrade_page( Product_page ):
HIDDEN_PLAN_THRESHOLD = 3 FOCUSED_PLAN = 2
def __init__( self, user, notebooks, first_notebook, login_url, logout_url, rate_plan, groups, rate_plans, unsubscribe_button ): def __init__( self, user, notebooks, first_notebook, login_url, logout_url, rate_plan, groups, rate_plans, unsubscribe_button ):
MEGABYTE = 1024 * 1024 MEGABYTE = 1024 * 1024
@ -36,205 +36,48 @@ class Upgrade_page( Product_page ):
), ),
), ),
P( P(
""" ( user.rate_plan == 0 ) and u"30-day free trial on all plans." or None,
Upgrade, downgrade, or cancel anytime. 60-day money-back guarantee. Span( u"Upgrade, downgrade, or cancel anytime.", class_ = u"lighter_text" ),
""",
class_ = u"upgrade_subtitle", class_ = u"upgrade_subtitle",
), ),
P( P(
Table( Table(
self.fee_row( rate_plans, user ), self.fee_row( rate_plans, user ),
self.spacer_row( rate_plans ),
Tr( Tr(
Td(
u"Designed for",
class_ = u"feature_name",
),
[ Td( [ Td(
plan[ u"designed_for" ], ( plan[ u"included_users" ] == 1 ) and
class_ = u"feature_value" + self.displayed( index ), Span( Span( u"Single", class_ = u"highlight" ), u"user", title = u"This plan includes one user account, so it's ideal for individuals." ) or
Span( u"Up to", Span( "%s" % plan[ u"included_users" ], class_ = u"highlight" ), u"users", title = "This plan includes multiple accounts, including an admin area where you can create and manage users for your organization." ),
class_ = u"feature_value" + ( index == self.FOCUSED_PLAN and u" focused_feature_value" or u"" ),
) for ( index, plan ) in enumerate( rate_plans ) ], ) for ( index, plan ) in enumerate( rate_plans ) ],
), ),
Tr( Tr(
Td(
A( u"Included accounts", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'users_description' ); return false;" ),
class_ = u"feature_name",
),
[ Td( [ Td(
( plan[ u"included_users" ] == 1 ) and u"1 user" or "up to<br>%s users" % plan[ u"included_users" ], plan[ u"storage_quota_bytes" ] and
class_ = u"feature_value" + self.displayed( index ), Span( "%s MB" % ( plan[ u"storage_quota_bytes" ] // MEGABYTE ), class_ = u"highlight" ) or
Span( u"unlimited", class_ = u"highlight" ),
u"storage",
title = u"Storage space for your notes, documents, and files.",
class_ = u"feature_value" + ( index == self.FOCUSED_PLAN and u" focused_feature_value" or u"" ),
) for ( index, plan ) in enumerate( rate_plans ) ], ) for ( index, plan ) in enumerate( rate_plans ) ],
), ),
Tr( plan[ u"notebook_sharing"] and Tr(
Td(
Ul(
Li( u"Collaborate on a wiki with multiple people in your organization." ),
Li( u"Even collaborate with other Luminotes users beyond your included accounts." ),
Li( u"Only one subscription is necessary." ),
),
colspan = len( rate_plans ) + 1,
id = u"users_description",
class_ = u"feature_description undisplayed",
),
),
Tr(
Td(
A( u"Included storage space", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'storage_description' ); return false;" ),
class_ = u"feature_name",
),
[ Td(
plan[ u"storage_quota_bytes" ] and "%s MB" % ( plan[ u"storage_quota_bytes" ] // MEGABYTE ) or u"unlimited",
class_ = u"feature_value" + self.displayed( index ),
) for ( index, plan ) in enumerate( rate_plans ) ],
),
Tr(
Td(
Ul(
Li( u"More space for your wiki notes." ),
Li( u"More space for your documents and files." ),
Li( u"All of your users share a common pool of storage space." ),
),
colspan = len( rate_plans ) + 1,
id = u"storage_description",
class_ = u"feature_description undisplayed",
),
),
Tr(
Td(
A( u"Unlimited wiki notebooks", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'notebooks_description' ); return false;" ),
class_ = u"feature_name",
),
[ Td(
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ),
class_ = u"feature_value" + self.displayed( index ),
) for ( index, plan ) in enumerate( rate_plans ) ],
),
Tr(
Td(
Ul(
Li( u"Create a unique notebook for each subject." ),
Li( u"Keep work and personal notebooks separate." ),
),
colspan = len( rate_plans ) + 1,
id = u"notebooks_description",
class_ = u"feature_description undisplayed",
),
),
Tr(
Td(
A( u"Friendly email support", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'support_description' ); return false;" ),
class_ = u"feature_name",
),
[ Td(
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ),
class_ = u"feature_value" + self.displayed( index ),
) for ( index, plan ) in enumerate( rate_plans ) ],
),
Tr(
Td(
Ul(
Li( u"Fast email responses to your support questions. From a real live human." ),
Li( u"No waiting on hold with a call center." ),
),
colspan = len( rate_plans ) + 1,
id = u"support_description",
class_ = u"feature_description undisplayed",
),
),
Tr(
Td(
A( u"Invite people to view your wiki", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'view_description' ); return false;" ),
class_ = u"feature_name",
),
[ Td(
plan[ u"notebook_sharing" ] and
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ) or u"&nbsp",
class_ = u"feature_value" + self.displayed( index ),
) for ( index, plan ) in enumerate( rate_plans ) ],
),
Tr(
Td(
Ul(
Li( u"Invite specific people to read your wiki." ),
Li( u"Invite as many people as you want." ),
Li( u"Share only the notebooks you want to share. Keep the others private." ),
),
colspan = len( rate_plans ) + 1,
id = u"view_description",
class_ = u"feature_description undisplayed",
),
),
Tr(
Td(
A( u"Invite people to edit your wiki", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'edit_description' ); return false;" ),
class_ = u"feature_name",
),
[ Td( [ Td(
plan[ u"notebook_collaboration" ] and plan[ u"notebook_collaboration" ] and
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ) or u"&nbsp", Span( u"Invite", Span( u"editors", class_ = u"highlight" ), title = u"Invite people to collaborate on your wiki. Share only the notebooks you want. Keep the others private." ) or
class_ = u"feature_value" + self.displayed( index ), Span( u"Invite", Span( u"viewers", class_ = u"highlight" ), title = u"Invite people to view your wiki. Share only the notebooks you want. Keep the others private." ),
class_ = u"feature_value" + ( index == self.FOCUSED_PLAN and u" focused_feature_value" or u"" ),
) for ( index, plan ) in enumerate( rate_plans ) ], ) for ( index, plan ) in enumerate( rate_plans ) ],
), ) or None,
Tr( self.button_row( rate_plans, user ),
Td( self.spacer_row( rate_plans, bottom = True ),
Ul(
Li( u"Invite specific people to collaborate on your wiki." ),
Li( u"Decide who can edit and who can only view." ),
Li( u"Invite as many people as you want. They only need free Luminotes accounts." ),
Li( u"Revoke collaboration access with a single click." ),
Li( u"Share only the notebooks you want to share. Keep the others private." ),
),
colspan = len( rate_plans ) + 1,
id = u"edit_description",
class_ = u"feature_description undisplayed",
),
),
Tr(
Td(
A( u"User administration", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'admin_description' ); return false;" ),
class_ = u"feature_name",
),
[ Td(
plan[ u"user_admin" ] and
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ) or u"&nbsp",
class_ = u"feature_value" + self.displayed( index ),
) for ( index, plan ) in enumerate( rate_plans ) ],
),
Tr(
Td(
Ul(
Li( u"Manage all Luminotes accounts for your organization from one web page." ),
Li( u"Create and delete users as needed." ),
),
colspan = len( rate_plans ) + 1,
id = u"admin_description",
class_ = u"feature_description undisplayed",
),
),
border = u"1", border = u"1",
id = u"upgrade_table", id = u"upgrade_table",
), ),
class_ = u"upgrade_table_area", class_ = u"upgrade_table_area",
), ),
Script(
"""
function toggle_plans() {
var nodes = getElementsByTagAndClassName( null, "hidden_plans" );
for ( var i in nodes ) { var node = nodes[ i ]; toggleElementClass( "undisplayed", node ); }
toggleElementClass( "undisplayed", "more_plans_link" );
toggleElementClass( "undisplayed", "fewer_plans_link" );
return false;
}
""",
type = u"text/javascript",
),
Div(
A( "Show me plans for teams.", id = "more_plans_link", href = u"#", onclick = u"return toggle_plans();" ),
A( "Show me fewer plans.", id = "fewer_plans_link", class_ = u"undisplayed", href = u"#", onclick = u"return toggle_plans();" ),
class_ = u"small_text",
),
user and user.username not in ( u"anonymous", None ) and P( user and user.username not in ( u"anonymous", None ) and P(
u"You're currently subscribed to Luminotes %s." % u"You're currently subscribed to Luminotes %s." %
user_plan[ u"name" ].capitalize(), user_plan[ u"name" ].capitalize(),
@ -329,19 +172,37 @@ class Upgrade_page( Product_page ):
""", """,
class_ = u"upgrade_text", class_ = u"upgrade_text",
), ),
H4( u"How does the 30-day free trial work?", class_ = u"upgrade_question" ),
P(
"""
When you subscribe to Luminotes, your first 30 days are completely free. And if you
cancel during that period, you aren't charged a thing. During those 30 days, you have
full access to all features of your selected subscription plan. That way, you can see
whether Luminotes works for you without any sort of commitment.
""",
class_ = u"upgrade_text",
),
H4( u"Once I subscribe, can I cancel anytime?", class_ = u"upgrade_question" ), H4( u"Once I subscribe, can I cancel anytime?", class_ = u"upgrade_question" ),
P( P(
""" """
Of course. This isn't a cell phone plan. There are no contracts or cancellation fees. Of course. There are no contracts or cancellation fees. There are no hidden fees. You
You can upgrade, downgrade, or cancel your account anytime. Simply login to your can upgrade, downgrade, or cancel your account anytime. Simply login to your account
account and return to this pricing page. and return to this pricing page.
""",
class_ = u"upgrade_text",
),
P(
"""
And if you cancel during your trial period, then you are not charged anything at all.
""", """,
class_ = u"upgrade_text", class_ = u"upgrade_text",
), ),
H4( u"What is your refund policy?", class_ = u"upgrade_question" ), H4( u"What is your refund policy?", class_ = u"upgrade_question" ),
P( P(
""" """
It's this simple: Luminotes comes with a 60-day money-back guarantee. No questions asked. It's this simple: Luminotes comes with a 30-day money-back guarantee, starting from
the end of the 30-day free trial. No questions asked. So that gives you a full 60
days to see whether Luminotes meets your needs.
""", """,
class_ = u"upgrade_text", class_ = u"upgrade_text",
), ),
@ -359,24 +220,41 @@ class Upgrade_page( Product_page ):
P( P(
A( name = "yearly" ), A( name = "yearly" ),
Div(
u"Get two months free with a yearly subscription!",
class_ = u"upgrade_subtitle",
),
Table( Table(
Tr( Td( self.fee_row( rate_plans, user, yearly = True ),
u"Get two months free with a yearly subscription!", self.spacer_row( rate_plans ),
class_ = u"upgrade_subtitle",
colspan = u"%d" % len( rate_plans ),
), colspan = u"%d" % len( rate_plans ) ),
self.fee_row( rate_plans, user, include_blank = False, yearly = True ),
Tr( Tr(
[ Td( [ Td(
( plan[ u"included_users" ] == 1 ) and u"1 user" or "up to<br />%s users" % plan[ u"included_users" ], ( plan[ u"included_users" ] == 1 ) and
class_ = u"feature_value", Span( Span( u"Single", class_ = u"highlight" ), u"user", title = u"This plan includes one user account, so it's ideal for individuals." ) or
) for plan in rate_plans ], Span( u"Up to", Span( "%s" % plan[ u"included_users" ], class_ = u"highlight" ), u"users", title = "This plan includes multiple accounts, including an admin area where you can create and manage users for your organization." ),
class_ = u"feature_value" + ( index == self.FOCUSED_PLAN and u" focused_feature_value" or u"" ),
) for ( index, plan ) in enumerate( rate_plans ) ],
), ),
Tr( Tr(
[ Td( [ Td(
plan[ u"storage_quota_bytes" ] and "%s MB" % ( plan[ u"storage_quota_bytes" ] // MEGABYTE ) or u"unlimited", plan[ u"storage_quota_bytes" ] and
) for plan in rate_plans ], Span( "%s MB" % ( plan[ u"storage_quota_bytes" ] // MEGABYTE ), class_ = u"highlight" ) or
Span( u"unlimited", class_ = u"highlight" ),
u"storage",
title = u"Storage space for your notes, documents, and files.",
class_ = u"feature_value" + ( index == self.FOCUSED_PLAN and u" focused_feature_value" or u"" ),
) for ( index, plan ) in enumerate( rate_plans ) ],
), ),
plan[ u"notebook_sharing"] and Tr(
[ Td(
plan[ u"notebook_collaboration" ] and
Span( u"Invite", Span( u"editors", class_ = u"highlight" ), title = u"Invite people to collaborate on your wiki. Share only the notebooks you want. Keep the others private." ) or
Span( u"Invite", Span( u"viewers", class_ = u"highlight" ), title = u"Invite people to view your wiki. Share only the notebooks you want. Keep the others private." ),
class_ = u"feature_value" + ( index == self.FOCUSED_PLAN and u" focused_feature_value" or u"" ),
) for ( index, plan ) in enumerate( rate_plans ) ],
) or None,
self.button_row( rate_plans, user, yearly = True ),
self.spacer_row( rate_plans, bottom = True ),
border = u"1", border = u"1",
id = u"upgrade_table_small", id = u"upgrade_table_small",
), ),
@ -398,38 +276,67 @@ class Upgrade_page( Product_page ):
), ),
) )
def fee_row( self, rate_plans, user, include_blank = True, yearly = False ): def fee_row( self, rate_plans, user, yearly = False ):
return Tr( def make_fee_area( plan, index ):
include_blank and Th( u"&nbsp;" ) or None, fee_area = (
[ Th( Span( plan[ u"name" ].capitalize(), class_ = u"plan_name" ),
plan[ u"name" ].capitalize(),
plan[ u"fee" ] and Div( plan[ u"fee" ] and Div(
yearly and Span( yearly and Span(
u"$%s" % plan[ u"yearly_fee" ], u"$%s" % plan[ u"yearly_fee" ],
Span( u"/year", class_ = u"month_text" ), Span( u"/year", class_ = u"month_text" ),
class_ = u"price_text", class_ = u"price_text",
separator = u"", separator = u"",
) or Span( ) or Div(
u"$%s" % plan[ u"fee" ], u"$%s" % plan[ u"fee" ],
Span( u"/month", class_ = u"month_text" ), Span( u"/month", class_ = u"month_text" ),
class_ = u"price_text", class_ = u"price_text",
separator = u"", separator = u"",
), ),
user and user.username not in ( u"anonymous", None ) and user.rate_plan != index \ ) or Div( Div( u"No fee", class_ = u"price_text" ) ),
and ( yearly and ( plan.get( u"yearly_button" ).strip() and plan.get( u"yearly_button" ) % user.object_id or None ) or \ Div(
( plan.get( u"button" ).strip() and plan.get( u"button" ) % user.object_id or None ) ) or None, u"For", plan[ u"designed_for" ],
) or Div( Span( u"No fee", class_ = u"price_text" ) ), class_ = u"small_text",
( not user or user.username in ( u"anonymous", None ) ) and Div( ),
A( ( index == self.FOCUSED_PLAN ) and Div( u"Best value", class_ = u"focused_text highlight" ) or None,
Img( src = u"/static/images/sign_up_button.png", width = "76", height = "23" ), )
href = u"/sign_up?plan=%s&yearly=%s" % ( index, yearly ),
), # if this is a demo/guest user, then make the fee area a big link to the sign up page
class_ = u"sign_up_button_area", if not user or user.username in ( u"anonymous", None ):
) or None, fee_area = A( href = u"/sign_up?plan=%s&yearly=%s" % ( index, yearly ), *fee_area )
class_ = u"plan_name" + self.displayed( index, yearly ), else:
fee_area = Span( *fee_area )
return fee_area
return Tr(
[ Th(
make_fee_area( plan, index ),
class_ = u"plan_name_area plan_width" + ( index == self.FOCUSED_PLAN and u" focused_plan_name_area" or u"" ),
) for ( index, plan ) in enumerate( rate_plans ) ], ) for ( index, plan ) in enumerate( rate_plans ) ],
) )
@staticmethod def button_row( self, rate_plans, user, yearly = False ):
def displayed( index, yearly = False ): return Tr(
return ( index >= Upgrade_page.HIDDEN_PLAN_THRESHOLD and not yearly ) and " undisplayed hidden_plans" or "" [ Td(
Div(
# 1 = modifying an existing subscription, 0 = new subscription
user and user.username not in ( u"anonymous", None ) and user.rate_plan != index \
and ( yearly and ( plan.get( u"yearly_button" ) and plan.get( u"yearly_button" ).strip() and
plan.get( u"yearly_button" ) % ( user.object_id, user.rate_plan and 1 or 0 ) or None ) or \
( plan.get( u"button" ) and plan.get( u"button" ).strip() and
plan.get( u"button" ) % ( user.object_id, user.rate_plan and 1 or 0 ) or None ) ) or None,
( not user or user.username in ( u"anonymous", None ) ) and A(
Img( src = u"/static/images/sign_up_button.png", width = "76", height = "23" ),
href = u"/sign_up?plan=%s&yearly=%s" % ( index, yearly ),
) or None,
class_ = u"subscribe_button_area",
),
( user.rate_plan == 0 ) and Div( "30-day free trial", class_ = u"small_text" ) or None,
class_ = ( index == self.FOCUSED_PLAN and u"focused_feature_value" or u"" ),
) for ( index, plan ) in enumerate( rate_plans ) ],
)
def spacer_row( self, rate_plans, bottom = False ):
border_bottom = bottom and " focused_border_bottom" or ""
return Tr( [ Td( class_ = ( i == self.FOCUSED_PLAN and u"focused_feature_value" + border_bottom or u"spacer_row" ) ) for i in range( len( rate_plans ) ) ], class_ = u"spacer_row" )