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.
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
* Fixed a bug that prevented notes from being automatically saved in certain
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
%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
Luminotes Desktop installer prompts you to select the installation destination

View File

@ -26,7 +26,7 @@ settings = {
"session_filter.storage_type": "ram",
"session_filter.timeout": 60 * 24 * 365, # one year
"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_access_file": os.path.join( gettempdir(), "luminotes%s.log" % username_postfix ),
"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
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 ):
( year, month, day, hours, minutes, seconds, fractional_seconds ) = \
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
return datetime(

View File

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

View File

@ -212,7 +212,11 @@ class Forum( object ):
# if a single note was requested, just return that one note
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

View File

@ -463,7 +463,7 @@ class Users( object ):
)
return dict(
form = button % user_id,
form = button % ( user_id, 0 ) # 0 = new subscription, 1 = modify an existing subscription
)
@expose()
@ -1450,7 +1450,12 @@ class Users( object ):
if mc_gross and mc_gross not in ( fee, yearly_fee ):
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" )
if mc_amount3 and mc_amount3 not in ( fee, yearly_fee ):
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():
raise Payment_error( u"invalid item_name", params )
# verify period1 and period2 (should not be present)
if params.get( u"period1" ) or params.get( u"period2" ):
raise Payment_error( u"invalid period", params )
# verify period1 (free 30-day trial)
period1 = params.get( u"period1" )
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
period3 = params.get( u"period3" )
@ -1509,7 +1519,7 @@ class Users( object ):
self.__database.save( user, commit = False )
self.update_groups( user )
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
else:
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
RETRY_TIMEOUT = 15
if rate_plan is None or retry_count > RETRY_TIMEOUT:
if retry_count > RETRY_TIMEOUT:
note = Thanks_error_note()
# if the rate plan of the subscription matches the user's current rate plan, success
elif rate_plan == result[ u"user" ].rate_plan:
note = Thanks_note( self.__rate_plans[ rate_plan ][ u"name" ].capitalize() )
result[ "conversion" ] = "subscribe_%s" % rate_plan
# otherwise, display an auto-reloading "processing..." page
else:
# if a rate plan is given, display an auto-reloading "processing..." page
elif rate_plan is not None:
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[ "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"fee": 1.99,
u"yearly_fee": 19.90,
u"button": u"[subscribe here user %s!] button",
u"yearly_button": u"[yearly 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 (modify=%s)",
},
{
u"name": "extra super",
@ -91,8 +91,8 @@ class Test_controller( object ):
u"included_users": 3,
u"fee": 9.00,
u"yearly_fee": 90.00,
u"button": u"[or here user %s!] button",
u"yearly_button": u"[yearly 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 (modify=%s)",
},
],
"luminotes.download_products": [

View File

@ -66,6 +66,40 @@ class Test_database( object ):
assert obj.revision.replace( tzinfo = utc ) == original_revision
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 ):
obj = self.database.select_one( tuple, Stub_object.sql_tuple() )
@ -185,7 +219,7 @@ class Test_database( object ):
self.connection.rollback()
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 )
self.database.commit()
assert next_id

View File

@ -268,6 +268,54 @@ class Test_forums( Test_controller ):
user = self.database.load( User, self.user.object_id )
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 ):
self.login()

View File

@ -542,7 +542,7 @@ class Test_users( Test_controller ):
form = result.get( u"form" )
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 ):
self.login()
@ -555,7 +555,7 @@ class Test_users( Test_controller ):
form = result.get( u"form" )
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 ):
self.login()
@ -3351,6 +3351,43 @@ class Test_users( Test_controller ):
user = self.database.load( User, self.user.object_id )
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 ):
data = dict( self.PAYMENT_DATA )
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 ].notebook_id == self.anon_notebook.object_id
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 ):
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
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 (
id text NOT NULL,

View File

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

View File

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

View File

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

View File

@ -40,10 +40,10 @@
background-image: url(/static/images/toolbar/strikethrough_button.png);
}
#title_button_preload {
#font_button_preload {
height: 0;
overflow: hidden;
background-image: url(/static/images/toolbar/title_button.png);
background-image: url(/static/images/toolbar/font_button.png);
}
#bullet_list_button_preload {
@ -65,6 +65,8 @@
}
.hook_area {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
padding-top: 1.5em;
padding-bottom: 1.5em;
width: 100%;
@ -93,6 +95,8 @@
background-color: #ffff99;
font-weight: bold;
padding: 1em;
margin-top: 1em;
margin-bottom: 1em;
}
.hook_action {
@ -156,6 +160,8 @@
}
.thumbnail_area {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
background-color: #fffece;
padding-bottom: 0.5em;
}
@ -301,6 +307,10 @@
padding-bottom: 0.5em;
}
.lighter_text {
color: #267dff;
}
.upgrade_question {
text-align: left;
}
@ -322,6 +332,12 @@
.upgrade_table_area {
text-align: center;
margin-bottom: 1em;
}
.luminotes_online_link_area {
margin-top: 1em;
clear: both;
}
#upgrade_table {
@ -337,43 +353,80 @@
}
#upgrade_table th {
padding: 0.5em;
padding: 0.5em 1.5em 0.5em 1.5em;
border: 1px solid #999999;
border-bottom: 1px solid #cccccc;
}
.plan_width {
width: 20%;
}
.download_plan_width {
width: 400px;
}
#upgrade_table td {
text-align: center;
background-color: #fafafa;
padding: 0.5em;
border: 1px solid #999999;
padding: 0.25em;
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 {
text-align: center;
background-color: #d0e0f0;
font-size: 125%;
}
#upgrade_table ul {
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 {
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 {
@ -386,26 +439,68 @@
}
#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 {
text-align: center;
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;
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 {
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 {
padding-top: 0.5em;
padding-top: 1em;
padding-bottom: 0.5em;
}
.yearly_link {
@ -413,7 +508,8 @@
}
.price_text {
color: #ff6600;
margin-top: 0.25em;
color: #267dff;
}
.month_text {
@ -422,7 +518,7 @@
}
.version_text {
padding-top: 0.25em;
padding-top: 0.5em;
font-size: 72%;
font-weight: normal;
}
@ -432,10 +528,6 @@
margin-bottom: 0;
}
.sign_up_button_area {
margin-top: 0.5em;
}
.thumbnail_left {
float: left;
margin: 0.5em;

View File

@ -107,10 +107,10 @@ h1 {
background-image: url(/static/images/toolbar/strikethrough_button_hover.png);
}
#title_button_hover_preload {
#font_button_hover_preload {
height: 0;
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 {
@ -167,10 +167,10 @@ h1 {
background-image: url(/static/images/toolbar/strikethrough_button_down_hover.png);
}
#title_button_down_hover_preload {
#font_button_down_hover_preload {
height: 0;
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 {
@ -227,10 +227,10 @@ h1 {
background-image: url(/static/images/toolbar/strikethrough_button_down.png);
}
#title_button_down_preload {
#font_button_down_preload {
height: 0;
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 {
@ -427,6 +427,12 @@ h1 {
color: #ff6600;
}
#notebook_header_links {
position: absolute;
top: 1.7em;
right: 1em;
}
#rename_form {
margin: 0;
}
@ -493,6 +499,9 @@ h1 {
}
.note_button {
-moz-border-radius-topleft: 4px;
-moz-border-radius-topright: 4px;
-webkit-border-radius: 4px;
border-style: outset;
border-width: 0px;
background-color: #d0e0f0;
@ -511,9 +520,12 @@ h1 {
#save_button {
margin-left: 0.5em;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
}
.note_frame {
-moz-border-radius: 5px;
margin: 0em;
padding: 0em;
overflow: hidden;
@ -560,6 +572,7 @@ h1 {
}
.pulldown {
-moz-border-radius: 4px;
position: absolute;
font-size: 72%;
text-align: left;
@ -581,6 +594,10 @@ h1 {
text-decoration: none;
}
.selected_mark {
vertical-align: top;
}
.suggestion {
padding: 0.25em 0.5em 0.25em 0.5em;
}
@ -590,16 +607,31 @@ h1 {
}
.pulldown_label {
-moz-user-select: none;
color: #000000;
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 {
color: #ff6600;
cursor: pointer;
}
.message {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
padding: 0.5em;
margin-bottom: 0.5em;
font-weight: bold;
@ -607,12 +639,16 @@ h1 {
}
.message_inner {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
padding: 0.5em;
line-height: 140%;
background-color: #ffaa44;
}
.error {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
padding: 0.5em;
border: 1px solid #550000;
margin-bottom: 0.5em;
@ -622,6 +658,8 @@ h1 {
}
.error_inner {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
padding: 0.5em;
line-height: 140%;
color: #ffffff;
@ -629,6 +667,8 @@ h1 {
}
.message_button {
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
margin-left: 0.5em;
border-style: outset;
border-width: 0px;
@ -676,7 +716,7 @@ h1 {
.link_area_item {
font-size: 75%;
padding: 0.25em 0.25em 0.25em 0.5em;
padding: 0.15em 0.25em 0.15em 0.5em;
}
.note_tree_item {
@ -872,6 +912,8 @@ h1 {
}
.hook_action_area {
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
background-color: #ffff99;
font-weight: bold;
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
Editor.prototype.resize = function () {
if ( !this.document ) return;
@ -437,10 +441,14 @@ Editor.prototype.key_released = function ( event ) {
Editor.prototype.cleanup_html = function ( key_code ) {
if ( WEBKIT ) {
// 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" ) )
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
// https://bugs.webkit.org/show_bug.cgi?id=13490
// 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;
var replacement = withDocument( this.document, function () {
if ( style == undefined )
return createDOM( "span" );
// font-size is set when ending title mode
if ( style.indexOf( "font-size: " ) != -1 )
return null;
@ -712,7 +718,9 @@ Editor.prototype.end_link = function () {
// end of the link if it's not already there
if ( link && WEBKIT ) {
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
// 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( "underline", "onclick", function ( event ) { self.toggle_button( event, "underline" ); } );
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( "insertUnorderedList", "onclick", function ( event ) { self.toggle_button( event, "insertUnorderedList" ); } );
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( "underline" );
this.make_image_button( "strikethrough" );
this.make_image_button( "font" );
this.make_image_button( "title" );
this.make_image_button( "insertUnorderedList", "bullet_list" );
this.make_image_button( "insertOrderedList", "numbered_list" );
@ -1069,8 +1071,8 @@ Wiki.prototype.key_pressed = function ( event ) {
var code = event.key().code;
if ( event.modifier().ctrl ) {
// ctrl-n: new note
if ( code == 78 )
// ctrl-m: make a new note
if ( code == 77 )
this.create_blank_editor( event );
}
}
@ -1112,8 +1114,8 @@ Wiki.prototype.editor_key_pressed = function ( editor, event ) {
// ctrl-l: link
} else if ( code == 76 ) {
this.toggle_link_button( event );
// ctrl-n: new note
} else if ( code == 78 ) {
// ctrl-m: make a new note
} else if ( code == 77 ) {
this.create_blank_editor( event );
// ctrl-h: hide note
} else if ( code == 72 ) {
@ -1150,8 +1152,10 @@ Wiki.prototype.editor_key_pressed = function ( editor, event ) {
} else if ( code == 8 && editor.document.selection ) {
var range = editor.document.selection.createRange();
range.moveStart( "character", -1 );
range.text = "";
event.stop();
if ( range.text != "" ) {
range.text = "";
event.stop();
}
}
}
@ -1330,6 +1334,7 @@ Wiki.prototype.update_toolbar = function() {
this.update_button( "italic", "i", node_names );
this.update_button( "underline", "u", 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( "insertUnorderedList", "ul", node_names );
this.update_button( "insertOrderedList", "ol", node_names );
@ -1405,6 +1410,30 @@ Wiki.prototype.toggle_attach_button = function ( event ) {
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 ) {
this.clear_messages();
this.clear_pulldowns();
@ -2790,7 +2819,7 @@ Wiki.prototype.start_notebook_rename = function () {
"form", { "id": "rename_form" }, notebook_name_field, ok_button
);
replaceChildNodes( "notebook_header_area", rename_form );
replaceChildNodes( "notebook_header_name", rename_form );
var self = this;
connect( rename_form, "onsubmit", function ( event ) {
@ -2801,6 +2830,9 @@ Wiki.prototype.start_notebook_rename = function () {
self.end_notebook_rename();
event.stop();
} );
connect( notebook_name_field, "onclick", function ( event ) {
event.stop();
} );
notebook_name_field.focus();
notebook_name_field.select();
@ -2971,11 +3003,15 @@ function Pulldown( wiki, notebook_id, pulldown_id, anchor, relative_to, ephemera
if ( this.ephemeral ) {
// 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;
connect( this.div, "onmouseover", function ( event ) {
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 ) {
var position = calculate_position( this.div, this.anchor, this.relative_to, always_left_align );
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 () {
@ -3302,10 +3345,16 @@ Link_pulldown.prototype.display_summary = function ( title, summary ) {
}
Link_pulldown.prototype.title_field_clicked = function ( event ) {
disconnectAll( this.div );
this.ephemeral = false;
event.stop();
}
Link_pulldown.prototype.title_field_focused = function ( event ) {
disconnectAll( this.div );
this.ephemeral = false;
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 ) {
this.wiki = wiki;
this.notebook_id = notebook_id;

View File

@ -31,9 +31,8 @@ class Download_page( Product_page ):
),
),
P(
"""
Install Luminotes on your computer. 60-day money-back guarantee.
""",
u"Install Luminotes on your computer.",
Span( u"60-day money-back guarantee.", class_ = u"lighter_text" ),
class_ = u"upgrade_subtitle",
),
Div(
@ -60,167 +59,66 @@ class Download_page( Product_page ):
P(
Table(
Tr(
Th( u"&nbsp;" ),
Th(
u"Luminotes Desktop",
Span( u"Luminotes Desktop", class_ = u"plan_name" ),
Div(
A( "version", VERSION, href = news_url ),
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(
download_button,
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",
id = u"upgrade_table",
),
@ -232,7 +130,7 @@ class Download_page( Product_page ):
u"Don't want to install anything? Need collaboration features? ",
A( u"Use Luminotes online", href = u"/pricing" ),
u".",
class_ = u"small_text",
class_ = u"small_text luminotes_online_link_area",
separator = u"",
),
@ -321,13 +219,17 @@ class Download_page( Product_page ):
Table(
Tr(
Th(
u"Luminotes Desktop",
Span( u"Luminotes Desktop", class_ = u"plan_name" ),
class_ = u"plan_name_area",
)
),
Tr(
Td(
Div(
download_button,
class_ = u"download_button_area",
),
class_ = u"plan_name",
)
),
),
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"underline_button_preload" ),
Span( id = u"strikethrough_button_preload" ),
Span( id = u"font_button_preload" ),
Span( id = u"title_button_preload" ),
Span( id = u"bullet_list_button_preload" ),
Span( id = u"numbered_list_button_preload" ),

View File

@ -18,8 +18,8 @@ class Header( Div ):
Div(
u"version", VERSION, 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"blog", href = u"http://luminotes.com/blog", target = "_new" ),
A( u"community", href = u"http://luminotes.com/community", target = "_new" ), u" | ",
A( u"blog", href = u"http://luminotes.com/blog/", target = "_new" ),
class_ = u"header_links",
),
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(
A(
u"subscribe to rss",
u"follow",
href = u"%s?rss" % notebook_path,
id = u"blog_rss_link",
title = u"Subscribe to the RSS feed for the Luminotes blog.",
@ -52,7 +52,7 @@ class Link_area( Div ):
class_ = u"link_area_item",
) or ( updates_path and rate_plan.get( u"notebook_sharing" ) and ( not forum_tag ) and Div(
A(
u"subscribe to rss",
u"follow",
href = updates_path,
id = u"notebook_rss_link",
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(
Div(
A(
( notebook.name != u"trash" ) and A(
u"import",
href = u"#",
id = u"import_link",
title = u"Import %ss from other software into Luminotes." % note_word,
),
u"|",
) or None,
( notebook.name != u"trash" ) and u"|" or None,
A(
u"export",
href = u"#",
@ -103,27 +103,35 @@ class Link_area( Div ):
class_ = u"link_area_item",
) 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(
A(
u"rename",
href = u"#",
id = u"rename_notebook_link",
title = u"Change the name of this %s." % notebook_word,
),
class_ = u"link_area_item",
),
class_ = u"link_area_item",
) or None,
( notebook.owner and notebook.name != u"trash" ) and Div(
A(
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(
( notebook.owner and notebook.name != u"trash" and
user.username and rate_plan.get( u"notebook_sharing" ) ) and Div(
A(
u"share",
href = u"#",
@ -133,16 +141,6 @@ class Link_area( Div ):
class_ = u"link_area_item",
) 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(
u"trash_notebook",
A(

View File

@ -231,6 +231,12 @@ class Main_page( Page ):
) or None,
Rounded_div(
( 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 \
Strong( "%s %s" % ( notebook.name, VERSION ) ) or \
Span(
@ -238,12 +244,6 @@ class Main_page( Page ):
and Strong( notebook.name ) \
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",
corners = ( u"tl", u"tr", u"br" ),
),
@ -267,6 +267,8 @@ class Main_page( Page ):
) or None,
( 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,
( 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(
notebook_path, len( notes ), total_notes_count, start, count,
return_text = u"return to the discussion",

View File

@ -21,13 +21,12 @@ class Product_page( Page ):
Div(
Div(
# TODO make into a table kinda like on the footer of change.gov?
Div(
Div(
Ul(
Li( u"About", class_ = u"footer_category" ),
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"team", href = u"/meet_the_team" ) ),
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 ):
def __init__( self, rate_plan_name ):
def __init__( self, rate_plan_name = None ):
Span.__init__(
self,
H3( u"thank you" ),
@ -13,9 +13,9 @@ class Thanks_note( Span ):
),
P(
u"""
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.
""" % rate_plan_name,
You are now subscribed to Luminotes%s. Please click on one of your
notebooks to the left to get started with your newly upgraded wiki.
""" % ( rate_plan_name and u" %s" % rate_plan_name or u"" ),
),
P(
u"""

View File

@ -10,7 +10,7 @@ class Toolbar( Div ):
P(
Div( Input(
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",
width = u"40", height = u"40",
class_ = "image_button",
@ -60,6 +60,13 @@ class Toolbar( Div ):
width = u"40", height = u"40",
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(
type = u"image",
id = u"title", title = u"title",
@ -94,7 +101,7 @@ class Toolbar( Div ):
Span( id = "italic_button_hover_preload" ),
Span( id = "underline_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 = "numbered_list_button_hover_preload" ),
@ -105,7 +112,7 @@ class Toolbar( Div ):
Span( id = "italic_button_down_hover_preload" ),
Span( id = "underline_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 = "numbered_list_button_down_hover_preload" ),
@ -116,7 +123,7 @@ class Toolbar( Div ):
Span( id = "italic_button_down_preload" ),
Span( id = "underline_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 = "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 ):
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 ):
MEGABYTE = 1024 * 1024
@ -36,205 +36,48 @@ class Upgrade_page( Product_page ):
),
),
P(
"""
Upgrade, downgrade, or cancel anytime. 60-day money-back guarantee.
""",
( user.rate_plan == 0 ) and u"30-day free trial on all plans." or None,
Span( u"Upgrade, downgrade, or cancel anytime.", class_ = u"lighter_text" ),
class_ = u"upgrade_subtitle",
),
P(
Table(
self.fee_row( rate_plans, user ),
self.spacer_row( rate_plans ),
Tr(
Td(
u"Designed for",
class_ = u"feature_name",
),
[ Td(
plan[ u"designed_for" ],
class_ = u"feature_value" + self.displayed( index ),
( plan[ u"included_users" ] == 1 ) and
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 ) ],
),
Tr(
Td(
A( u"Included accounts", href = u"#", onclick = u"toggleElementClass( 'undisplayed', 'users_description' ); return false;" ),
class_ = u"feature_name",
),
[ Td(
( plan[ u"included_users" ] == 1 ) and u"1 user" or "up to<br>%s users" % plan[ u"included_users" ],
class_ = u"feature_value" + self.displayed( index ),
plan[ u"storage_quota_bytes" ] and
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 ) ],
),
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",
),
plan[ u"notebook_sharing"] and Tr(
[ Td(
plan[ u"notebook_collaboration" ] and
Img( src = u"/static/images/check.png", width = u"22", height = u"22" ) or u"&nbsp",
class_ = u"feature_value" + self.displayed( index ),
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 ) ],
),
Tr(
Td(
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",
),
),
) or None,
self.button_row( rate_plans, user ),
self.spacer_row( rate_plans, bottom = True ),
border = u"1",
id = u"upgrade_table",
),
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(
u"You're currently subscribed to Luminotes %s." %
user_plan[ u"name" ].capitalize(),
@ -329,19 +172,37 @@ class Upgrade_page( Product_page ):
""",
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" ),
P(
"""
Of course. This isn't a cell phone plan. There are no contracts or cancellation fees.
You can upgrade, downgrade, or cancel your account anytime. Simply login to your
account and return to this pricing page.
Of course. There are no contracts or cancellation fees. There are no hidden fees. You
can upgrade, downgrade, or cancel your account anytime. Simply login to your account
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",
),
H4( u"What is your refund policy?", class_ = u"upgrade_question" ),
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",
),
@ -359,24 +220,41 @@ class Upgrade_page( Product_page ):
P(
A( name = "yearly" ),
Div(
u"Get two months free with a yearly subscription!",
class_ = u"upgrade_subtitle",
),
Table(
Tr( Td(
u"Get two months free with a yearly subscription!",
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 ),
self.fee_row( rate_plans, user, yearly = True ),
self.spacer_row( rate_plans ),
Tr(
[ Td(
( plan[ u"included_users" ] == 1 ) and u"1 user" or "up to<br />%s users" % plan[ u"included_users" ],
class_ = u"feature_value",
) for plan in rate_plans ],
( plan[ u"included_users" ] == 1 ) and
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 ) ],
),
Tr(
[ Td(
plan[ u"storage_quota_bytes" ] and "%s MB" % ( plan[ u"storage_quota_bytes" ] // MEGABYTE ) or u"unlimited",
) for plan in rate_plans ],
plan[ u"storage_quota_bytes" ] and
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",
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 ):
return Tr(
include_blank and Th( u"&nbsp;" ) or None,
[ Th(
plan[ u"name" ].capitalize(),
def fee_row( self, rate_plans, user, yearly = False ):
def make_fee_area( plan, index ):
fee_area = (
Span( plan[ u"name" ].capitalize(), class_ = u"plan_name" ),
plan[ u"fee" ] and Div(
yearly and Span(
u"$%s" % plan[ u"yearly_fee" ],
Span( u"/year", class_ = u"month_text" ),
class_ = u"price_text",
separator = u"",
) or Span(
) or Div(
u"$%s" % plan[ u"fee" ],
Span( u"/month", class_ = u"month_text" ),
class_ = u"price_text",
separator = u"",
),
user and user.username not in ( u"anonymous", None ) and user.rate_plan != index \
and ( yearly and ( plan.get( u"yearly_button" ).strip() and plan.get( u"yearly_button" ) % user.object_id or None ) or \
( plan.get( u"button" ).strip() and plan.get( u"button" ) % user.object_id or None ) ) or None,
) or Div( Span( u"No fee", class_ = u"price_text" ) ),
( not user or user.username in ( u"anonymous", None ) ) and Div(
A(
Img( src = u"/static/images/sign_up_button.png", width = "76", height = "23" ),
href = u"/sign_up?plan=%s&yearly=%s" % ( index, yearly ),
),
class_ = u"sign_up_button_area",
) or None,
class_ = u"plan_name" + self.displayed( index, yearly ),
) or Div( Div( u"No fee", class_ = u"price_text" ) ),
Div(
u"For", plan[ u"designed_for" ],
class_ = u"small_text",
),
( index == self.FOCUSED_PLAN ) and Div( u"Best value", class_ = u"focused_text highlight" ) or None,
)
# if this is a demo/guest user, then make the fee area a big link to the sign up page
if not user or user.username in ( u"anonymous", None ):
fee_area = A( href = u"/sign_up?plan=%s&yearly=%s" % ( index, yearly ), *fee_area )
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 ) ],
)
@staticmethod
def displayed( index, yearly = False ):
return ( index >= Upgrade_page.HIDDEN_PLAN_THRESHOLD and not yearly ) and " undisplayed hidden_plans" or ""
def button_row( self, rate_plans, user, yearly = False ):
return Tr(
[ 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" )