Lots more work on the payment code necessary to support Luminotes Desktop.
This commit is contained in:
parent
1ae3596f3d
commit
9247683a72
10
NEWS
10
NEWS
|
@ -1,5 +1,13 @@
|
||||||
1.5.0 beta 2:
|
1.5.0:
|
||||||
|
* Fixed a Luminotes Desktop Internet Explorer bug in which note links within
|
||||||
|
the "download as html" document pointed to notes in the local Luminotes
|
||||||
|
installation instead of notes within the stand-alone document.
|
||||||
|
* Fixed a bug in which Luminotes Desktop file attachment did not always work
|
||||||
|
due to incorrect upload progress reporting.
|
||||||
|
* In the revision changes pulldown, no longer showing "by desktopuser" in
|
||||||
|
Luminotes Desktop.
|
||||||
* Added a Luminotes Desktop download page.
|
* Added a Luminotes Desktop download page.
|
||||||
|
* Added code for supporting product download access.
|
||||||
|
|
||||||
1.5.0 beta 1: August 27, 2008
|
1.5.0 beta 1: August 27, 2008
|
||||||
* Completed the Luminotes Desktop Windows installer.
|
* Completed the Luminotes Desktop Windows installer.
|
||||||
|
|
|
@ -107,12 +107,26 @@ settings = {
|
||||||
""",
|
""",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"luminotes.download_products": [
|
||||||
|
{
|
||||||
|
"name": "Luminotes Desktop",
|
||||||
|
"designed_for": "individuals",
|
||||||
|
"storage_quota_bytes": None,
|
||||||
|
"included_users": 1,
|
||||||
|
"notebook_sharing": False,
|
||||||
|
"notebook_collaboration": False,
|
||||||
|
"user_admin": False,
|
||||||
|
"fee": "20.00",
|
||||||
|
"item_number": "5000",
|
||||||
|
"filename": "luminotes.exe",
|
||||||
|
"button":
|
||||||
|
"""
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
],
|
||||||
"luminotes.unsubscribe_button":
|
"luminotes.unsubscribe_button":
|
||||||
"""
|
"""
|
||||||
""",
|
""",
|
||||||
"luminotes.download_button":
|
|
||||||
"""
|
|
||||||
""",
|
|
||||||
},
|
},
|
||||||
"/files/download": {
|
"/files/download": {
|
||||||
"stream_response": True,
|
"stream_response": True,
|
||||||
|
|
|
@ -18,6 +18,7 @@ from Users import grab_user_id, Access_error
|
||||||
from Expire import strongly_expire
|
from Expire import strongly_expire
|
||||||
from model.File import File
|
from model.File import File
|
||||||
from model.User import User
|
from model.User import User
|
||||||
|
from model.Download_access import Download_access
|
||||||
from view.Upload_page import Upload_page
|
from view.Upload_page import Upload_page
|
||||||
from view.Blank_page import Blank_page
|
from view.Blank_page import Blank_page
|
||||||
from view.Json import Json
|
from view.Json import Json
|
||||||
|
@ -249,7 +250,7 @@ class Files( object ):
|
||||||
"""
|
"""
|
||||||
Controller for dealing with uploaded files, corresponding to the "/files" URL.
|
Controller for dealing with uploaded files, corresponding to the "/files" URL.
|
||||||
"""
|
"""
|
||||||
def __init__( self, database, users ):
|
def __init__( self, database, users, download_products ):
|
||||||
"""
|
"""
|
||||||
Create a new Files object.
|
Create a new Files object.
|
||||||
|
|
||||||
|
@ -257,11 +258,14 @@ class Files( object ):
|
||||||
@param database: database that file metadata is stored in
|
@param database: database that file metadata is stored in
|
||||||
@type users: controller.Users
|
@type users: controller.Users
|
||||||
@param users: controller for all users
|
@param users: controller for all users
|
||||||
|
@type download_products: [ { "name": unicode, ... } ]
|
||||||
|
@param download_products: list of configured downloadable products
|
||||||
@rtype: Files
|
@rtype: Files
|
||||||
@return: newly constructed Files
|
@return: newly constructed Files
|
||||||
"""
|
"""
|
||||||
self.__database = database
|
self.__database = database
|
||||||
self.__users = users
|
self.__users = users
|
||||||
|
self.__download_products = download_products
|
||||||
|
|
||||||
@expose()
|
@expose()
|
||||||
@end_transaction
|
@end_transaction
|
||||||
|
@ -331,6 +335,68 @@ class Files( object ):
|
||||||
|
|
||||||
return stream()
|
return stream()
|
||||||
|
|
||||||
|
@expose()
|
||||||
|
@end_transaction
|
||||||
|
@validate(
|
||||||
|
access_id = Valid_id(),
|
||||||
|
item_number = Valid_int(),
|
||||||
|
)
|
||||||
|
def download_product( self, access_id, item_number ):
|
||||||
|
"""
|
||||||
|
Return the contents of downloadable product file.
|
||||||
|
|
||||||
|
@type access_id: unicode
|
||||||
|
@param access_id: id of download access object that grants access to the file
|
||||||
|
@type item_number: int or int as unicode
|
||||||
|
@param item_number: number of the downloadable product
|
||||||
|
@rtype: generator
|
||||||
|
@return: file data
|
||||||
|
@raise Access_error: the access_id is unknown, doesn't grant access to the file, or the
|
||||||
|
item_number is unknown
|
||||||
|
"""
|
||||||
|
# release the session lock before beginning to stream the download. otherwise, if the
|
||||||
|
# download is cancelled before it's done, the lock won't be released
|
||||||
|
try:
|
||||||
|
cherrypy.session.release_lock()
|
||||||
|
except ( KeyError, OSError ):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# find the product corresponding to the given item_number
|
||||||
|
products = [
|
||||||
|
product for product in self.__download_products
|
||||||
|
if unicode( item_number ) == product.get( u"item_number" )
|
||||||
|
]
|
||||||
|
if len( products ) == 0:
|
||||||
|
raise Access_error()
|
||||||
|
|
||||||
|
product = products[ 0 ]
|
||||||
|
|
||||||
|
# load the download_access object corresponding to the given id
|
||||||
|
download_access = self.__database.load( Download_access, access_id )
|
||||||
|
if download_access is None:
|
||||||
|
raise Access_error()
|
||||||
|
|
||||||
|
public_filename = product[ u"filename" ].encode( "utf8" )
|
||||||
|
local_filename = u"products/%s" % product[ u"filename" ]
|
||||||
|
|
||||||
|
if not os.path.exists( local_filename ):
|
||||||
|
raise Access_error()
|
||||||
|
|
||||||
|
cherrypy.response.headerMap[ u"Content-Type" ] = u"application/octet-stream"
|
||||||
|
cherrypy.response.headerMap[ u"Content-Disposition" ] = 'attachment; filename="%s"' % public_filename
|
||||||
|
cherrypy.response.headerMap[ u"Content-Length" ] = os.path.getsize( local_filename )
|
||||||
|
|
||||||
|
def stream():
|
||||||
|
CHUNK_SIZE = 8192
|
||||||
|
local_file = file( local_filename, "rb" )
|
||||||
|
|
||||||
|
while True:
|
||||||
|
data = local_file.read( CHUNK_SIZE )
|
||||||
|
if len( data ) == 0: break
|
||||||
|
yield data
|
||||||
|
|
||||||
|
return stream()
|
||||||
|
|
||||||
@expose( view = File_preview_page )
|
@expose( view = File_preview_page )
|
||||||
@end_transaction
|
@end_transaction
|
||||||
@grab_user_id
|
@grab_user_id
|
||||||
|
|
|
@ -49,9 +49,14 @@ class Root( object ):
|
||||||
settings[ u"global" ].get( u"luminotes.support_email", u"" ),
|
settings[ u"global" ].get( u"luminotes.support_email", u"" ),
|
||||||
settings[ u"global" ].get( u"luminotes.payment_email", u"" ),
|
settings[ u"global" ].get( u"luminotes.payment_email", u"" ),
|
||||||
settings[ u"global" ].get( u"luminotes.rate_plans", [] ),
|
settings[ u"global" ].get( u"luminotes.rate_plans", [] ),
|
||||||
|
settings[ u"global" ].get( u"luminotes.download_products", [] ),
|
||||||
)
|
)
|
||||||
self.__groups = Groups( database, self.__users )
|
self.__groups = Groups( database, self.__users )
|
||||||
self.__files = Files( database, self.__users )
|
self.__files = Files(
|
||||||
|
database,
|
||||||
|
self.__users,
|
||||||
|
settings[ u"global" ].get( u"luminotes.download_products", [] ),
|
||||||
|
)
|
||||||
self.__notebooks = Notebooks( database, self.__users, self.__files, settings[ u"global" ].get( u"luminotes.https_url", u"" ) )
|
self.__notebooks = Notebooks( database, self.__users, self.__files, settings[ u"global" ].get( u"luminotes.https_url", u"" ) )
|
||||||
self.__forums = Forums( database, self.__users )
|
self.__forums = Forums( database, self.__users )
|
||||||
self.__suppress_exceptions = suppress_exceptions # used for unit tests
|
self.__suppress_exceptions = suppress_exceptions # used for unit tests
|
||||||
|
@ -155,6 +160,24 @@ class Root( object ):
|
||||||
redirect = u"/users/redeem_invite/%s" % invite_id,
|
redirect = u"/users/redeem_invite/%s" % invite_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@expose()
|
||||||
|
def d( self, download_access_id ):
|
||||||
|
"""
|
||||||
|
Redirect to the product download thanks URL, based on the given download access id. The sole
|
||||||
|
purpose of this method is to shorten product download URLs sent by email so email clients don't
|
||||||
|
wrap them.
|
||||||
|
"""
|
||||||
|
# if the value looks like an id, it's a download access id, so redirect
|
||||||
|
try:
|
||||||
|
validator = Valid_id()
|
||||||
|
download_access_id = validator( download_access_id )
|
||||||
|
except ValueError:
|
||||||
|
raise cherrypy.NotFound
|
||||||
|
|
||||||
|
return dict(
|
||||||
|
redirect = u"/users/download_thanks/access_id=%s" % download_access_id,
|
||||||
|
)
|
||||||
|
|
||||||
@expose( view = Front_page )
|
@expose( view = Front_page )
|
||||||
@strongly_expire
|
@strongly_expire
|
||||||
@end_transaction
|
@end_transaction
|
||||||
|
@ -350,9 +373,10 @@ class Root( object ):
|
||||||
@end_transaction
|
@end_transaction
|
||||||
@grab_user_id
|
@grab_user_id
|
||||||
@validate(
|
@validate(
|
||||||
|
upgrade = Valid_bool( none_okay = True ),
|
||||||
user_id = Valid_id( none_okay = True ),
|
user_id = Valid_id( none_okay = True ),
|
||||||
)
|
)
|
||||||
def download( self, user_id = None ):
|
def download( self, upgrade = False, user_id = None ):
|
||||||
"""
|
"""
|
||||||
Provide the information necessary to display the Luminotes download page.
|
Provide the information necessary to display the Luminotes download page.
|
||||||
"""
|
"""
|
||||||
|
@ -363,7 +387,8 @@ class Root( object ):
|
||||||
else:
|
else:
|
||||||
result[ "first_notebook" ] = None
|
result[ "first_notebook" ] = None
|
||||||
|
|
||||||
result[ "download_button" ] = self.__settings[ u"global" ].get( u"luminotes.download_button" )
|
result[ "download_products" ] = self.__settings[ u"global" ].get( u"luminotes.download_products" )
|
||||||
|
result[ "upgrade" ] = upgrade
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ import re
|
||||||
import urllib
|
import urllib
|
||||||
import urllib2
|
import urllib2
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
import smtplib
|
||||||
|
from email import Message
|
||||||
from pytz import utc
|
from pytz import utc
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from model.User import User
|
from model.User import User
|
||||||
|
@ -9,6 +11,7 @@ from model.Group import Group
|
||||||
from model.Notebook import Notebook
|
from model.Notebook import Notebook
|
||||||
from model.Note import Note
|
from model.Note import Note
|
||||||
from model.Password_reset import Password_reset
|
from model.Password_reset import Password_reset
|
||||||
|
from model.Download_access import Download_access
|
||||||
from model.Invite import Invite
|
from model.Invite import Invite
|
||||||
from Expose import expose
|
from Expose import expose
|
||||||
from Validate import validate, Valid_string, Valid_bool, Valid_int, Validation_error
|
from Validate import validate, Valid_string, Valid_bool, Valid_int, Validation_error
|
||||||
|
@ -21,7 +24,10 @@ from view.Redeem_invite_note import Redeem_invite_note
|
||||||
from view.Blank_page import Blank_page
|
from view.Blank_page import Blank_page
|
||||||
from view.Thanks_note import Thanks_note
|
from view.Thanks_note import Thanks_note
|
||||||
from view.Thanks_error_note import Thanks_error_note
|
from view.Thanks_error_note import Thanks_error_note
|
||||||
|
from view.Thanks_download_note import Thanks_download_note
|
||||||
|
from view.Thanks_download_error_note import Thanks_download_error_note
|
||||||
from view.Processing_note import Processing_note
|
from view.Processing_note import Processing_note
|
||||||
|
from view.Processing_download_note import Processing_download_note
|
||||||
from view.Form_submit_page import Form_submit_page
|
from view.Form_submit_page import Form_submit_page
|
||||||
|
|
||||||
|
|
||||||
|
@ -181,7 +187,7 @@ class Users( object ):
|
||||||
"""
|
"""
|
||||||
Controller for dealing with users, corresponding to the "/users" URL.
|
Controller for dealing with users, corresponding to the "/users" URL.
|
||||||
"""
|
"""
|
||||||
def __init__( self, database, http_url, https_url, support_email, payment_email, rate_plans ):
|
def __init__( self, database, http_url, https_url, support_email, payment_email, rate_plans, download_products ):
|
||||||
"""
|
"""
|
||||||
Create a new Users object.
|
Create a new Users object.
|
||||||
|
|
||||||
|
@ -195,8 +201,10 @@ class Users( object ):
|
||||||
@param support_email: email address for support requests
|
@param support_email: email address for support requests
|
||||||
@type payment_email: unicode
|
@type payment_email: unicode
|
||||||
@param payment_email: email address for payment
|
@param payment_email: email address for payment
|
||||||
@type rate_plans: [ { "name": unicode, "storage_quota_bytes": int } ]
|
@type rate_plans: [ { "name": unicode, ... } ]
|
||||||
@param rate_plans: list of configured rate plans
|
@param rate_plans: list of configured rate plans
|
||||||
|
@type download_products: [ { "name": unicode, ... } ]
|
||||||
|
@param download_products: list of configured downloadable products
|
||||||
@rtype: Users
|
@rtype: Users
|
||||||
@return: newly constructed Users
|
@return: newly constructed Users
|
||||||
"""
|
"""
|
||||||
|
@ -206,6 +214,7 @@ class Users( object ):
|
||||||
self.__support_email = support_email
|
self.__support_email = support_email
|
||||||
self.__payment_email = payment_email
|
self.__payment_email = payment_email
|
||||||
self.__rate_plans = rate_plans
|
self.__rate_plans = rate_plans
|
||||||
|
self.__download_products = download_products
|
||||||
|
|
||||||
def create_user( self, username, password = None, password_repeat = None, email_address = None, initial_rate_plan = None ):
|
def create_user( self, username, password = None, password_repeat = None, email_address = None, initial_rate_plan = None ):
|
||||||
"""
|
"""
|
||||||
|
@ -802,9 +811,6 @@ class Users( object ):
|
||||||
@raise Password_reset_error: an error occured when sending the password reset email
|
@raise Password_reset_error: an error occured when sending the password reset email
|
||||||
@raise Validation_error: one of the arguments is invalid
|
@raise Validation_error: one of the arguments is invalid
|
||||||
"""
|
"""
|
||||||
import smtplib
|
|
||||||
from email import Message
|
|
||||||
|
|
||||||
# check whether there are actually any users with the given email address
|
# check whether there are actually any users with the given email address
|
||||||
users = self.__database.select_many( User, User.sql_load_by_email_address( email_address ) )
|
users = self.__database.select_many( User, User.sql_load_by_email_address( email_address ) )
|
||||||
|
|
||||||
|
@ -1252,6 +1258,9 @@ class Users( object ):
|
||||||
|
|
||||||
self.__database.commit()
|
self.__database.commit()
|
||||||
|
|
||||||
|
#PAYPAL_URL = u"https://www.sandbox.paypal.com/cgi-bin/webscr"
|
||||||
|
PAYPAL_URL = u"https://www.paypal.com/cgi-bin/webscr"
|
||||||
|
|
||||||
@expose( view = Blank_page )
|
@expose( view = Blank_page )
|
||||||
@end_transaction
|
@end_transaction
|
||||||
def paypal_notify( self, **params ):
|
def paypal_notify( self, **params ):
|
||||||
|
@ -1262,9 +1271,6 @@ class Users( object ):
|
||||||
record in the database with their new rate plan. paypal_notify() is
|
record in the database with their new rate plan. paypal_notify() is
|
||||||
invoked by PayPal itself.
|
invoked by PayPal itself.
|
||||||
"""
|
"""
|
||||||
#PAYPAL_URL = u"https://www.sandbox.paypal.com/cgi-bin/webscr"
|
|
||||||
PAYPAL_URL = u"https://www.paypal.com/cgi-bin/webscr"
|
|
||||||
|
|
||||||
# check that payment_status is Completed
|
# check that payment_status is Completed
|
||||||
payment_status = params.get( u"payment_status" )
|
payment_status = params.get( u"payment_status" )
|
||||||
if payment_status == u"Refunded":
|
if payment_status == u"Refunded":
|
||||||
|
@ -1283,19 +1289,117 @@ class Users( object ):
|
||||||
raise Payment_error( u"unsupported mc_currency", params )
|
raise Payment_error( u"unsupported mc_currency", params )
|
||||||
|
|
||||||
# verify item_number
|
# verify item_number
|
||||||
plan_index = params.get( u"item_number" )
|
item_number = params.get( u"item_number" )
|
||||||
if plan_index == None or plan_index == u"":
|
if item_number == None or item_number == u"":
|
||||||
return dict() # ignore this transaction if there's no item number
|
return dict() # ignore this transaction if there's no item number
|
||||||
|
|
||||||
try:
|
try:
|
||||||
plan_index = int( plan_index )
|
int( item_number )
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise Payment_error( u"invalid item_number", params )
|
raise Payment_error( u"invalid item_number", params )
|
||||||
if plan_index == 0 or plan_index >= len( self.__rate_plans ):
|
|
||||||
raise Payment_error( u"invalid item_number", params )
|
|
||||||
|
|
||||||
|
product = None
|
||||||
|
for potential_product in self.__download_products:
|
||||||
|
if unicode( item_number ) == potential_product.get( u"item_number" ):
|
||||||
|
product = potential_product
|
||||||
|
|
||||||
|
if product:
|
||||||
|
self.__paypal_notify_download( params, product, unicode( item_number ) )
|
||||||
|
else:
|
||||||
|
plan_index = int( item_number )
|
||||||
|
try:
|
||||||
|
rate_plan = self.__rate_plans[ plan_index ]
|
||||||
|
except IndexError:
|
||||||
|
raise Payment_error( u"invalid item_number", params )
|
||||||
|
self.__paypal_notify_subscribe( params, rate_plan, plan_index )
|
||||||
|
|
||||||
|
return dict()
|
||||||
|
|
||||||
|
TRANSACTION_ID_PATTERN = re.compile( u"^[a-zA-Z0-9]+$" )
|
||||||
|
|
||||||
|
def __paypal_notify_download( self, params, product, item_number ):
|
||||||
|
# verify that quantity * the expected fee == mc_gross
|
||||||
|
fee = float( product[ u"fee" ] )
|
||||||
|
|
||||||
|
try:
|
||||||
|
mc_gross = float( params.get( u"mc_gross" ) )
|
||||||
|
if not mc_gross: raise ValueError()
|
||||||
|
except ( TypeError, ValueError ):
|
||||||
|
raise Payment_error( u"invalid mc_gross", params )
|
||||||
|
|
||||||
|
try:
|
||||||
|
quantity = float( params.get( u"quantity" ) )
|
||||||
|
if not quantity: raise ValueError()
|
||||||
|
except ( TypeError, ValueError ):
|
||||||
|
raise Payment_error( u"invalid quantity", params )
|
||||||
|
|
||||||
|
if quantity * fee != mc_gross:
|
||||||
|
raise Payment_error( u"invalid mc_gross", params )
|
||||||
|
|
||||||
|
# verify item_name
|
||||||
|
item_name = params.get( u"item_name" )
|
||||||
|
if item_name and product[ u"name" ].lower() not in item_name.lower():
|
||||||
|
raise Payment_error( u"invalid item_name", params )
|
||||||
|
|
||||||
|
params[ u"cmd" ] = u"_notify-validate"
|
||||||
|
encoded_params = urllib.urlencode( params )
|
||||||
|
|
||||||
|
# verify txn_type
|
||||||
|
txn_type = params.get( u"txn_type" )
|
||||||
|
if txn_type and txn_type != u"web_accept":
|
||||||
|
raise Payment_error( u"invalid txn_type", params )
|
||||||
|
|
||||||
|
# verify txn_id
|
||||||
|
txn_id = params.get( u"txn_id" )
|
||||||
|
if not self.TRANSACTION_ID_PATTERN.search( txn_id ):
|
||||||
|
raise Payment_error( u"invalid txn_id", params )
|
||||||
|
|
||||||
|
# ask paypal to verify the request
|
||||||
|
request = urllib2.Request( self.PAYPAL_URL )
|
||||||
|
request.add_header( u"Content-type", u"application/x-www-form-urlencoded" )
|
||||||
|
request_file = urllib2.urlopen( self.PAYPAL_URL, encoded_params )
|
||||||
|
result = request_file.read()
|
||||||
|
|
||||||
|
if result != u"VERIFIED":
|
||||||
|
raise Payment_error( result, params )
|
||||||
|
|
||||||
|
# update the database with a record of the transaction, thereby giving the user access to the
|
||||||
|
# download
|
||||||
|
download_access_id = self.__database.next_id( Download_access, commit = False )
|
||||||
|
download_access = Download_access.create( download_access_id, item_number, txn_id )
|
||||||
|
self.__database.save( download_access, commit = False )
|
||||||
|
self.__database.commit()
|
||||||
|
|
||||||
|
# using the reported payer email, send the user an email with a download link
|
||||||
|
email_address = params.get( u"payer_email" )
|
||||||
|
if not email_address:
|
||||||
|
return
|
||||||
|
|
||||||
|
# create an email message with a unique invitation link
|
||||||
|
message = Message.Message()
|
||||||
|
message[ u"From" ] = u"Luminotes personal wiki <%s>" % self.__support_email
|
||||||
|
message[ u"To" ] = email_address
|
||||||
|
message[ u"Subject" ] = u"Luminotes Desktop download"
|
||||||
|
|
||||||
|
payload = \
|
||||||
|
u"Thank you for purchasing Luminotes Desktop!\n\n" + \
|
||||||
|
u"To download the installer, please follow this link:\n\n" + \
|
||||||
|
u"%s/d/%s\n\n" % ( self.__https_url or self.__http_url, download_access_id ) + \
|
||||||
|
u"You can use this link anytime to download Luminotes Desktop or upgrade\n" + \
|
||||||
|
u"to new versions as they are released. So you should probably keep the" + \
|
||||||
|
u"link around.\n\n" + \
|
||||||
|
u"If you have any questions, please email support@luminotes.com\n\n" + \
|
||||||
|
u"Enjoy!"
|
||||||
|
|
||||||
|
message.set_payload( payload )
|
||||||
|
|
||||||
|
# send the message out through localhost's smtp server
|
||||||
|
server = smtplib.SMTP()
|
||||||
|
server.connect()
|
||||||
|
server.sendmail( message[ u"From" ], [ email_address ], message.as_string() )
|
||||||
|
server.quit()
|
||||||
|
|
||||||
|
def __paypal_notify_subscribe( self, params, rate_plan, plan_index ):
|
||||||
# verify mc_gross
|
# verify mc_gross
|
||||||
rate_plan = self.__rate_plans[ plan_index ]
|
|
||||||
fee = u"%0.2f" % rate_plan[ u"fee" ]
|
fee = u"%0.2f" % rate_plan[ u"fee" ]
|
||||||
yearly_fee = u"%0.2f" % rate_plan[ u"yearly_fee" ]
|
yearly_fee = u"%0.2f" % rate_plan[ u"yearly_fee" ]
|
||||||
mc_gross = params.get( u"mc_gross" )
|
mc_gross = params.get( u"mc_gross" )
|
||||||
|
@ -1329,9 +1433,9 @@ class Users( object ):
|
||||||
encoded_params = urllib.urlencode( params )
|
encoded_params = urllib.urlencode( params )
|
||||||
|
|
||||||
# ask paypal to verify the request
|
# ask paypal to verify the request
|
||||||
request = urllib2.Request( PAYPAL_URL )
|
request = urllib2.Request( self.PAYPAL_URL )
|
||||||
request.add_header( u"Content-type", u"application/x-www-form-urlencoded" )
|
request.add_header( u"Content-type", u"application/x-www-form-urlencoded" )
|
||||||
request_file = urllib2.urlopen( PAYPAL_URL, encoded_params )
|
request_file = urllib2.urlopen( self.PAYPAL_URL, encoded_params )
|
||||||
result = request_file.read()
|
result = request_file.read()
|
||||||
|
|
||||||
if result != u"VERIFIED":
|
if result != u"VERIFIED":
|
||||||
|
@ -1366,8 +1470,6 @@ class Users( object ):
|
||||||
else:
|
else:
|
||||||
raise Payment_error( "unknown txn_type", params )
|
raise Payment_error( "unknown txn_type", params )
|
||||||
|
|
||||||
return dict()
|
|
||||||
|
|
||||||
def update_groups( self, user ):
|
def update_groups( self, user ):
|
||||||
"""
|
"""
|
||||||
Update a user's group membership as a result of a rate plan change. This method does not commit
|
Update a user's group membership as a result of a rate plan change. This method does not commit
|
||||||
|
@ -1468,6 +1570,93 @@ class Users( object ):
|
||||||
def rate_plan( self, plan_index ):
|
def rate_plan( self, plan_index ):
|
||||||
return self.__rate_plans[ plan_index ]
|
return self.__rate_plans[ plan_index ]
|
||||||
|
|
||||||
|
@expose( view = Main_page )
|
||||||
|
@end_transaction
|
||||||
|
@grab_user_id
|
||||||
|
def thanks_download( self, **params ):
|
||||||
|
"""
|
||||||
|
Provide the information necessary to display the download thanks page, including a product
|
||||||
|
download link. This information can be accessed with an item_number and either a txn_id or a
|
||||||
|
download access_id.
|
||||||
|
"""
|
||||||
|
item_number = params.get( u"item_number" )
|
||||||
|
try:
|
||||||
|
item_number = int( item_number )
|
||||||
|
except ( TypeError, ValueError ):
|
||||||
|
raise Payment_error( u"invalid item_number", params )
|
||||||
|
|
||||||
|
# if a valid txn_id is provided, redirect to this page with the corresponding access_id.
|
||||||
|
# that way, if the user bookmarks the page, they'll bookmark it with the access_id rather
|
||||||
|
# than the txn_id
|
||||||
|
txn_id = params.get( u"txn_id" )
|
||||||
|
if txn_id:
|
||||||
|
if not self.TRANSACTION_ID_PATTERN.search( txn_id ):
|
||||||
|
raise Payment_error( u"invalid txn_id", params )
|
||||||
|
|
||||||
|
download_access = self.__database.select_one( Download_access, Download_access.sql_load_by_transaction_id( txn_id ) )
|
||||||
|
if download_access:
|
||||||
|
return dict(
|
||||||
|
redirect = u"/users/thanks_download?access_id=%s&item_number=%s" % ( download_access.object_id, item_number )
|
||||||
|
)
|
||||||
|
|
||||||
|
download_access_id = params.get( u"access_id" )
|
||||||
|
download_url = None
|
||||||
|
|
||||||
|
if download_access_id:
|
||||||
|
try:
|
||||||
|
Valid_id()( download_access_id )
|
||||||
|
except ValueError:
|
||||||
|
raise Payment_error( u"invalid access_id", params )
|
||||||
|
|
||||||
|
download_access = self.__database.load( Download_access, download_access_id )
|
||||||
|
if download_access:
|
||||||
|
if download_access.item_number != unicode( item_number ):
|
||||||
|
raise Payment_error( u"incorrect item_number", params )
|
||||||
|
download_url = u"%s/files/download_product/access_id=%s&item_number=%s" % \
|
||||||
|
( self.__https_url or u"", download_access_id, item_number )
|
||||||
|
|
||||||
|
if not txn_id and not download_access_id:
|
||||||
|
raise Payment_error( u"either txn_id or access_id required", params )
|
||||||
|
|
||||||
|
anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ), use_cache = True )
|
||||||
|
if anonymous:
|
||||||
|
main_notebook = self.__database.select_one( Notebook, anonymous.sql_load_notebooks( undeleted_only = True ) )
|
||||||
|
else:
|
||||||
|
main_notebook = None
|
||||||
|
|
||||||
|
result = self.current( params.get( u"user_id" ) )
|
||||||
|
|
||||||
|
retry_count = params.get( u"retry_count", "" )
|
||||||
|
try:
|
||||||
|
retry_count = int( retry_count )
|
||||||
|
except ValueError:
|
||||||
|
retry_count = None
|
||||||
|
|
||||||
|
# if there's no download access or we've retried too many times, give up and display an error
|
||||||
|
RETRY_TIMEOUT = 15
|
||||||
|
if download_url is None and retry_count > RETRY_TIMEOUT:
|
||||||
|
note = Thanks_download_error_note()
|
||||||
|
# if the rate plan of the subscription matches the user's current rate plan, success
|
||||||
|
elif download_url:
|
||||||
|
note = Thanks_download_note( download_url )
|
||||||
|
result[ "conversion" ] = "download_%s" % item_number
|
||||||
|
# otherwise, display an auto-reloading "processing..." page
|
||||||
|
else:
|
||||||
|
note = Processing_download_note( download_access_id, item_number, retry_count )
|
||||||
|
|
||||||
|
result[ "notebook" ] = main_notebook
|
||||||
|
result[ "startup_notes" ] = self.__database.select_many( Note, main_notebook.sql_load_startup_notes() )
|
||||||
|
result[ "total_notes_count" ] = self.__database.select_one( Note, main_notebook.sql_count_notes(), use_cache = True )
|
||||||
|
result[ "note_read_write" ] = False
|
||||||
|
result[ "notes" ] = [ Note.create(
|
||||||
|
object_id = u"thanks",
|
||||||
|
contents = unicode( note ),
|
||||||
|
notebook_id = main_notebook.object_id,
|
||||||
|
) ]
|
||||||
|
result[ "invites" ] = []
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
@expose( view = Json )
|
@expose( view = Json )
|
||||||
@end_transaction
|
@end_transaction
|
||||||
@grab_user_id
|
@grab_user_id
|
||||||
|
|
|
@ -96,6 +96,21 @@ class Test_controller( object ):
|
||||||
u"yearly_button": u"[yearly or here user %s!] button",
|
u"yearly_button": u"[yearly or here user %s!] button",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"luminotes.download_products": [
|
||||||
|
{
|
||||||
|
"name": "local desktop extravaganza",
|
||||||
|
"designed_for": "individuals",
|
||||||
|
"storage_quota_bytes": None,
|
||||||
|
"included_users": 1,
|
||||||
|
"notebook_sharing": False,
|
||||||
|
"notebook_collaboration": False,
|
||||||
|
"user_admin": False,
|
||||||
|
"fee": "30.00",
|
||||||
|
"item_number": "5000",
|
||||||
|
"filename": "test.exe",
|
||||||
|
"button": u"",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
u"/files/download": {
|
u"/files/download": {
|
||||||
u"stream_response": True,
|
u"stream_response": True,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# -*- coding: utf8 -*-
|
# -*- coding: utf8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import types
|
import types
|
||||||
import urllib
|
import urllib
|
||||||
|
@ -14,6 +15,7 @@ from model.Note import Note
|
||||||
from model.User import User
|
from model.User import User
|
||||||
from model.Invite import Invite
|
from model.Invite import Invite
|
||||||
from model.File import File
|
from model.File import File
|
||||||
|
from model.Download_access import Download_access
|
||||||
from controller.Notebooks import Access_error
|
from controller.Notebooks import Access_error
|
||||||
from controller.Files import Upload_file, Parse_error
|
from controller.Files import Upload_file, Parse_error
|
||||||
|
|
||||||
|
@ -90,6 +92,11 @@ class Test_files( Test_controller ):
|
||||||
Upload_file.exists = exists
|
Upload_file.exists = exists
|
||||||
Upload_file.close = close
|
Upload_file.close = close
|
||||||
|
|
||||||
|
# write a test product file
|
||||||
|
test_product_file = file( u"products/test.exe", "wb" )
|
||||||
|
test_product_file.write( self.file_data )
|
||||||
|
test_product_file.close()
|
||||||
|
|
||||||
self.make_users()
|
self.make_users()
|
||||||
self.make_notebooks()
|
self.make_notebooks()
|
||||||
self.database.commit()
|
self.database.commit()
|
||||||
|
@ -128,6 +135,8 @@ class Test_files( Test_controller ):
|
||||||
if self.upload_thread:
|
if self.upload_thread:
|
||||||
self.upload_thread.join()
|
self.upload_thread.join()
|
||||||
|
|
||||||
|
os.remove( u"products/test.exe" )
|
||||||
|
|
||||||
def test_download( self, filename = None, quote_filename = None, file_data = None, preview = None ):
|
def test_download( self, filename = None, quote_filename = None, file_data = None, preview = None ):
|
||||||
self.login()
|
self.login()
|
||||||
|
|
||||||
|
@ -327,6 +336,139 @@ class Test_files( Test_controller ):
|
||||||
|
|
||||||
assert u"access" in result[ u"body" ][ 0 ]
|
assert u"access" in result[ u"body" ][ 0 ]
|
||||||
|
|
||||||
|
def test_download_product( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
download_access = Download_access.create( access_id, item_number, transaction_id )
|
||||||
|
self.database.save( download_access )
|
||||||
|
|
||||||
|
result = self.http_get(
|
||||||
|
"/files/download_product?access_id=%s&item_number=%s" % ( access_id, item_number ),
|
||||||
|
session_id = self.session_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = result[ u"headers" ]
|
||||||
|
assert headers
|
||||||
|
assert headers[ u"Content-Type" ] == u"application/octet-stream"
|
||||||
|
|
||||||
|
filename = u"test.exe".encode( "utf8" )
|
||||||
|
assert headers[ u"Content-Disposition" ] == 'attachment; filename="%s"' % filename
|
||||||
|
|
||||||
|
gen = result[ u"body" ]
|
||||||
|
assert isinstance( gen, types.GeneratorType )
|
||||||
|
pieces = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
for piece in gen:
|
||||||
|
pieces.append( piece )
|
||||||
|
except AttributeError, exc:
|
||||||
|
if u"session_storage" not in str( exc ):
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
file_data = "".join( pieces )
|
||||||
|
assert file_data == self.file_data
|
||||||
|
|
||||||
|
def test_download_product_without_login( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
download_access = Download_access.create( access_id, item_number, transaction_id )
|
||||||
|
self.database.save( download_access )
|
||||||
|
|
||||||
|
result = self.http_get(
|
||||||
|
"/files/download_product?access_id=%s&item_number=%s" % ( access_id, item_number ),
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = result[ u"headers" ]
|
||||||
|
assert headers
|
||||||
|
assert headers[ u"Content-Type" ] == u"application/octet-stream"
|
||||||
|
|
||||||
|
filename = u"test.exe".encode( "utf8" )
|
||||||
|
assert headers[ u"Content-Disposition" ] == 'attachment; filename="%s"' % filename
|
||||||
|
|
||||||
|
gen = result[ u"body" ]
|
||||||
|
assert isinstance( gen, types.GeneratorType )
|
||||||
|
pieces = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
for piece in gen:
|
||||||
|
pieces.append( piece )
|
||||||
|
except AttributeError, exc:
|
||||||
|
if u"session_storage" not in str( exc ):
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
file_data = "".join( pieces )
|
||||||
|
assert file_data == self.file_data
|
||||||
|
|
||||||
|
def test_download_product_unknown_access_id( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
download_access = Download_access.create( access_id, item_number, transaction_id )
|
||||||
|
self.database.save( download_access )
|
||||||
|
|
||||||
|
result = self.http_get(
|
||||||
|
"/files/download_product?access_id=%s&item_number=%s" % ( u"unknownid", item_number ),
|
||||||
|
session_id = self.session_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert u"access" in result[ u"body" ][ 0 ]
|
||||||
|
headers = result[ u"headers" ]
|
||||||
|
assert headers
|
||||||
|
assert headers[ u"Content-Type" ] == u"text/html"
|
||||||
|
assert not headers.get( u"Content-Disposition" )
|
||||||
|
|
||||||
|
def test_download_product_unknown_item_number( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
download_access = Download_access.create( access_id, item_number, transaction_id )
|
||||||
|
self.database.save( download_access )
|
||||||
|
|
||||||
|
result = self.http_get(
|
||||||
|
"/files/download_product?access_id=%s&item_number=%s" % ( access_id, u"1137" ),
|
||||||
|
session_id = self.session_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert u"access" in result[ u"body" ][ 0 ]
|
||||||
|
headers = result[ u"headers" ]
|
||||||
|
assert headers
|
||||||
|
assert headers[ u"Content-Type" ] == u"text/html"
|
||||||
|
assert not headers.get( u"Content-Disposition" )
|
||||||
|
|
||||||
|
def test_download_product_missing_file( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
self.settings[ u"global" ][ u"luminotes.download_products" ][ 0 ][ u"filename" ] = u"notthere.exe"
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
download_access = Download_access.create( access_id, item_number, transaction_id )
|
||||||
|
self.database.save( download_access )
|
||||||
|
|
||||||
|
result = self.http_get(
|
||||||
|
"/files/download_product?access_id=%s&item_number=%s" % ( access_id, item_number ),
|
||||||
|
session_id = self.session_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert u"access" in result[ u"body" ][ 0 ]
|
||||||
|
headers = result[ u"headers" ]
|
||||||
|
assert headers
|
||||||
|
assert headers[ u"Content-Type" ] == u"text/html"
|
||||||
|
assert not headers.get( u"Content-Disposition" )
|
||||||
|
|
||||||
def test_preview( self ):
|
def test_preview( self ):
|
||||||
self.login()
|
self.login()
|
||||||
|
|
||||||
|
|
|
@ -538,3 +538,9 @@ class Test_root( Test_controller ):
|
||||||
result = self.http_get( "/i/%s" % invite_id )
|
result = self.http_get( "/i/%s" % invite_id )
|
||||||
|
|
||||||
assert result[ u"redirect" ] == u"/users/redeem_invite/%s" % invite_id
|
assert result[ u"redirect" ] == u"/users/redeem_invite/%s" % invite_id
|
||||||
|
|
||||||
|
def test_download_thanks( self ):
|
||||||
|
download_access_id = u"foobarbaz"
|
||||||
|
result = self.http_get( "/d/%s" % download_access_id )
|
||||||
|
|
||||||
|
assert result[ u"redirect" ] == u"/users/download_thanks/access_id=%s" % download_access_id
|
||||||
|
|
|
@ -7,11 +7,13 @@ from nose.tools import raises
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from Test_controller import Test_controller
|
from Test_controller import Test_controller
|
||||||
import Stub_urllib2
|
import Stub_urllib2
|
||||||
|
from config.Version import VERSION
|
||||||
from model.User import User
|
from model.User import User
|
||||||
from model.Group import Group
|
from model.Group import Group
|
||||||
from model.Notebook import Notebook
|
from model.Notebook import Notebook
|
||||||
from model.Note import Note
|
from model.Note import Note
|
||||||
from model.Password_reset import Password_reset
|
from model.Password_reset import Password_reset
|
||||||
|
from model.Download_access import Download_access
|
||||||
from model.Invite import Invite
|
from model.Invite import Invite
|
||||||
from controller.Users import Invite_error, Payment_error
|
from controller.Users import Invite_error, Payment_error
|
||||||
import controller.Users as Users
|
import controller.Users as Users
|
||||||
|
@ -3944,6 +3946,150 @@ 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 == 1
|
assert user.rate_plan == 1
|
||||||
|
|
||||||
|
DOWNLOAD_PAYMENT_DATA = {
|
||||||
|
u"last_name": u"User",
|
||||||
|
u"txn_id": u"txn",
|
||||||
|
u"receiver_email": u"unittest@luminotes.com",
|
||||||
|
u"payment_status": u"Completed",
|
||||||
|
u"payment_gross": u"30.00",
|
||||||
|
u"residence_country": u"US",
|
||||||
|
u"payer_status": u"verified",
|
||||||
|
u"txn_type": u"web_accept",
|
||||||
|
u"payment_date": u"15:38:18 Jan 10 2008 PST",
|
||||||
|
u"first_name": u"Test",
|
||||||
|
u"item_name": u"local desktop extravaganza",
|
||||||
|
u"charset": u"windows-1252",
|
||||||
|
u"notify_version": u"2.4",
|
||||||
|
u"item_number": u"5000",
|
||||||
|
u"receiver_id": u"rcv",
|
||||||
|
u"business": u"unittest@luminotes.com",
|
||||||
|
u"payer_id": u"pyr",
|
||||||
|
u"verify_sign": u"vfy",
|
||||||
|
u"payment_fee": u"1.19",
|
||||||
|
u"mc_fee": u"1.19",
|
||||||
|
u"mc_currency": u"USD",
|
||||||
|
u"shipping": u"0.00",
|
||||||
|
u"payer_email": u"buyer@luminotes.com",
|
||||||
|
u"payment_type": u"instant",
|
||||||
|
u"mc_gross": u"30.00",
|
||||||
|
u"quantity": u"1",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __assert_download_payment_success( self, result, expect_email = True ):
|
||||||
|
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
|
||||||
|
|
||||||
|
# verify that the user has been granted download access
|
||||||
|
download_access = self.database.select_one( Download_access, "select * from download_access order by revision desc limit 1;" );
|
||||||
|
assert download_access
|
||||||
|
assert download_access.item_number == u"5000"
|
||||||
|
assert download_access.transaction_id == u"txn"
|
||||||
|
|
||||||
|
if not expect_email:
|
||||||
|
return
|
||||||
|
|
||||||
|
# verify that an email has been sent to the user
|
||||||
|
assert smtplib.SMTP.connected == False
|
||||||
|
assert "<%s>" % self.settings[ u"global" ][ u"luminotes.support_email" ] in smtplib.SMTP.from_address
|
||||||
|
assert smtplib.SMTP.to_addresses == [ u"buyer@luminotes.com" ]
|
||||||
|
assert u"Thank you" in smtplib.SMTP.message
|
||||||
|
assert u"download" in smtplib.SMTP.message
|
||||||
|
assert u"upgrade" in smtplib.SMTP.message
|
||||||
|
|
||||||
|
expected_download_link = u"%s/d/%s" % \
|
||||||
|
( self.settings[ u"global" ][ u"luminotes.https_url" ], download_access.object_id )
|
||||||
|
assert expected_download_link in smtplib.SMTP.message
|
||||||
|
|
||||||
|
def test_paypal_notify_download_payment( self ):
|
||||||
|
data = dict( self.DOWNLOAD_PAYMENT_DATA )
|
||||||
|
result = self.http_post( "/users/paypal_notify", data );
|
||||||
|
self.__assert_download_payment_success( result )
|
||||||
|
|
||||||
|
def test_paypal_notify_download_payment_multiple_quantity( self ):
|
||||||
|
data = dict( self.DOWNLOAD_PAYMENT_DATA )
|
||||||
|
data[ u"mc_gross" ] = u"90.0"
|
||||||
|
data[ u"quantity" ] = u"3"
|
||||||
|
result = self.http_post( "/users/paypal_notify", data );
|
||||||
|
self.__assert_download_payment_success( result )
|
||||||
|
|
||||||
|
def __assert_download_payment_error( self, result ):
|
||||||
|
assert u"error" in result
|
||||||
|
download_access = self.database.select_one( Download_access, "select * from download_access order by revision desc limit 1;" );
|
||||||
|
assert not download_access
|
||||||
|
assert not smtplib.SMTP.message
|
||||||
|
|
||||||
|
def test_paypal_notify_download_payment_missing_mc_gross( self ):
|
||||||
|
data = dict( self.DOWNLOAD_PAYMENT_DATA )
|
||||||
|
del( data[ u"mc_gross" ] )
|
||||||
|
result = self.http_post( "/users/paypal_notify", data );
|
||||||
|
self.__assert_download_payment_error( result )
|
||||||
|
|
||||||
|
def test_paypal_notify_download_payment_none_mc_gross( self ):
|
||||||
|
data = dict( self.DOWNLOAD_PAYMENT_DATA )
|
||||||
|
data[ u"mc_gross" ] = None
|
||||||
|
result = self.http_post( "/users/paypal_notify", data );
|
||||||
|
self.__assert_download_payment_error( result )
|
||||||
|
|
||||||
|
def test_paypal_notify_download_payment_missing_quantity( self ):
|
||||||
|
data = dict( self.DOWNLOAD_PAYMENT_DATA )
|
||||||
|
del( data[ u"quantity" ] )
|
||||||
|
result = self.http_post( "/users/paypal_notify", data );
|
||||||
|
self.__assert_download_payment_error( result )
|
||||||
|
|
||||||
|
def test_paypal_notify_download_payment_none_quantity( self ):
|
||||||
|
data = dict( self.DOWNLOAD_PAYMENT_DATA )
|
||||||
|
data[ u"quantity" ] = None
|
||||||
|
result = self.http_post( "/users/paypal_notify", data );
|
||||||
|
self.__assert_download_payment_error( result )
|
||||||
|
|
||||||
|
def test_paypal_notify_download_payment_quantity_mc_gross_mismatch( self ):
|
||||||
|
data = dict( self.DOWNLOAD_PAYMENT_DATA )
|
||||||
|
data[ u"quantity" ] = u"2"
|
||||||
|
result = self.http_post( "/users/paypal_notify", data );
|
||||||
|
self.__assert_download_payment_error( result )
|
||||||
|
|
||||||
|
def test_paypal_notify_download_payment_mc_gross_fee_mismatch( self ):
|
||||||
|
data = dict( self.DOWNLOAD_PAYMENT_DATA )
|
||||||
|
data[ u"quantity" ] = u"2"
|
||||||
|
data[ u"mc_gross" ] = u"61.0"
|
||||||
|
result = self.http_post( "/users/paypal_notify", data );
|
||||||
|
self.__assert_download_payment_error( result )
|
||||||
|
|
||||||
|
def test_paypal_notify_download_payment_invalid_item_name( self ):
|
||||||
|
data = dict( self.DOWNLOAD_PAYMENT_DATA )
|
||||||
|
data[ u"item_name" ] = u"something unexpected"
|
||||||
|
result = self.http_post( "/users/paypal_notify", data );
|
||||||
|
self.__assert_download_payment_error( result )
|
||||||
|
|
||||||
|
def test_paypal_notify_download_payment_partial_item_name( self ):
|
||||||
|
data = dict( self.DOWNLOAD_PAYMENT_DATA )
|
||||||
|
data[ u"item_name" ] = u"ultra LOCAL DESKTOP extravaganza digital download!"
|
||||||
|
result = self.http_post( "/users/paypal_notify", data );
|
||||||
|
self.__assert_download_payment_success( result )
|
||||||
|
|
||||||
|
def test_paypal_notify_download_payment_invalid_txn_type( self ):
|
||||||
|
data = dict( self.DOWNLOAD_PAYMENT_DATA )
|
||||||
|
data[ u"txn_type" ] = u"web_wtf"
|
||||||
|
result = self.http_post( "/users/paypal_notify", data );
|
||||||
|
self.__assert_download_payment_error( result )
|
||||||
|
|
||||||
|
def test_paypal_notify_download_payment_invalid_txn_id( self ):
|
||||||
|
data = dict( self.DOWNLOAD_PAYMENT_DATA )
|
||||||
|
data[ u"txn_id" ] = u"not even remotely valid"
|
||||||
|
result = self.http_post( "/users/paypal_notify", data );
|
||||||
|
self.__assert_download_payment_error( result )
|
||||||
|
|
||||||
|
def test_paypal_notify_download_payment_missing_payer_email( self ):
|
||||||
|
data = dict( self.DOWNLOAD_PAYMENT_DATA )
|
||||||
|
data[ u"payer_email" ] = u""
|
||||||
|
result = self.http_post( "/users/paypal_notify", data );
|
||||||
|
self.__assert_download_payment_success( result, expect_email = False )
|
||||||
|
|
||||||
def test_thanks( self ):
|
def test_thanks( self ):
|
||||||
self.user.rate_plan = 1
|
self.user.rate_plan = 1
|
||||||
user = self.database.save( self.user )
|
user = self.database.save( self.user )
|
||||||
|
@ -4143,6 +4289,508 @@ class Test_users( Test_controller ):
|
||||||
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" in result[ u"notes" ][ 0 ].contents
|
||||||
|
|
||||||
|
def test_thanks_download( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
download_access = Download_access.create( access_id, item_number, transaction_id )
|
||||||
|
self.database.save( download_access )
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
access_id = access_id,
|
||||||
|
item_number = item_number,
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
assert result[ u"user" ].username == self.user.username
|
||||||
|
assert len( result[ u"notebooks" ] ) == 5
|
||||||
|
notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
|
||||||
|
assert notebook.object_id == self.notebooks[ 0 ].object_id
|
||||||
|
assert notebook.name == self.notebooks[ 0 ].name
|
||||||
|
assert notebook.read_write == True
|
||||||
|
assert notebook.owner == True
|
||||||
|
assert notebook.rank == 0
|
||||||
|
|
||||||
|
assert result[ u"login_url" ] == None
|
||||||
|
assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"
|
||||||
|
|
||||||
|
rate_plan = result[ u"rate_plan" ]
|
||||||
|
assert rate_plan
|
||||||
|
assert rate_plan[ u"name" ] == u"super"
|
||||||
|
assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
|
||||||
|
|
||||||
|
assert result[ u"conversion" ] == u"download_5000"
|
||||||
|
assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
|
||||||
|
assert len( result[ u"startup_notes" ] ) == 1
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
|
||||||
|
assert result[ u"note_read_write" ] is False
|
||||||
|
|
||||||
|
assert result[ u"notes" ]
|
||||||
|
assert len( result[ u"notes" ] ) == 1
|
||||||
|
assert result[ u"notes" ][ 0 ].title == u"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"Luminotes Desktop" in result[ u"notes" ][ 0 ].contents
|
||||||
|
assert u"Download" in result[ u"notes" ][ 0 ].contents
|
||||||
|
assert VERSION in result[ u"notes" ][ 0 ].contents
|
||||||
|
|
||||||
|
expected_download_link = u"%s/files/download_product/access_id=%s&item_number=%s" % \
|
||||||
|
( self.settings[ u"global" ][ u"luminotes.https_url" ], access_id, item_number )
|
||||||
|
assert expected_download_link in result[ u"notes" ][ 0 ].contents
|
||||||
|
|
||||||
|
def test_thanks_download_without_login( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
download_access = Download_access.create( access_id, item_number, transaction_id )
|
||||||
|
self.database.save( download_access )
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
access_id = access_id,
|
||||||
|
item_number = item_number,
|
||||||
|
) )
|
||||||
|
|
||||||
|
assert result[ u"user" ].username == self.anonymous.username
|
||||||
|
assert len( result[ u"notebooks" ] ) == 1
|
||||||
|
|
||||||
|
assert result[ u"login_url" ]
|
||||||
|
assert result[ u"logout_url" ]
|
||||||
|
|
||||||
|
rate_plan = result[ u"rate_plan" ]
|
||||||
|
assert rate_plan
|
||||||
|
assert rate_plan[ u"name" ] == u"super"
|
||||||
|
assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
|
||||||
|
|
||||||
|
assert result[ u"conversion" ] == u"download_5000"
|
||||||
|
assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
|
||||||
|
assert len( result[ u"startup_notes" ] ) == 1
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
|
||||||
|
assert result[ u"note_read_write" ] is False
|
||||||
|
|
||||||
|
assert result[ u"notes" ]
|
||||||
|
assert len( result[ u"notes" ] ) == 1
|
||||||
|
assert result[ u"notes" ][ 0 ].title == u"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"Luminotes Desktop" in result[ u"notes" ][ 0 ].contents
|
||||||
|
assert u"Download" in result[ u"notes" ][ 0 ].contents
|
||||||
|
assert VERSION in result[ u"notes" ][ 0 ].contents
|
||||||
|
|
||||||
|
expected_download_link = u"%s/files/download_product/access_id=%s&item_number=%s" % \
|
||||||
|
( self.settings[ u"global" ][ u"luminotes.https_url" ], access_id, item_number )
|
||||||
|
assert expected_download_link in result[ u"notes" ][ 0 ].contents
|
||||||
|
|
||||||
|
def test_thanks_download_invalid_item_number( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000abc"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
download_access = Download_access.create( access_id, item_number, transaction_id )
|
||||||
|
self.database.save( download_access )
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
access_id = access_id,
|
||||||
|
item_number = item_number,
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
assert u"error" in result
|
||||||
|
|
||||||
|
def test_thanks_download_none_item_number( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = None
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
download_access = Download_access.create( access_id, item_number, transaction_id )
|
||||||
|
self.database.save( download_access )
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
access_id = access_id,
|
||||||
|
item_number = item_number,
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
assert u"error" in result
|
||||||
|
|
||||||
|
def test_thanks_download_missing_item_number( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
access_id = access_id,
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
assert u"error" in result
|
||||||
|
|
||||||
|
def test_thanks_download_incorrect_item_number( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
download_access = Download_access.create( access_id, item_number, transaction_id )
|
||||||
|
self.database.save( download_access )
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
access_id = access_id,
|
||||||
|
item_number = u"1234",
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
assert u"error" in result
|
||||||
|
|
||||||
|
def test_thanks_download_txn_id( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
download_access = Download_access.create( access_id, item_number, transaction_id )
|
||||||
|
self.database.save( download_access )
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
txn_id = transaction_id,
|
||||||
|
item_number = item_number,
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
redirect = result.get( u"redirect" )
|
||||||
|
expected_redirect = "/users/thanks_download?access_id=%s&item_number=%s" % ( access_id, item_number )
|
||||||
|
assert redirect == expected_redirect
|
||||||
|
|
||||||
|
def test_thanks_download_invalid_txn_id( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"invalid txn id"
|
||||||
|
|
||||||
|
download_access = Download_access.create( access_id, item_number, transaction_id )
|
||||||
|
self.database.save( download_access )
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
txn_id = transaction_id,
|
||||||
|
item_number = item_number,
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
assert u"error" in result
|
||||||
|
|
||||||
|
def test_thanks_download_not_yet_paid( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
access_id = access_id,
|
||||||
|
item_number = item_number,
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
# an unknown transaction id might just mean we're still waiting for the transaction to come in,
|
||||||
|
# so expect a retry
|
||||||
|
assert result[ u"user" ].username == self.user.username
|
||||||
|
assert len( result[ u"notebooks" ] ) == 5
|
||||||
|
notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
|
||||||
|
assert notebook.object_id == self.notebooks[ 0 ].object_id
|
||||||
|
assert notebook.name == self.notebooks[ 0 ].name
|
||||||
|
assert notebook.read_write == True
|
||||||
|
assert notebook.owner == True
|
||||||
|
assert notebook.rank == 0
|
||||||
|
|
||||||
|
assert result[ u"login_url" ] == None
|
||||||
|
assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"
|
||||||
|
|
||||||
|
rate_plan = result[ u"rate_plan" ]
|
||||||
|
assert rate_plan
|
||||||
|
assert rate_plan[ u"name" ] == u"super"
|
||||||
|
assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
|
||||||
|
|
||||||
|
assert not result.get( u"conversion" )
|
||||||
|
assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
|
||||||
|
assert len( result[ u"startup_notes" ] ) == 1
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
|
||||||
|
assert result[ u"note_read_write" ] is False
|
||||||
|
|
||||||
|
assert result[ u"notes" ]
|
||||||
|
assert len( result[ u"notes" ] ) == 1
|
||||||
|
assert u"processing" in result[ u"notes" ][ 0 ].title
|
||||||
|
assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
|
||||||
|
assert u"being processed" in result[ u"notes" ][ 0 ].contents
|
||||||
|
assert u"retry_count=1" in result[ u"notes" ][ 0 ].contents
|
||||||
|
|
||||||
|
def test_thanks_download_not_yet_paid_with_retry( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
access_id = access_id,
|
||||||
|
item_number = item_number,
|
||||||
|
retry_count = u"3",
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
# an unknown transaction id might just mean we're still waiting for the transaction to come in,
|
||||||
|
# so expect a retry
|
||||||
|
assert result[ u"user" ].username == self.user.username
|
||||||
|
assert len( result[ u"notebooks" ] ) == 5
|
||||||
|
notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
|
||||||
|
assert notebook.object_id == self.notebooks[ 0 ].object_id
|
||||||
|
assert notebook.name == self.notebooks[ 0 ].name
|
||||||
|
assert notebook.read_write == True
|
||||||
|
assert notebook.owner == True
|
||||||
|
assert notebook.rank == 0
|
||||||
|
|
||||||
|
assert result[ u"login_url" ] == None
|
||||||
|
assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"
|
||||||
|
|
||||||
|
rate_plan = result[ u"rate_plan" ]
|
||||||
|
assert rate_plan
|
||||||
|
assert rate_plan[ u"name" ] == u"super"
|
||||||
|
assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
|
||||||
|
|
||||||
|
assert not result.get( u"conversion" )
|
||||||
|
assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
|
||||||
|
assert len( result[ u"startup_notes" ] ) == 1
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
|
||||||
|
assert result[ u"note_read_write" ] is False
|
||||||
|
|
||||||
|
assert result[ u"notes" ]
|
||||||
|
assert len( result[ u"notes" ] ) == 1
|
||||||
|
assert u"processing" in result[ u"notes" ][ 0 ].title
|
||||||
|
assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
|
||||||
|
assert u"being processed" in result[ u"notes" ][ 0 ].contents
|
||||||
|
assert u"retry_count=4" in result[ u"notes" ][ 0 ].contents
|
||||||
|
|
||||||
|
def test_thanks_download_not_yet_paid_with_retry_timeout( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
access_id = access_id,
|
||||||
|
item_number = item_number,
|
||||||
|
retry_count = u"16",
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
# an unknown transaction id might just mean we're still waiting for the transaction to come in,
|
||||||
|
# so expect a retry
|
||||||
|
assert result[ u"user" ].username == self.user.username
|
||||||
|
assert len( result[ u"notebooks" ] ) == 5
|
||||||
|
notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
|
||||||
|
assert notebook.object_id == self.notebooks[ 0 ].object_id
|
||||||
|
assert notebook.name == self.notebooks[ 0 ].name
|
||||||
|
assert notebook.read_write == True
|
||||||
|
assert notebook.owner == True
|
||||||
|
assert notebook.rank == 0
|
||||||
|
|
||||||
|
assert result[ u"login_url" ] == None
|
||||||
|
assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"
|
||||||
|
|
||||||
|
rate_plan = result[ u"rate_plan" ]
|
||||||
|
assert rate_plan
|
||||||
|
assert rate_plan[ u"name" ] == u"super"
|
||||||
|
assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
|
||||||
|
|
||||||
|
assert not result.get( u"conversion" )
|
||||||
|
assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
|
||||||
|
assert len( result[ u"startup_notes" ] ) == 1
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
|
||||||
|
assert result[ u"note_read_write" ] is False
|
||||||
|
|
||||||
|
assert result[ u"notes" ]
|
||||||
|
assert len( result[ u"notes" ] ) == 1
|
||||||
|
assert result[ u"notes" ][ 0 ].title == u"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
|
||||||
|
|
||||||
|
def test_thanks_download_not_yet_paid_txn_id( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
txn_id = transaction_id,
|
||||||
|
item_number = item_number,
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
# an unknown transaction id might just mean we're still waiting for the transaction to come in,
|
||||||
|
# so expect a retry
|
||||||
|
assert result[ u"user" ].username == self.user.username
|
||||||
|
assert len( result[ u"notebooks" ] ) == 5
|
||||||
|
notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
|
||||||
|
assert notebook.object_id == self.notebooks[ 0 ].object_id
|
||||||
|
assert notebook.name == self.notebooks[ 0 ].name
|
||||||
|
assert notebook.read_write == True
|
||||||
|
assert notebook.owner == True
|
||||||
|
assert notebook.rank == 0
|
||||||
|
|
||||||
|
assert result[ u"login_url" ] == None
|
||||||
|
assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"
|
||||||
|
|
||||||
|
rate_plan = result[ u"rate_plan" ]
|
||||||
|
assert rate_plan
|
||||||
|
assert rate_plan[ u"name" ] == u"super"
|
||||||
|
assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
|
||||||
|
|
||||||
|
assert not result.get( u"conversion" )
|
||||||
|
assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
|
||||||
|
assert len( result[ u"startup_notes" ] ) == 1
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
|
||||||
|
assert result[ u"note_read_write" ] is False
|
||||||
|
|
||||||
|
assert result[ u"notes" ]
|
||||||
|
assert len( result[ u"notes" ] ) == 1
|
||||||
|
assert u"processing" in result[ u"notes" ][ 0 ].title
|
||||||
|
assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
|
||||||
|
assert u"being processed" in result[ u"notes" ][ 0 ].contents
|
||||||
|
assert u"retry_count=1" in result[ u"notes" ][ 0 ].contents
|
||||||
|
|
||||||
|
def test_thanks_download_not_yet_paid_txn_id_with_retry( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
txn_id = transaction_id,
|
||||||
|
item_number = item_number,
|
||||||
|
retry_count = u"3",
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
# an unknown transaction id might just mean we're still waiting for the transaction to come in,
|
||||||
|
# so expect a retry
|
||||||
|
assert result[ u"user" ].username == self.user.username
|
||||||
|
assert len( result[ u"notebooks" ] ) == 5
|
||||||
|
notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
|
||||||
|
assert notebook.object_id == self.notebooks[ 0 ].object_id
|
||||||
|
assert notebook.name == self.notebooks[ 0 ].name
|
||||||
|
assert notebook.read_write == True
|
||||||
|
assert notebook.owner == True
|
||||||
|
assert notebook.rank == 0
|
||||||
|
|
||||||
|
assert result[ u"login_url" ] == None
|
||||||
|
assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"
|
||||||
|
|
||||||
|
rate_plan = result[ u"rate_plan" ]
|
||||||
|
assert rate_plan
|
||||||
|
assert rate_plan[ u"name" ] == u"super"
|
||||||
|
assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
|
||||||
|
|
||||||
|
assert not result.get( u"conversion" )
|
||||||
|
assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
|
||||||
|
assert len( result[ u"startup_notes" ] ) == 1
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
|
||||||
|
assert result[ u"note_read_write" ] is False
|
||||||
|
|
||||||
|
assert result[ u"notes" ]
|
||||||
|
assert len( result[ u"notes" ] ) == 1
|
||||||
|
assert u"processing" in result[ u"notes" ][ 0 ].title
|
||||||
|
assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
|
||||||
|
assert u"being processed" in result[ u"notes" ][ 0 ].contents
|
||||||
|
assert u"retry_count=4" in result[ u"notes" ][ 0 ].contents
|
||||||
|
|
||||||
|
def test_thanks_download_not_yet_paid_txn_id_with_retry_timeout( self ):
|
||||||
|
access_id = u"wheeaccessid"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
txn_id = transaction_id,
|
||||||
|
item_number = item_number,
|
||||||
|
retry_count = u"16",
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
# an unknown transaction id might just mean we're still waiting for the transaction to come in,
|
||||||
|
# so expect a retry
|
||||||
|
assert result[ u"user" ].username == self.user.username
|
||||||
|
assert len( result[ u"notebooks" ] ) == 5
|
||||||
|
notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
|
||||||
|
assert notebook.object_id == self.notebooks[ 0 ].object_id
|
||||||
|
assert notebook.name == self.notebooks[ 0 ].name
|
||||||
|
assert notebook.read_write == True
|
||||||
|
assert notebook.owner == True
|
||||||
|
assert notebook.rank == 0
|
||||||
|
|
||||||
|
assert result[ u"login_url" ] == None
|
||||||
|
assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"
|
||||||
|
|
||||||
|
rate_plan = result[ u"rate_plan" ]
|
||||||
|
assert rate_plan
|
||||||
|
assert rate_plan[ u"name" ] == u"super"
|
||||||
|
assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
|
||||||
|
|
||||||
|
assert not result.get( u"conversion" )
|
||||||
|
assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
|
||||||
|
assert len( result[ u"startup_notes" ] ) == 1
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
|
||||||
|
assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
|
||||||
|
assert result[ u"note_read_write" ] is False
|
||||||
|
|
||||||
|
assert result[ u"notes" ]
|
||||||
|
assert len( result[ u"notes" ] ) == 1
|
||||||
|
assert result[ u"notes" ][ 0 ].title == u"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
|
||||||
|
|
||||||
|
def test_thanks_download_missing_txn_id_missing_access_id( self ):
|
||||||
|
item_number = u"5000"
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
item_number = item_number,
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
assert u"error" in result
|
||||||
|
|
||||||
|
def test_thanks_download_invalid_access_id( self ):
|
||||||
|
access_id = u"invalid access id"
|
||||||
|
item_number = u"5000"
|
||||||
|
transaction_id = u"txn"
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/users/thanks_download", dict(
|
||||||
|
access_id = access_id,
|
||||||
|
item_number = item_number,
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
assert u"error" in result
|
||||||
|
|
||||||
def test_rate_plan( self ):
|
def test_rate_plan( self ):
|
||||||
plan_index = 1
|
plan_index = 1
|
||||||
rate_plan = cherrypy.root.users.rate_plan( plan_index )
|
rate_plan = cherrypy.root.users.rate_plan( plan_index )
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
from Persistent import Persistent, quote
|
||||||
|
|
||||||
|
|
||||||
|
class Download_access( Persistent ):
|
||||||
|
"""
|
||||||
|
Access for a particular user to a downloadable product. This object is used to create unique
|
||||||
|
per-customer product download links without requiring the user to have a Luminotes account.
|
||||||
|
"""
|
||||||
|
def __init__( self, object_id, revision = None, item_number = None, transaction_id = None ):
|
||||||
|
"""
|
||||||
|
Create a download access record with the given id.
|
||||||
|
|
||||||
|
@type object_id: unicode
|
||||||
|
@param object_id: id of the download access
|
||||||
|
@type revision: datetime or NoneType
|
||||||
|
@param revision: revision timestamp of the object (optional, defaults to now)
|
||||||
|
@type item_number: unicode or NoneType
|
||||||
|
@param item_number: number of the item to which download access is granted (optional)
|
||||||
|
@type transaction_id: unicode or NoneType
|
||||||
|
@param transaction_id: payment processor id for the transaction used to pay for this download
|
||||||
|
(optional)
|
||||||
|
@rtype: Download_access
|
||||||
|
@return: newly constructed download access object
|
||||||
|
"""
|
||||||
|
Persistent.__init__( self, object_id, revision )
|
||||||
|
self.__item_number = item_number
|
||||||
|
self.__transaction_id = transaction_id
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create( object_id, item_number = None, transaction_id = None ):
|
||||||
|
"""
|
||||||
|
Convenience constructor for creating a new download access object.
|
||||||
|
|
||||||
|
@type item_number: unicode or NoneType
|
||||||
|
@param item_number: number of the item to which download access is granted (optional)
|
||||||
|
@type transaction_id: unicode or NoneType
|
||||||
|
@param transaction_id: payment processor id for the transaction used to pay for this download
|
||||||
|
(optional)
|
||||||
|
@rtype: Download_access
|
||||||
|
@return: newly constructed download access object
|
||||||
|
"""
|
||||||
|
return Download_access( object_id, item_number = item_number, transaction_id = transaction_id )
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sql_load( object_id, revision = None ):
|
||||||
|
# download access objects don't store old revisions
|
||||||
|
if revision:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
return "select id, revision, item_number, transaction_id from download_access where id = %s;" % quote( object_id )
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sql_load_by_transaction_id( transaction_id ):
|
||||||
|
return "select id, revision, item_number, transaction_id from download_access where transaction_id = %s;" % quote( transaction_id )
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sql_id_exists( object_id, revision = None ):
|
||||||
|
if revision:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
return "select id from download_access where id = %s;" % quote( object_id )
|
||||||
|
|
||||||
|
def sql_exists( self ):
|
||||||
|
return Download_access.sql_id_exists( self.object_id )
|
||||||
|
|
||||||
|
def sql_create( self ):
|
||||||
|
return "insert into download_access ( id, revision, item_number, transaction_id ) values ( %s, %s, %s, %s );" % \
|
||||||
|
( quote( self.object_id ), quote( self.revision ), quote( self.__item_number ), quote( self.__transaction_id ) )
|
||||||
|
|
||||||
|
def sql_update( self ):
|
||||||
|
return "update download_access set revision = %s, item_number = %s, transaction_id = %s where id = %s;" % \
|
||||||
|
( quote( self.revision ), quote( self.__item_number ), quote( self.__transaction_id ), quote( self.object_id ) )
|
||||||
|
|
||||||
|
item_number = property( lambda self: self.__item_number )
|
||||||
|
transaction_id = property( lambda self: self.__transaction_id )
|
|
@ -0,0 +1,8 @@
|
||||||
|
CREATE TABLE download_access (
|
||||||
|
id text NOT NULL,
|
||||||
|
revision timestamp with time zone NOT NULL,
|
||||||
|
item_number text,
|
||||||
|
transaction_id text
|
||||||
|
);
|
||||||
|
ALTER TABLE ONLY download_access ADD CONSTRAINT download_access_pkey PRIMARY KEY (id);
|
||||||
|
CREATE INDEX download_access_transaction_id_index ON download_access USING btree (transaction_id);
|
|
@ -7,6 +7,7 @@ DROP TABLE note;
|
||||||
DROP VIEW notebook_current;
|
DROP VIEW notebook_current;
|
||||||
DROP TABLE notebook;
|
DROP TABLE notebook;
|
||||||
DROP TABLE password_reset;
|
DROP TABLE password_reset;
|
||||||
|
DROP TABLE download_access;
|
||||||
DROP TABLE user_notebook;
|
DROP TABLE user_notebook;
|
||||||
DROP TABLE user_group;
|
DROP TABLE user_group;
|
||||||
DROP TABLE invite;
|
DROP TABLE invite;
|
||||||
|
|
|
@ -180,6 +180,21 @@ CREATE TABLE password_reset (
|
||||||
|
|
||||||
ALTER TABLE public.password_reset OWNER TO luminotes;
|
ALTER TABLE public.password_reset OWNER TO luminotes;
|
||||||
|
|
||||||
|
|
||||||
|
-- Name: download_access; Type: TABLE; Schema: public; Owner: luminotes; Tablespace:
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE download_access (
|
||||||
|
id text NOT NULL,
|
||||||
|
revision timestamp with time zone NOT NULL,
|
||||||
|
item_number text,
|
||||||
|
transaction_id text
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.download_access OWNER TO luminotes;
|
||||||
|
|
||||||
|
--
|
||||||
--
|
--
|
||||||
-- Name: user_group; Type: TABLE; Schema: public; Owner: luminotes; Tablespace:
|
-- Name: user_group; Type: TABLE; Schema: public; Owner: luminotes; Tablespace:
|
||||||
--
|
--
|
||||||
|
@ -256,6 +271,14 @@ ALTER TABLE ONLY password_reset
|
||||||
ADD CONSTRAINT password_reset_pkey PRIMARY KEY (id);
|
ADD CONSTRAINT password_reset_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: download_access_pkey; Type: CONSTRAINT; Schema: public; Owner: luminotes; Tablespace:
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY download_access
|
||||||
|
ADD CONSTRAINT download_access_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: user_notebook_pkey; Type: CONSTRAINT; Schema: public; Owner: luminotes; Tablespace:
|
-- Name: user_notebook_pkey; Type: CONSTRAINT; Schema: public; Owner: luminotes; Tablespace:
|
||||||
--
|
--
|
||||||
|
@ -327,6 +350,13 @@ CREATE INDEX note_notebook_id_title_index ON note USING btree (notebook_id, md5(
|
||||||
CREATE INDEX password_reset_email_address_index ON password_reset USING btree (email_address);
|
CREATE INDEX password_reset_email_address_index ON password_reset USING btree (email_address);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: download_access_transaction_id_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace:
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX download_access_transaction_id_index ON download_access USING btree (transaction_id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: search_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace:
|
-- Name: search_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
|
@ -132,6 +132,17 @@ CREATE TABLE password_reset (
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- Name: download_access; Type: TABLE; Schema: public; Owner: luminotes; Tablespace:
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE download_access (
|
||||||
|
id text NOT NULL,
|
||||||
|
revision timestamp with time zone NOT NULL,
|
||||||
|
item_number text,
|
||||||
|
transaction_id text
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: user_group; Type: TABLE; Schema: public; Owner: luminotes; Tablespace:
|
-- Name: user_group; Type: TABLE; Schema: public; Owner: luminotes; Tablespace:
|
||||||
--
|
--
|
||||||
|
@ -213,6 +224,13 @@ CREATE INDEX note_notebook_id_startup_index ON note (notebook_id, startup);
|
||||||
CREATE INDEX note_notebook_id_title_index ON note (notebook_id, title);
|
CREATE INDEX note_notebook_id_title_index ON note (notebook_id, title);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: password_reset_id_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace:
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX password_reset_id_index ON password_reset (id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: password_reset_email_address_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace:
|
-- Name: password_reset_email_address_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace:
|
||||||
--
|
--
|
||||||
|
@ -220,6 +238,18 @@ CREATE INDEX note_notebook_id_title_index ON note (notebook_id, title);
|
||||||
CREATE INDEX password_reset_email_address_index ON password_reset (email_address);
|
CREATE INDEX password_reset_email_address_index ON password_reset (email_address);
|
||||||
|
|
||||||
|
|
||||||
|
-- Name: download_access_id_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace:
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX download_access_id_index ON password_reset (id);
|
||||||
|
|
||||||
|
|
||||||
|
-- Name: download_access_transaction_id_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace:
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX download_access_transaction_id_index ON download_access (transaction_id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: search_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace:
|
-- Name: search_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
from model.Download_access import Download_access
|
||||||
|
|
||||||
|
|
||||||
|
class Test_download_access( object ):
|
||||||
|
def setUp( self ):
|
||||||
|
self.object_id = u"17"
|
||||||
|
self.item_number = u"999"
|
||||||
|
self.transaction_id = u"foooooooo234"
|
||||||
|
|
||||||
|
self.download_access = Download_access.create( self.object_id, self.item_number, self.transaction_id )
|
||||||
|
|
||||||
|
def test_create( self ):
|
||||||
|
assert self.download_access.object_id == self.object_id
|
||||||
|
assert self.download_access.item_number == self.item_number
|
||||||
|
assert self.download_access.transaction_id == self.transaction_id
|
|
@ -4,9 +4,12 @@ from config.Version import VERSION
|
||||||
|
|
||||||
|
|
||||||
class Download_page( Product_page ):
|
class Download_page( Product_page ):
|
||||||
def __init__( self, user, notebooks, first_notebook, login_url, logout_url, rate_plan, groups, download_button ):
|
def __init__( self, user, notebooks, first_notebook, login_url, logout_url, rate_plan, groups, download_products, upgrade = False ):
|
||||||
MEGABYTE = 1024 * 1024
|
MEGABYTE = 1024 * 1024
|
||||||
|
|
||||||
|
# for now, just assume there's a single download package
|
||||||
|
download_button = download_products[ 0 ].get( "button" )
|
||||||
|
|
||||||
Product_page.__init__(
|
Product_page.__init__(
|
||||||
self,
|
self,
|
||||||
user,
|
user,
|
||||||
|
@ -33,6 +36,17 @@ class Download_page( Product_page ):
|
||||||
class_ = u"upgrade_subtitle",
|
class_ = u"upgrade_subtitle",
|
||||||
),
|
),
|
||||||
Div(
|
Div(
|
||||||
|
upgrade and P(
|
||||||
|
B( "Upgrading:" ),
|
||||||
|
u"""
|
||||||
|
If you have already purchased Luminotes Desktop and would like to download a newer
|
||||||
|
version, simply follow the link you received after your purchase. Can't find
|
||||||
|
the link or need help? Please
|
||||||
|
""",
|
||||||
|
A( u"contact support", href = u"/contact_info" ),
|
||||||
|
u"for assistance.",
|
||||||
|
class_ = u"upgrade_text",
|
||||||
|
) or None,
|
||||||
Div(
|
Div(
|
||||||
Img( src = u"/static/images/installer_screenshot.png", width = u"350", height = u"273" ),
|
Img( src = u"/static/images/installer_screenshot.png", width = u"350", height = u"273" ),
|
||||||
class_ = u"desktop_screenshot",
|
class_ = u"desktop_screenshot",
|
||||||
|
|
|
@ -17,7 +17,7 @@ class Header( Div ):
|
||||||
A( title_image, href = u"http://luminotes.com/", target = "_new" ),
|
A( title_image, href = u"http://luminotes.com/", target = "_new" ),
|
||||||
Div(
|
Div(
|
||||||
u"version", VERSION, u" | ",
|
u"version", VERSION, u" | ",
|
||||||
A( u"upgrade", href = u"http://luminotes.com/pricing", 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"support", href = u"http://luminotes.com/support", 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",
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
from Tags import Html, Head, Meta, H3, P
|
||||||
|
|
||||||
|
|
||||||
|
class Processing_download_note( Html ):
|
||||||
|
def __init__( self, download_access_id, item_number, retry_count ):
|
||||||
|
if not retry_count:
|
||||||
|
retry_count = 0
|
||||||
|
|
||||||
|
retry_count += 1
|
||||||
|
|
||||||
|
Html.__init__(
|
||||||
|
self,
|
||||||
|
Head(
|
||||||
|
Meta(
|
||||||
|
http_equiv = u"Refresh",
|
||||||
|
content = u"2; URL=/users/thanks_download?access_id=%s&item_number=%s&retry_count=%s" %
|
||||||
|
( download_access_id, item_number, retry_count ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
H3( u"processing..." ),
|
||||||
|
P(
|
||||||
|
"""
|
||||||
|
Your payment is being processed. This shouldn't take more than a minute. Please wait...
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
)
|
|
@ -0,0 +1,30 @@
|
||||||
|
from Tags import Span, H3, P, A
|
||||||
|
|
||||||
|
|
||||||
|
class Thanks_download_error_note( Span ):
|
||||||
|
def __init__( self ):
|
||||||
|
Span.__init__(
|
||||||
|
self,
|
||||||
|
H3( u"thank you" ),
|
||||||
|
P(
|
||||||
|
u"""
|
||||||
|
Thank you for purchasing Luminotes Desktop!
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
P(
|
||||||
|
u"""
|
||||||
|
Luminotes has not yet received confirmation of your payment. Please
|
||||||
|
check back in a few minutes by refreshing this page, or check your
|
||||||
|
email for a Luminotes Desktop download message.
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
P(
|
||||||
|
"""
|
||||||
|
If your payment is not received within the next few minutes, please
|
||||||
|
""",
|
||||||
|
A( u"contact support", href = u"/contact_info", target = "_top" ),
|
||||||
|
u"""
|
||||||
|
for assistance.
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
)
|
|
@ -0,0 +1,36 @@
|
||||||
|
from Tags import Span, H3, P, A
|
||||||
|
from config.Version import VERSION
|
||||||
|
|
||||||
|
|
||||||
|
class Thanks_download_note( Span ):
|
||||||
|
def __init__( self, download_url ):
|
||||||
|
Span.__init__(
|
||||||
|
self,
|
||||||
|
H3( u"thank you" ),
|
||||||
|
P(
|
||||||
|
u"""
|
||||||
|
Thank you for purchasing Luminotes Desktop!
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
P(
|
||||||
|
A( u"Download Luminotes Desktop version %s" % VERSION, href = download_url ),
|
||||||
|
"""
|
||||||
|
and get started taking notes with your own personal wiki.
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
P(
|
||||||
|
u"""
|
||||||
|
It's a good idea to bookmark this page so that you can download
|
||||||
|
Luminotes Desktop or upgrade to new versions as they are released.
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
P(
|
||||||
|
u"""
|
||||||
|
If you have any questions about Luminotes Desktop or your purchase, please
|
||||||
|
""",
|
||||||
|
A( u"contact support", href = u"/contact_info", target = "_top" ),
|
||||||
|
u"""
|
||||||
|
for assistance.
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
)
|
Reference in New Issue