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 code for supporting product download access.
|
||||
|
||||
1.5.0 beta 1: August 27, 2008
|
||||
* 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.download_button":
|
||||
"""
|
||||
""",
|
||||
},
|
||||
"/files/download": {
|
||||
"stream_response": True,
|
||||
|
|
|
@ -18,6 +18,7 @@ from Users import grab_user_id, Access_error
|
|||
from Expire import strongly_expire
|
||||
from model.File import File
|
||||
from model.User import User
|
||||
from model.Download_access import Download_access
|
||||
from view.Upload_page import Upload_page
|
||||
from view.Blank_page import Blank_page
|
||||
from view.Json import Json
|
||||
|
@ -249,7 +250,7 @@ class Files( object ):
|
|||
"""
|
||||
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.
|
||||
|
||||
|
@ -257,11 +258,14 @@ class Files( object ):
|
|||
@param database: database that file metadata is stored in
|
||||
@type users: controller.Users
|
||||
@param users: controller for all users
|
||||
@type download_products: [ { "name": unicode, ... } ]
|
||||
@param download_products: list of configured downloadable products
|
||||
@rtype: Files
|
||||
@return: newly constructed Files
|
||||
"""
|
||||
self.__database = database
|
||||
self.__users = users
|
||||
self.__download_products = download_products
|
||||
|
||||
@expose()
|
||||
@end_transaction
|
||||
|
@ -331,6 +335,68 @@ class Files( object ):
|
|||
|
||||
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 )
|
||||
@end_transaction
|
||||
@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.payment_email", u"" ),
|
||||
settings[ u"global" ].get( u"luminotes.rate_plans", [] ),
|
||||
settings[ u"global" ].get( u"luminotes.download_products", [] ),
|
||||
)
|
||||
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.__forums = Forums( database, self.__users )
|
||||
self.__suppress_exceptions = suppress_exceptions # used for unit tests
|
||||
|
@ -155,6 +160,24 @@ class Root( object ):
|
|||
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 )
|
||||
@strongly_expire
|
||||
@end_transaction
|
||||
|
@ -350,9 +373,10 @@ class Root( object ):
|
|||
@end_transaction
|
||||
@grab_user_id
|
||||
@validate(
|
||||
upgrade = Valid_bool( 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.
|
||||
"""
|
||||
|
@ -363,7 +387,8 @@ class Root( object ):
|
|||
else:
|
||||
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
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ import re
|
|||
import urllib
|
||||
import urllib2
|
||||
import cherrypy
|
||||
import smtplib
|
||||
from email import Message
|
||||
from pytz import utc
|
||||
from datetime import datetime, timedelta
|
||||
from model.User import User
|
||||
|
@ -9,6 +11,7 @@ from model.Group import Group
|
|||
from model.Notebook import Notebook
|
||||
from model.Note import Note
|
||||
from model.Password_reset import Password_reset
|
||||
from model.Download_access import Download_access
|
||||
from model.Invite import Invite
|
||||
from Expose import expose
|
||||
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.Thanks_note import Thanks_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_download_note import Processing_download_note
|
||||
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.
|
||||
"""
|
||||
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.
|
||||
|
||||
|
@ -195,8 +201,10 @@ class Users( object ):
|
|||
@param support_email: email address for support requests
|
||||
@type payment_email: unicode
|
||||
@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
|
||||
@type download_products: [ { "name": unicode, ... } ]
|
||||
@param download_products: list of configured downloadable products
|
||||
@rtype: Users
|
||||
@return: newly constructed Users
|
||||
"""
|
||||
|
@ -206,6 +214,7 @@ class Users( object ):
|
|||
self.__support_email = support_email
|
||||
self.__payment_email = payment_email
|
||||
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 ):
|
||||
"""
|
||||
|
@ -802,9 +811,6 @@ class Users( object ):
|
|||
@raise Password_reset_error: an error occured when sending the password reset email
|
||||
@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
|
||||
users = self.__database.select_many( User, User.sql_load_by_email_address( email_address ) )
|
||||
|
||||
|
@ -1252,6 +1258,9 @@ class Users( object ):
|
|||
|
||||
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 )
|
||||
@end_transaction
|
||||
def paypal_notify( self, **params ):
|
||||
|
@ -1262,9 +1271,6 @@ class Users( object ):
|
|||
record in the database with their new rate plan. paypal_notify() is
|
||||
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
|
||||
payment_status = params.get( u"payment_status" )
|
||||
if payment_status == u"Refunded":
|
||||
|
@ -1283,19 +1289,117 @@ class Users( object ):
|
|||
raise Payment_error( u"unsupported mc_currency", params )
|
||||
|
||||
# verify item_number
|
||||
plan_index = params.get( u"item_number" )
|
||||
if plan_index == None or plan_index == u"":
|
||||
item_number = params.get( u"item_number" )
|
||||
if item_number == None or item_number == u"":
|
||||
return dict() # ignore this transaction if there's no item number
|
||||
|
||||
try:
|
||||
plan_index = int( plan_index )
|
||||
int( item_number )
|
||||
except ValueError:
|
||||
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
|
||||
rate_plan = self.__rate_plans[ plan_index ]
|
||||
fee = u"%0.2f" % rate_plan[ u"fee" ]
|
||||
yearly_fee = u"%0.2f" % rate_plan[ u"yearly_fee" ]
|
||||
mc_gross = params.get( u"mc_gross" )
|
||||
|
@ -1329,9 +1433,9 @@ class Users( object ):
|
|||
encoded_params = urllib.urlencode( params )
|
||||
|
||||
# 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_file = urllib2.urlopen( PAYPAL_URL, encoded_params )
|
||||
request_file = urllib2.urlopen( self.PAYPAL_URL, encoded_params )
|
||||
result = request_file.read()
|
||||
|
||||
if result != u"VERIFIED":
|
||||
|
@ -1366,8 +1470,6 @@ class Users( object ):
|
|||
else:
|
||||
raise Payment_error( "unknown txn_type", params )
|
||||
|
||||
return dict()
|
||||
|
||||
def update_groups( self, user ):
|
||||
"""
|
||||
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 ):
|
||||
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 )
|
||||
@end_transaction
|
||||
@grab_user_id
|
||||
|
|
|
@ -96,6 +96,21 @@ class Test_controller( object ):
|
|||
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"stream_response": True,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf8 -*-
|
||||
|
||||
import os
|
||||
import time
|
||||
import types
|
||||
import urllib
|
||||
|
@ -14,6 +15,7 @@ from model.Note import Note
|
|||
from model.User import User
|
||||
from model.Invite import Invite
|
||||
from model.File import File
|
||||
from model.Download_access import Download_access
|
||||
from controller.Notebooks import Access_error
|
||||
from controller.Files import Upload_file, Parse_error
|
||||
|
||||
|
@ -90,6 +92,11 @@ class Test_files( Test_controller ):
|
|||
Upload_file.exists = exists
|
||||
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_notebooks()
|
||||
self.database.commit()
|
||||
|
@ -128,6 +135,8 @@ class Test_files( Test_controller ):
|
|||
if self.upload_thread:
|
||||
self.upload_thread.join()
|
||||
|
||||
os.remove( u"products/test.exe" )
|
||||
|
||||
def test_download( self, filename = None, quote_filename = None, file_data = None, preview = None ):
|
||||
self.login()
|
||||
|
||||
|
@ -327,6 +336,139 @@ class Test_files( Test_controller ):
|
|||
|
||||
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 ):
|
||||
self.login()
|
||||
|
||||
|
|
|
@ -538,3 +538,9 @@ class Test_root( Test_controller ):
|
|||
result = self.http_get( "/i/%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 Test_controller import Test_controller
|
||||
import Stub_urllib2
|
||||
from config.Version import VERSION
|
||||
from model.User import User
|
||||
from model.Group import Group
|
||||
from model.Notebook import Notebook
|
||||
from model.Note import Note
|
||||
from model.Password_reset import Password_reset
|
||||
from model.Download_access import Download_access
|
||||
from model.Invite import Invite
|
||||
from controller.Users import Invite_error, Payment_error
|
||||
import controller.Users as Users
|
||||
|
@ -3944,6 +3946,150 @@ class Test_users( Test_controller ):
|
|||
user = self.database.load( User, self.user.object_id )
|
||||
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 ):
|
||||
self.user.rate_plan = 1
|
||||
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"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 ):
|
||||
plan_index = 1
|
||||
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 TABLE notebook;
|
||||
DROP TABLE password_reset;
|
||||
DROP TABLE download_access;
|
||||
DROP TABLE user_notebook;
|
||||
DROP TABLE user_group;
|
||||
DROP TABLE invite;
|
||||
|
|
|
@ -180,6 +180,21 @@ CREATE TABLE password_reset (
|
|||
|
||||
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:
|
||||
--
|
||||
|
@ -256,6 +271,14 @@ ALTER TABLE ONLY password_reset
|
|||
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:
|
||||
--
|
||||
|
@ -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);
|
||||
|
||||
|
||||
--
|
||||
-- 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:
|
||||
--
|
||||
|
|
|
@ -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:
|
||||
--
|
||||
|
@ -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);
|
||||
|
||||
|
||||
--
|
||||
-- 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:
|
||||
--
|
||||
|
@ -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);
|
||||
|
||||
|
||||
-- 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:
|
||||
--
|
||||
|
|
|
@ -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 ):
|
||||
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
|
||||
|
||||
# for now, just assume there's a single download package
|
||||
download_button = download_products[ 0 ].get( "button" )
|
||||
|
||||
Product_page.__init__(
|
||||
self,
|
||||
user,
|
||||
|
@ -33,6 +36,17 @@ class Download_page( Product_page ):
|
|||
class_ = u"upgrade_subtitle",
|
||||
),
|
||||
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(
|
||||
Img( src = u"/static/images/installer_screenshot.png", width = u"350", height = u"273" ),
|
||||
class_ = u"desktop_screenshot",
|
||||
|
|
|
@ -17,7 +17,7 @@ class Header( Div ):
|
|||
A( title_image, href = u"http://luminotes.com/", target = "_new" ),
|
||||
Div(
|
||||
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"blog", href = u"http://luminotes.com/blog", target = "_new" ),
|
||||
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