witten
/
luminotes
Archived
1
0
Fork 0

Lots more work on the payment code necessary to support Luminotes Desktop.

This commit is contained in:
Dan Helfman 2008-09-11 00:36:12 -07:00
parent 1ae3596f3d
commit 9247683a72
21 changed files with 1407 additions and 29 deletions

10
NEWS
View File

@ -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.

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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()

View File

@ -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

View File

@ -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 )

75
model/Download_access.py Normal file
View File

@ -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 )

8
model/delta/1.5.0.sql Normal file
View File

@ -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);

View File

@ -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;

View File

@ -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:
--

View File

@ -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:
--

View File

@ -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

0
products/.empty Normal file
View File

View File

@ -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",

View File

@ -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",

View File

@ -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...
""",
),
)

View File

@ -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.
""",
),
)

View File

@ -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.
""",
),
)