Browse Source

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

Dan Helfman 10 years ago
parent
commit
9247683a72

+ 9
- 1
NEWS View File

@@ -1,5 +1,13 @@
1
-1.5.0 beta 2: 
1
+1.5.0: 
2
+ * Fixed a Luminotes Desktop Internet Explorer bug in which note links within
3
+   the "download as html" document pointed to notes in the local Luminotes
4
+   installation instead of notes within the stand-alone document.
5
+ * Fixed a bug in which Luminotes Desktop file attachment did not always work
6
+   due to incorrect upload progress reporting.
7
+ * In the revision changes pulldown, no longer showing "by desktopuser" in
8
+   Luminotes Desktop.
2 9
  * Added a Luminotes Desktop download page.
10
+ * Added code for supporting product download access.
3 11
 
4 12
 1.5.0 beta 1: August 27, 2008
5 13
  * Completed the Luminotes Desktop Windows installer.

+ 17
- 3
config/Common.py View File

@@ -107,12 +107,26 @@ settings = {
107 107
           """,
108 108
       },
109 109
     ],
110
+    "luminotes.download_products": [
111
+      {
112
+        "name": "Luminotes Desktop",
113
+        "designed_for": "individuals",
114
+        "storage_quota_bytes": None,
115
+        "included_users": 1,
116
+        "notebook_sharing": False,
117
+        "notebook_collaboration": False,
118
+        "user_admin": False,
119
+        "fee": "20.00",
120
+        "item_number": "5000",
121
+        "filename": "luminotes.exe",
122
+        "button":
123
+          """
124
+          """,
125
+      },
126
+    ],
110 127
     "luminotes.unsubscribe_button":
111 128
       """
112 129
       """,
113
-    "luminotes.download_button":
114
-      """
115
-      """,
116 130
   },
117 131
   "/files/download": {
118 132
     "stream_response": True,

+ 67
- 1
controller/Files.py View File

@@ -18,6 +18,7 @@ from Users import grab_user_id, Access_error
18 18
 from Expire import strongly_expire
19 19
 from model.File import File
20 20
 from model.User import User
21
+from model.Download_access import Download_access
21 22
 from view.Upload_page import Upload_page
22 23
 from view.Blank_page import Blank_page
23 24
 from view.Json import Json
@@ -249,7 +250,7 @@ class Files( object ):
249 250
   """
250 251
   Controller for dealing with uploaded files, corresponding to the "/files" URL.
251 252
   """
252
-  def __init__( self, database, users ):
253
+  def __init__( self, database, users, download_products ):
253 254
     """
254 255
     Create a new Files object.
255 256
 
@@ -257,11 +258,14 @@ class Files( object ):
257 258
     @param database: database that file metadata is stored in
258 259
     @type users: controller.Users
259 260
     @param users: controller for all users
261
+    @type download_products: [ { "name": unicode, ... } ]
262
+    @param download_products: list of configured downloadable products
260 263
     @rtype: Files
261 264
     @return: newly constructed Files
262 265
     """
263 266
     self.__database = database
264 267
     self.__users = users
268
+    self.__download_products = download_products
265 269
 
266 270
   @expose()
267 271
   @end_transaction
@@ -331,6 +335,68 @@ class Files( object ):
331 335
 
332 336
     return stream()
333 337
 
338
+  @expose()
339
+  @end_transaction
340
+  @validate(
341
+    access_id = Valid_id(),
342
+    item_number = Valid_int(),
343
+  )
344
+  def download_product( self, access_id, item_number ):
345
+    """
346
+    Return the contents of downloadable product file.
347
+
348
+    @type access_id: unicode
349
+    @param access_id: id of download access object that grants access to the file
350
+    @type item_number: int or int as unicode
351
+    @param item_number: number of the downloadable product
352
+    @rtype: generator
353
+    @return: file data
354
+    @raise Access_error: the access_id is unknown, doesn't grant access to the file, or the
355
+           item_number is unknown
356
+    """
357
+    # release the session lock before beginning to stream the download. otherwise, if the
358
+    # download is cancelled before it's done, the lock won't be released
359
+    try:
360
+      cherrypy.session.release_lock()
361
+    except ( KeyError, OSError ):
362
+      pass
363
+
364
+    # find the product corresponding to the given item_number
365
+    products = [
366
+      product for product in self.__download_products
367
+      if unicode( item_number ) == product.get( u"item_number" )
368
+    ]
369
+    if len( products ) == 0:
370
+      raise Access_error()
371
+
372
+    product = products[ 0 ]
373
+
374
+    # load the download_access object corresponding to the given id
375
+    download_access = self.__database.load( Download_access, access_id )
376
+    if download_access is None:
377
+      raise Access_error()
378
+
379
+    public_filename = product[ u"filename" ].encode( "utf8" )
380
+    local_filename = u"products/%s" % product[ u"filename" ]
381
+
382
+    if not os.path.exists( local_filename ):
383
+      raise Access_error()
384
+
385
+    cherrypy.response.headerMap[ u"Content-Type" ] = u"application/octet-stream"
386
+    cherrypy.response.headerMap[ u"Content-Disposition" ] = 'attachment; filename="%s"' % public_filename
387
+    cherrypy.response.headerMap[ u"Content-Length" ] = os.path.getsize( local_filename )
388
+
389
+    def stream():
390
+      CHUNK_SIZE = 8192
391
+      local_file = file( local_filename, "rb" )
392
+
393
+      while True:
394
+        data = local_file.read( CHUNK_SIZE )
395
+        if len( data ) == 0: break
396
+        yield data        
397
+
398
+    return stream()
399
+
334 400
   @expose( view = File_preview_page )
335 401
   @end_transaction
336 402
   @grab_user_id

+ 28
- 3
controller/Root.py View File

@@ -49,9 +49,14 @@ class Root( object ):
49 49
       settings[ u"global" ].get( u"luminotes.support_email", u"" ),
50 50
       settings[ u"global" ].get( u"luminotes.payment_email", u"" ),
51 51
       settings[ u"global" ].get( u"luminotes.rate_plans", [] ),
52
+      settings[ u"global" ].get( u"luminotes.download_products", [] ),
52 53
     )
53 54
     self.__groups = Groups( database, self.__users )
54
-    self.__files = Files( database, self.__users )
55
+    self.__files = Files(
56
+      database,
57
+      self.__users,
58
+      settings[ u"global" ].get( u"luminotes.download_products", [] ),
59
+    )
55 60
     self.__notebooks = Notebooks( database, self.__users, self.__files, settings[ u"global" ].get( u"luminotes.https_url", u"" ) )
56 61
     self.__forums = Forums( database, self.__users )
57 62
     self.__suppress_exceptions = suppress_exceptions # used for unit tests
@@ -155,6 +160,24 @@ class Root( object ):
155 160
       redirect = u"/users/redeem_invite/%s" % invite_id,
156 161
     )
157 162
 
163
+  @expose()
164
+  def d( self, download_access_id ):
165
+    """
166
+    Redirect to the product download thanks URL, based on the given download access id. The sole
167
+    purpose of this method is to shorten product download URLs sent by email so email clients don't
168
+    wrap them.
169
+    """
170
+    # if the value looks like an id, it's a download access id, so redirect
171
+    try:
172
+      validator = Valid_id()
173
+      download_access_id = validator( download_access_id )
174
+    except ValueError:
175
+      raise cherrypy.NotFound
176
+
177
+    return dict(
178
+      redirect = u"/users/download_thanks/access_id=%s" % download_access_id,
179
+    )
180
+
158 181
   @expose( view = Front_page )
159 182
   @strongly_expire
160 183
   @end_transaction
@@ -350,9 +373,10 @@ class Root( object ):
350 373
   @end_transaction
351 374
   @grab_user_id
352 375
   @validate(
376
+    upgrade = Valid_bool( none_okay = True ),
353 377
     user_id = Valid_id( none_okay = True ),
354 378
   )
355
-  def download( self, user_id = None ):
379
+  def download( self, upgrade = False, user_id = None ):
356 380
     """
357 381
     Provide the information necessary to display the Luminotes download page.
358 382
     """
@@ -363,7 +387,8 @@ class Root( object ):
363 387
     else:
364 388
       result[ "first_notebook" ] = None
365 389
 
366
-    result[ "download_button" ] = self.__settings[ u"global" ].get( u"luminotes.download_button" )
390
+    result[ "download_products" ] = self.__settings[ u"global" ].get( u"luminotes.download_products" )
391
+    result[ "upgrade" ] = upgrade
367 392
 
368 393
     return result
369 394
 

+ 208
- 19
controller/Users.py View File

@@ -2,6 +2,8 @@ import re
2 2
 import urllib
3 3
 import urllib2
4 4
 import cherrypy
5
+import smtplib
6
+from email import Message
5 7
 from pytz import utc
6 8
 from datetime import datetime, timedelta
7 9
 from model.User import User
@@ -9,6 +11,7 @@ from model.Group import Group
9 11
 from model.Notebook import Notebook
10 12
 from model.Note import Note
11 13
 from model.Password_reset import Password_reset
14
+from model.Download_access import Download_access
12 15
 from model.Invite import Invite
13 16
 from Expose import expose
14 17
 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
21 24
 from view.Blank_page import Blank_page
22 25
 from view.Thanks_note import Thanks_note
23 26
 from view.Thanks_error_note import Thanks_error_note
27
+from view.Thanks_download_note import Thanks_download_note
28
+from view.Thanks_download_error_note import Thanks_download_error_note
24 29
 from view.Processing_note import Processing_note
30
+from view.Processing_download_note import Processing_download_note
25 31
 from view.Form_submit_page import Form_submit_page
26 32
 
27 33
 
@@ -181,7 +187,7 @@ class Users( object ):
181 187
   """
182 188
   Controller for dealing with users, corresponding to the "/users" URL.
183 189
   """
184
-  def __init__( self, database, http_url, https_url, support_email, payment_email, rate_plans ):
190
+  def __init__( self, database, http_url, https_url, support_email, payment_email, rate_plans, download_products ):
185 191
     """
186 192
     Create a new Users object.
187 193
 
@@ -195,8 +201,10 @@ class Users( object ):
195 201
     @param support_email: email address for support requests
196 202
     @type payment_email: unicode
197 203
     @param payment_email: email address for payment
198
-    @type rate_plans: [ { "name": unicode, "storage_quota_bytes": int } ]
204
+    @type rate_plans: [ { "name": unicode, ... } ]
199 205
     @param rate_plans: list of configured rate plans
206
+    @type download_products: [ { "name": unicode, ... } ]
207
+    @param download_products: list of configured downloadable products
200 208
     @rtype: Users
201 209
     @return: newly constructed Users
202 210
     """
@@ -206,6 +214,7 @@ class Users( object ):
206 214
     self.__support_email = support_email
207 215
     self.__payment_email = payment_email
208 216
     self.__rate_plans = rate_plans
217
+    self.__download_products = download_products
209 218
 
210 219
   def create_user( self, username, password = None, password_repeat = None, email_address = None, initial_rate_plan = None ):
211 220
     """
@@ -802,9 +811,6 @@ class Users( object ):
802 811
     @raise Password_reset_error: an error occured when sending the password reset email
803 812
     @raise Validation_error: one of the arguments is invalid
804 813
     """
805
-    import smtplib
806
-    from email import Message
807
-
808 814
     # check whether there are actually any users with the given email address
809 815
     users = self.__database.select_many( User, User.sql_load_by_email_address( email_address ) )
810 816
 
@@ -1252,6 +1258,9 @@ class Users( object ):
1252 1258
 
1253 1259
     self.__database.commit()
1254 1260
 
1261
+  #PAYPAL_URL = u"https://www.sandbox.paypal.com/cgi-bin/webscr"
1262
+  PAYPAL_URL = u"https://www.paypal.com/cgi-bin/webscr"
1263
+
1255 1264
   @expose( view = Blank_page )
1256 1265
   @end_transaction
1257 1266
   def paypal_notify( self, **params ):
@@ -1262,9 +1271,6 @@ class Users( object ):
1262 1271
     record in the database with their new rate plan. paypal_notify() is
1263 1272
     invoked by PayPal itself.
1264 1273
     """
1265
-    #PAYPAL_URL = u"https://www.sandbox.paypal.com/cgi-bin/webscr"
1266
-    PAYPAL_URL = u"https://www.paypal.com/cgi-bin/webscr"
1267
-
1268 1274
     # check that payment_status is Completed
1269 1275
     payment_status = params.get( u"payment_status" )
1270 1276
     if payment_status == u"Refunded":
@@ -1283,19 +1289,117 @@ class Users( object ):
1283 1289
       raise Payment_error( u"unsupported mc_currency", params )
1284 1290
 
1285 1291
     # verify item_number
1286
-    plan_index = params.get( u"item_number" )
1287
-    if plan_index == None or plan_index == u"":
1292
+    item_number = params.get( u"item_number" )
1293
+    if item_number == None or item_number == u"":
1288 1294
       return dict() # ignore this transaction if there's no item number
1289
-
1290 1295
     try:
1291
-      plan_index = int( plan_index )
1296
+      int( item_number )
1292 1297
     except ValueError:
1293 1298
       raise Payment_error( u"invalid item_number", params )
1294
-    if plan_index == 0 or plan_index >= len( self.__rate_plans ):
1295
-      raise Payment_error( u"invalid item_number", params )
1296 1299
 
1300
+    product = None
1301
+    for potential_product in self.__download_products:
1302
+      if unicode( item_number ) == potential_product.get( u"item_number" ):
1303
+        product = potential_product
1304
+
1305
+    if product:
1306
+      self.__paypal_notify_download( params, product, unicode( item_number ) )
1307
+    else:
1308
+      plan_index = int( item_number )
1309
+      try:
1310
+        rate_plan = self.__rate_plans[ plan_index ]
1311
+      except IndexError:
1312
+        raise Payment_error( u"invalid item_number", params )
1313
+      self.__paypal_notify_subscribe( params, rate_plan, plan_index )
1314
+
1315
+    return dict()
1316
+
1317
+  TRANSACTION_ID_PATTERN = re.compile( u"^[a-zA-Z0-9]+$" )
1318
+
1319
+  def __paypal_notify_download( self, params, product, item_number ):
1320
+    # verify that quantity * the expected fee == mc_gross
1321
+    fee = float( product[ u"fee" ] )
1322
+
1323
+    try:
1324
+      mc_gross = float( params.get( u"mc_gross" ) )
1325
+      if not mc_gross: raise ValueError()
1326
+    except ( TypeError, ValueError ):
1327
+      raise Payment_error( u"invalid mc_gross", params )
1328
+
1329
+    try:
1330
+      quantity = float( params.get( u"quantity" ) )
1331
+      if not quantity: raise ValueError()
1332
+    except ( TypeError, ValueError ):
1333
+      raise Payment_error( u"invalid quantity", params )
1334
+
1335
+    if quantity * fee != mc_gross:
1336
+      raise Payment_error( u"invalid mc_gross", params )
1337
+
1338
+    # verify item_name
1339
+    item_name = params.get( u"item_name" )
1340
+    if item_name and product[ u"name" ].lower() not in item_name.lower():
1341
+      raise Payment_error( u"invalid item_name", params )
1342
+
1343
+    params[ u"cmd" ] = u"_notify-validate"
1344
+    encoded_params = urllib.urlencode( params )
1345
+
1346
+    # verify txn_type
1347
+    txn_type = params.get( u"txn_type" )
1348
+    if txn_type and txn_type != u"web_accept":
1349
+      raise Payment_error( u"invalid txn_type", params )
1350
+    
1351
+    # verify txn_id
1352
+    txn_id = params.get( u"txn_id" )
1353
+    if not self.TRANSACTION_ID_PATTERN.search( txn_id ):
1354
+      raise Payment_error( u"invalid txn_id", params )
1355
+    
1356
+    # ask paypal to verify the request
1357
+    request = urllib2.Request( self.PAYPAL_URL )
1358
+    request.add_header( u"Content-type", u"application/x-www-form-urlencoded" )
1359
+    request_file = urllib2.urlopen( self.PAYPAL_URL, encoded_params )
1360
+    result = request_file.read()
1361
+
1362
+    if result != u"VERIFIED":
1363
+      raise Payment_error( result, params )
1364
+
1365
+    # update the database with a record of the transaction, thereby giving the user access to the
1366
+    # download
1367
+    download_access_id = self.__database.next_id( Download_access, commit = False )
1368
+    download_access = Download_access.create( download_access_id, item_number, txn_id )
1369
+    self.__database.save( download_access, commit = False )
1370
+    self.__database.commit()
1371
+
1372
+    # using the reported payer email, send the user an email with a download link
1373
+    email_address = params.get( u"payer_email" )
1374
+    if not email_address:
1375
+      return
1376
+
1377
+    # create an email message with a unique invitation link
1378
+    message = Message.Message()
1379
+    message[ u"From" ] = u"Luminotes personal wiki <%s>" % self.__support_email
1380
+    message[ u"To" ] = email_address
1381
+    message[ u"Subject" ] = u"Luminotes Desktop download"
1382
+
1383
+    payload = \
1384
+      u"Thank you for purchasing Luminotes Desktop!\n\n"  + \
1385
+      u"To download the installer, please follow this link:\n\n" + \
1386
+      u"%s/d/%s\n\n" % ( self.__https_url or self.__http_url, download_access_id ) + \
1387
+      u"You can use this link anytime to download Luminotes Desktop or upgrade\n" + \
1388
+      u"to new versions as they are released. So you should probably keep the" + \
1389
+      u"link around.\n\n" + \
1390
+      u"If you have any questions, please email support@luminotes.com\n\n" + \
1391
+      u"Enjoy!"
1392
+
1393
+    message.set_payload( payload )
1394
+
1395
+    # send the message out through localhost's smtp server
1396
+    server = smtplib.SMTP()
1397
+    server.connect()
1398
+    server.sendmail( message[ u"From" ], [ email_address ], message.as_string() )
1399
+    server.quit()
1400
+
1401
+  def __paypal_notify_subscribe( self, params, rate_plan, plan_index ):
1297 1402
     # verify mc_gross
1298
-    rate_plan = self.__rate_plans[ plan_index ]
1299 1403
     fee = u"%0.2f" % rate_plan[ u"fee" ]
1300 1404
     yearly_fee = u"%0.2f" % rate_plan[ u"yearly_fee" ]
1301 1405
     mc_gross = params.get( u"mc_gross" )
@@ -1329,9 +1433,9 @@ class Users( object ):
1329 1433
     encoded_params = urllib.urlencode( params )
1330 1434
     
1331 1435
     # ask paypal to verify the request
1332
-    request = urllib2.Request( PAYPAL_URL )
1436
+    request = urllib2.Request( self.PAYPAL_URL )
1333 1437
     request.add_header( u"Content-type", u"application/x-www-form-urlencoded" )
1334
-    request_file = urllib2.urlopen( PAYPAL_URL, encoded_params )
1438
+    request_file = urllib2.urlopen( self.PAYPAL_URL, encoded_params )
1335 1439
     result = request_file.read()
1336 1440
 
1337 1441
     if result != u"VERIFIED":
@@ -1366,8 +1470,6 @@ class Users( object ):
1366 1470
     else:
1367 1471
       raise Payment_error( "unknown txn_type", params )
1368 1472
 
1369
-    return dict()
1370
-
1371 1473
   def update_groups( self, user ):
1372 1474
     """
1373 1475
     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 ):
1468 1570
   def rate_plan( self, plan_index ):
1469 1571
     return self.__rate_plans[ plan_index ]
1470 1572
 
1573
+  @expose( view = Main_page )
1574
+  @end_transaction
1575
+  @grab_user_id
1576
+  def thanks_download( self, **params ):
1577
+    """
1578
+    Provide the information necessary to display the download thanks page, including a product
1579
+    download link. This information can be accessed with an item_number and either a txn_id or a
1580
+    download access_id.
1581
+    """
1582
+    item_number = params.get( u"item_number" )
1583
+    try:
1584
+      item_number = int( item_number )
1585
+    except ( TypeError, ValueError ):
1586
+      raise Payment_error( u"invalid item_number", params )
1587
+
1588
+    # if a valid txn_id is provided, redirect to this page with the corresponding access_id.
1589
+    # that way, if the user bookmarks the page, they'll bookmark it with the access_id rather
1590
+    # than the txn_id
1591
+    txn_id = params.get( u"txn_id" )
1592
+    if txn_id:
1593
+      if not self.TRANSACTION_ID_PATTERN.search( txn_id ):
1594
+        raise Payment_error( u"invalid txn_id", params )
1595
+
1596
+      download_access = self.__database.select_one( Download_access, Download_access.sql_load_by_transaction_id( txn_id ) )
1597
+      if download_access:
1598
+        return dict(
1599
+          redirect = u"/users/thanks_download?access_id=%s&item_number=%s" % ( download_access.object_id, item_number )
1600
+        )
1601
+
1602
+    download_access_id = params.get( u"access_id" )
1603
+    download_url = None
1604
+
1605
+    if download_access_id:
1606
+      try:
1607
+        Valid_id()( download_access_id )
1608
+      except ValueError:
1609
+        raise Payment_error( u"invalid access_id", params )
1610
+
1611
+      download_access = self.__database.load( Download_access, download_access_id )
1612
+      if download_access:
1613
+        if download_access.item_number != unicode( item_number ):
1614
+          raise Payment_error( u"incorrect item_number", params )
1615
+        download_url = u"%s/files/download_product/access_id=%s&item_number=%s" % \
1616
+                       ( self.__https_url or u"", download_access_id, item_number )
1617
+
1618
+    if not txn_id and not download_access_id:
1619
+      raise Payment_error( u"either txn_id or access_id required", params )
1620
+
1621
+    anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ), use_cache = True )
1622
+    if anonymous:
1623
+      main_notebook = self.__database.select_one( Notebook, anonymous.sql_load_notebooks( undeleted_only = True ) )
1624
+    else:
1625
+      main_notebook = None
1626
+
1627
+    result = self.current( params.get( u"user_id" ) )
1628
+
1629
+    retry_count = params.get( u"retry_count", "" )
1630
+    try:
1631
+      retry_count = int( retry_count )
1632
+    except ValueError:
1633
+      retry_count = None
1634
+
1635
+    # if there's no download access or we've retried too many times, give up and display an error
1636
+    RETRY_TIMEOUT = 15
1637
+    if download_url is None and retry_count > RETRY_TIMEOUT:
1638
+      note = Thanks_download_error_note()
1639
+    # if the rate plan of the subscription matches the user's current rate plan, success
1640
+    elif download_url:
1641
+      note = Thanks_download_note( download_url )
1642
+      result[ "conversion" ] = "download_%s" % item_number
1643
+    # otherwise, display an auto-reloading "processing..." page
1644
+    else:
1645
+      note = Processing_download_note( download_access_id, item_number, retry_count )
1646
+
1647
+    result[ "notebook" ] = main_notebook
1648
+    result[ "startup_notes" ] = self.__database.select_many( Note, main_notebook.sql_load_startup_notes() )
1649
+    result[ "total_notes_count" ] = self.__database.select_one( Note, main_notebook.sql_count_notes(), use_cache = True )
1650
+    result[ "note_read_write" ] = False
1651
+    result[ "notes" ] = [ Note.create(
1652
+      object_id = u"thanks",
1653
+      contents = unicode( note ),
1654
+      notebook_id = main_notebook.object_id,
1655
+    ) ]
1656
+    result[ "invites" ] = []
1657
+
1658
+    return result
1659
+
1471 1660
   @expose( view = Json )
1472 1661
   @end_transaction
1473 1662
   @grab_user_id

+ 15
- 0
controller/test/Test_controller.py View File

@@ -96,6 +96,21 @@ class Test_controller( object ):
96 96
             u"yearly_button": u"[yearly or here user %s!] button",
97 97
           },
98 98
         ],
99
+        "luminotes.download_products": [
100
+          {
101
+            "name": "local desktop extravaganza",
102
+            "designed_for": "individuals",
103
+            "storage_quota_bytes": None,
104
+            "included_users": 1,
105
+            "notebook_sharing": False,
106
+            "notebook_collaboration": False,
107
+            "user_admin": False,
108
+            "fee": "30.00",
109
+            "item_number": "5000",
110
+            "filename": "test.exe",
111
+            "button": u"",
112
+          },
113
+        ],
99 114
       },
100 115
       u"/files/download": {
101 116
         u"stream_response": True,

+ 142
- 0
controller/test/Test_files.py View File

@@ -1,5 +1,6 @@
1 1
 # -*- coding: utf8 -*-
2 2
 
3
+import os
3 4
 import time
4 5
 import types
5 6
 import urllib
@@ -14,6 +15,7 @@ from model.Note import Note
14 15
 from model.User import User
15 16
 from model.Invite import Invite
16 17
 from model.File import File
18
+from model.Download_access import Download_access
17 19
 from controller.Notebooks import Access_error
18 20
 from controller.Files import Upload_file, Parse_error
19 21
 
@@ -90,6 +92,11 @@ class Test_files( Test_controller ):
90 92
     Upload_file.exists = exists
91 93
     Upload_file.close = close
92 94
 
95
+    # write a test product file
96
+    test_product_file = file( u"products/test.exe", "wb" )
97
+    test_product_file.write( self.file_data )
98
+    test_product_file.close()
99
+
93 100
     self.make_users()
94 101
     self.make_notebooks()
95 102
     self.database.commit()
@@ -128,6 +135,8 @@ class Test_files( Test_controller ):
128 135
     if self.upload_thread:
129 136
       self.upload_thread.join()
130 137
 
138
+    os.remove( u"products/test.exe" )
139
+
131 140
   def test_download( self, filename = None, quote_filename = None, file_data = None, preview = None ):
132 141
     self.login()
133 142
 
@@ -327,6 +336,139 @@ class Test_files( Test_controller ):
327 336
 
328 337
     assert u"access" in result[ u"body" ][ 0 ]
329 338
 
339
+  def test_download_product( self ):
340
+    access_id = u"wheeaccessid"
341
+    item_number = u"5000"
342
+    transaction_id = u"txn"
343
+
344
+    self.login()
345
+
346
+    download_access = Download_access.create( access_id, item_number, transaction_id )
347
+    self.database.save( download_access )
348
+
349
+    result = self.http_get(
350
+      "/files/download_product?access_id=%s&item_number=%s" % ( access_id, item_number ),
351
+      session_id = self.session_id,
352
+    )
353
+
354
+    headers = result[ u"headers" ]
355
+    assert headers
356
+    assert headers[ u"Content-Type" ] == u"application/octet-stream"
357
+
358
+    filename = u"test.exe".encode( "utf8" )
359
+    assert headers[ u"Content-Disposition" ] == 'attachment; filename="%s"' % filename
360
+
361
+    gen = result[ u"body" ]
362
+    assert isinstance( gen, types.GeneratorType )
363
+    pieces = []
364
+
365
+    try:
366
+      for piece in gen:
367
+        pieces.append( piece )
368
+    except AttributeError, exc:
369
+      if u"session_storage" not in str( exc ):
370
+        raise exc
371
+
372
+    file_data = "".join( pieces )
373
+    assert file_data == self.file_data
374
+
375
+  def test_download_product_without_login( self ):
376
+    access_id = u"wheeaccessid"
377
+    item_number = u"5000"
378
+    transaction_id = u"txn"
379
+
380
+    download_access = Download_access.create( access_id, item_number, transaction_id )
381
+    self.database.save( download_access )
382
+
383
+    result = self.http_get(
384
+      "/files/download_product?access_id=%s&item_number=%s" % ( access_id, item_number ),
385
+    )
386
+
387
+    headers = result[ u"headers" ]
388
+    assert headers
389
+    assert headers[ u"Content-Type" ] == u"application/octet-stream"
390
+
391
+    filename = u"test.exe".encode( "utf8" )
392
+    assert headers[ u"Content-Disposition" ] == 'attachment; filename="%s"' % filename
393
+
394
+    gen = result[ u"body" ]
395
+    assert isinstance( gen, types.GeneratorType )
396
+    pieces = []
397
+
398
+    try:
399
+      for piece in gen:
400
+        pieces.append( piece )
401
+    except AttributeError, exc:
402
+      if u"session_storage" not in str( exc ):
403
+        raise exc
404
+
405
+    file_data = "".join( pieces )
406
+    assert file_data == self.file_data
407
+
408
+  def test_download_product_unknown_access_id( self ):
409
+    access_id = u"wheeaccessid"
410
+    item_number = u"5000"
411
+    transaction_id = u"txn"
412
+
413
+    self.login()
414
+
415
+    download_access = Download_access.create( access_id, item_number, transaction_id )
416
+    self.database.save( download_access )
417
+
418
+    result = self.http_get(
419
+      "/files/download_product?access_id=%s&item_number=%s" % ( u"unknownid", item_number ),
420
+      session_id = self.session_id,
421
+    )
422
+
423
+    assert u"access" in result[ u"body" ][ 0 ]
424
+    headers = result[ u"headers" ]
425
+    assert headers
426
+    assert headers[ u"Content-Type" ] == u"text/html"
427
+    assert not headers.get( u"Content-Disposition" )
428
+
429
+  def test_download_product_unknown_item_number( self ):
430
+    access_id = u"wheeaccessid"
431
+    item_number = u"5000"
432
+    transaction_id = u"txn"
433
+
434
+    self.login()
435
+
436
+    download_access = Download_access.create( access_id, item_number, transaction_id )
437
+    self.database.save( download_access )
438
+
439
+    result = self.http_get(
440
+      "/files/download_product?access_id=%s&item_number=%s" % ( access_id, u"1137" ),
441
+      session_id = self.session_id,
442
+    )
443
+
444
+    assert u"access" in result[ u"body" ][ 0 ]
445
+    headers = result[ u"headers" ]
446
+    assert headers
447
+    assert headers[ u"Content-Type" ] == u"text/html"
448
+    assert not headers.get( u"Content-Disposition" )
449
+
450
+  def test_download_product_missing_file( self ):
451
+    access_id = u"wheeaccessid"
452
+    item_number = u"5000"
453
+    transaction_id = u"txn"
454
+    self.settings[ u"global" ][ u"luminotes.download_products" ][ 0 ][ u"filename" ] = u"notthere.exe"
455
+
456
+    self.login()
457
+
458
+    download_access = Download_access.create( access_id, item_number, transaction_id )
459
+    self.database.save( download_access )
460
+
461
+    result = self.http_get(
462
+      "/files/download_product?access_id=%s&item_number=%s" % ( access_id, item_number ),
463
+      session_id = self.session_id,
464
+    )
465
+
466
+    assert u"access" in result[ u"body" ][ 0 ]
467
+    headers = result[ u"headers" ]
468
+    assert headers
469
+    assert headers[ u"Content-Type" ] == u"text/html"
470
+    assert not headers.get( u"Content-Disposition" )
471
+
330 472
   def test_preview( self ):
331 473
     self.login()
332 474
 

+ 6
- 0
controller/test/Test_root.py View File

@@ -538,3 +538,9 @@ class Test_root( Test_controller ):
538 538
     result = self.http_get( "/i/%s" % invite_id )
539 539
 
540 540
     assert result[ u"redirect" ] == u"/users/redeem_invite/%s" % invite_id
541
+
542
+  def test_download_thanks( self ):
543
+    download_access_id = u"foobarbaz"
544
+    result = self.http_get( "/d/%s" % download_access_id )
545
+
546
+    assert result[ u"redirect" ] == u"/users/download_thanks/access_id=%s" % download_access_id

+ 648
- 0
controller/test/Test_users.py View File

@@ -7,11 +7,13 @@ from nose.tools import raises
7 7
 from datetime import datetime, timedelta
8 8
 from Test_controller import Test_controller
9 9
 import Stub_urllib2
10
+from config.Version import VERSION
10 11
 from model.User import User
11 12
 from model.Group import Group
12 13
 from model.Notebook import Notebook
13 14
 from model.Note import Note
14 15
 from model.Password_reset import Password_reset
16
+from model.Download_access import Download_access
15 17
 from model.Invite import Invite
16 18
 from controller.Users import Invite_error, Payment_error
17 19
 import controller.Users as Users
@@ -3944,6 +3946,150 @@ class Test_users( Test_controller ):
3944 3946
     user = self.database.load( User, self.user.object_id )
3945 3947
     assert user.rate_plan == 1
3946 3948
 
3949
+  DOWNLOAD_PAYMENT_DATA = {
3950
+    u"last_name": u"User",
3951
+    u"txn_id": u"txn",
3952
+    u"receiver_email": u"unittest@luminotes.com",
3953
+    u"payment_status": u"Completed",
3954
+    u"payment_gross": u"30.00",
3955
+    u"residence_country": u"US",
3956
+    u"payer_status": u"verified",
3957
+    u"txn_type": u"web_accept",
3958
+    u"payment_date": u"15:38:18 Jan 10 2008 PST",
3959
+    u"first_name": u"Test",
3960
+    u"item_name": u"local desktop extravaganza",
3961
+    u"charset": u"windows-1252",
3962
+    u"notify_version": u"2.4",
3963
+    u"item_number": u"5000",
3964
+    u"receiver_id": u"rcv",
3965
+    u"business": u"unittest@luminotes.com",
3966
+    u"payer_id": u"pyr",
3967
+    u"verify_sign": u"vfy",
3968
+    u"payment_fee": u"1.19",
3969
+    u"mc_fee": u"1.19",
3970
+    u"mc_currency": u"USD",
3971
+    u"shipping": u"0.00",
3972
+    u"payer_email": u"buyer@luminotes.com",
3973
+    u"payment_type": u"instant",
3974
+    u"mc_gross": u"30.00",
3975
+    u"quantity": u"1",
3976
+  }
3977
+
3978
+  def __assert_download_payment_success( self, result, expect_email = True ):
3979
+    assert len( result ) == 1
3980
+    assert result.get( u"session_id" )
3981
+    assert Stub_urllib2.result == u"VERIFIED"
3982
+    assert Stub_urllib2.headers.get( u"Content-type" ) == u"application/x-www-form-urlencoded"
3983
+    assert Stub_urllib2.url.startswith( "https://" )
3984
+    assert u"paypal.com" in Stub_urllib2.url
3985
+    assert Stub_urllib2.encoded_params
3986
+
3987
+    # verify that the user has been granted download access
3988
+    download_access = self.database.select_one( Download_access, "select * from download_access order by revision desc limit 1;" );
3989
+    assert download_access
3990
+    assert download_access.item_number == u"5000"
3991
+    assert download_access.transaction_id == u"txn"
3992
+
3993
+    if not expect_email:
3994
+      return
3995
+
3996
+    # verify that an email has been sent to the user
3997
+    assert smtplib.SMTP.connected == False
3998
+    assert "<%s>" % self.settings[ u"global" ][ u"luminotes.support_email" ] in smtplib.SMTP.from_address
3999
+    assert smtplib.SMTP.to_addresses == [ u"buyer@luminotes.com" ]
4000
+    assert u"Thank you" in smtplib.SMTP.message
4001
+    assert u"download" in smtplib.SMTP.message
4002
+    assert u"upgrade" in smtplib.SMTP.message
4003
+
4004
+    expected_download_link = u"%s/d/%s" % \
4005
+      ( self.settings[ u"global" ][ u"luminotes.https_url" ], download_access.object_id )
4006
+    assert expected_download_link in smtplib.SMTP.message
4007
+
4008
+  def test_paypal_notify_download_payment( self ):
4009
+    data = dict( self.DOWNLOAD_PAYMENT_DATA )
4010
+    result = self.http_post( "/users/paypal_notify", data );
4011
+    self.__assert_download_payment_success( result )
4012
+
4013
+  def test_paypal_notify_download_payment_multiple_quantity( self ):
4014
+    data = dict( self.DOWNLOAD_PAYMENT_DATA )
4015
+    data[ u"mc_gross" ] = u"90.0"
4016
+    data[ u"quantity" ] = u"3"
4017
+    result = self.http_post( "/users/paypal_notify", data );
4018
+    self.__assert_download_payment_success( result )
4019
+
4020
+  def __assert_download_payment_error( self, result ):
4021
+    assert u"error" in result
4022
+    download_access = self.database.select_one( Download_access, "select * from download_access order by revision desc limit 1;" );
4023
+    assert not download_access
4024
+    assert not smtplib.SMTP.message
4025
+
4026
+  def test_paypal_notify_download_payment_missing_mc_gross( self ):
4027
+    data = dict( self.DOWNLOAD_PAYMENT_DATA )
4028
+    del( data[ u"mc_gross" ] )
4029
+    result = self.http_post( "/users/paypal_notify", data );
4030
+    self.__assert_download_payment_error( result )
4031
+
4032
+  def test_paypal_notify_download_payment_none_mc_gross( self ):
4033
+    data = dict( self.DOWNLOAD_PAYMENT_DATA )
4034
+    data[ u"mc_gross" ] = None
4035
+    result = self.http_post( "/users/paypal_notify", data );
4036
+    self.__assert_download_payment_error( result )
4037
+
4038
+  def test_paypal_notify_download_payment_missing_quantity( self ):
4039
+    data = dict( self.DOWNLOAD_PAYMENT_DATA )
4040
+    del( data[ u"quantity" ] )
4041
+    result = self.http_post( "/users/paypal_notify", data );
4042
+    self.__assert_download_payment_error( result )
4043
+
4044
+  def test_paypal_notify_download_payment_none_quantity( self ):
4045
+    data = dict( self.DOWNLOAD_PAYMENT_DATA )
4046
+    data[ u"quantity" ] = None
4047
+    result = self.http_post( "/users/paypal_notify", data );
4048
+    self.__assert_download_payment_error( result )
4049
+
4050
+  def test_paypal_notify_download_payment_quantity_mc_gross_mismatch( self ):
4051
+    data = dict( self.DOWNLOAD_PAYMENT_DATA )
4052
+    data[ u"quantity" ] = u"2"
4053
+    result = self.http_post( "/users/paypal_notify", data );
4054
+    self.__assert_download_payment_error( result )
4055
+
4056
+  def test_paypal_notify_download_payment_mc_gross_fee_mismatch( self ):
4057
+    data = dict( self.DOWNLOAD_PAYMENT_DATA )
4058
+    data[ u"quantity" ] = u"2"
4059
+    data[ u"mc_gross" ] = u"61.0"
4060
+    result = self.http_post( "/users/paypal_notify", data );
4061
+    self.__assert_download_payment_error( result )
4062
+
4063
+  def test_paypal_notify_download_payment_invalid_item_name( self ):
4064
+    data = dict( self.DOWNLOAD_PAYMENT_DATA )
4065
+    data[ u"item_name" ] = u"something unexpected"
4066
+    result = self.http_post( "/users/paypal_notify", data );
4067
+    self.__assert_download_payment_error( result )
4068
+
4069
+  def test_paypal_notify_download_payment_partial_item_name( self ):
4070
+    data = dict( self.DOWNLOAD_PAYMENT_DATA )
4071
+    data[ u"item_name" ] = u"ultra LOCAL DESKTOP extravaganza digital download!"
4072
+    result = self.http_post( "/users/paypal_notify", data );
4073
+    self.__assert_download_payment_success( result )
4074
+
4075
+  def test_paypal_notify_download_payment_invalid_txn_type( self ):
4076
+    data = dict( self.DOWNLOAD_PAYMENT_DATA )
4077
+    data[ u"txn_type" ] = u"web_wtf"
4078
+    result = self.http_post( "/users/paypal_notify", data );
4079
+    self.__assert_download_payment_error( result )
4080
+
4081
+  def test_paypal_notify_download_payment_invalid_txn_id( self ):
4082
+    data = dict( self.DOWNLOAD_PAYMENT_DATA )
4083
+    data[ u"txn_id" ] = u"not even remotely valid"
4084
+    result = self.http_post( "/users/paypal_notify", data );
4085
+    self.__assert_download_payment_error( result )
4086
+
4087
+  def test_paypal_notify_download_payment_missing_payer_email( self ):
4088
+    data = dict( self.DOWNLOAD_PAYMENT_DATA )
4089
+    data[ u"payer_email" ] = u""
4090
+    result = self.http_post( "/users/paypal_notify", data );
4091
+    self.__assert_download_payment_success( result, expect_email = False )
4092
+
3947 4093
   def test_thanks( self ):
3948 4094
     self.user.rate_plan = 1
3949 4095
     user = self.database.save( self.user )
@@ -4143,6 +4289,508 @@ class Test_users( Test_controller ):
4143 4289
     assert u"Thank you" in result[ u"notes" ][ 0 ].contents
4144 4290
     assert u"confirmation" in result[ u"notes" ][ 0 ].contents
4145 4291
 
4292
+  def test_thanks_download( self ):
4293
+    access_id = u"wheeaccessid"
4294
+    item_number = u"5000"
4295
+    transaction_id = u"txn"
4296
+
4297
+    download_access = Download_access.create( access_id, item_number, transaction_id )
4298
+    self.database.save( download_access )
4299
+
4300
+    self.login()
4301
+
4302
+    result = self.http_post( "/users/thanks_download", dict(
4303
+      access_id = access_id,
4304
+      item_number = item_number,
4305
+    ), session_id = self.session_id )
4306
+
4307
+    assert result[ u"user" ].username == self.user.username
4308
+    assert len( result[ u"notebooks" ] ) == 5
4309
+    notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
4310
+    assert notebook.object_id == self.notebooks[ 0 ].object_id
4311
+    assert notebook.name == self.notebooks[ 0 ].name
4312
+    assert notebook.read_write == True
4313
+    assert notebook.owner == True
4314
+    assert notebook.rank == 0
4315
+
4316
+    assert result[ u"login_url" ] == None
4317
+    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"
4318
+
4319
+    rate_plan = result[ u"rate_plan" ]
4320
+    assert rate_plan
4321
+    assert rate_plan[ u"name" ] == u"super"
4322
+    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
4323
+
4324
+    assert result[ u"conversion" ] == u"download_5000"
4325
+    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
4326
+    assert len( result[ u"startup_notes" ] ) == 1
4327
+    assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
4328
+    assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
4329
+    assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
4330
+    assert result[ u"note_read_write" ] is False
4331
+
4332
+    assert result[ u"notes" ]
4333
+    assert len( result[ u"notes" ] ) == 1
4334
+    assert result[ u"notes" ][ 0 ].title == u"thank you"
4335
+    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
4336
+    assert u"Thank you" in result[ u"notes" ][ 0 ].contents
4337
+    assert u"Luminotes Desktop" in result[ u"notes" ][ 0 ].contents
4338
+    assert u"Download" in result[ u"notes" ][ 0 ].contents
4339
+    assert VERSION in result[ u"notes" ][ 0 ].contents
4340
+
4341
+    expected_download_link = u"%s/files/download_product/access_id=%s&item_number=%s" % \
4342
+      ( self.settings[ u"global" ][ u"luminotes.https_url" ], access_id, item_number )
4343
+    assert expected_download_link in result[ u"notes" ][ 0 ].contents
4344
+
4345
+  def test_thanks_download_without_login( self ):
4346
+    access_id = u"wheeaccessid"
4347
+    item_number = u"5000"
4348
+    transaction_id = u"txn"
4349
+
4350
+    download_access = Download_access.create( access_id, item_number, transaction_id )
4351
+    self.database.save( download_access )
4352
+
4353
+    result = self.http_post( "/users/thanks_download", dict(
4354
+      access_id = access_id,
4355
+      item_number = item_number,
4356
+    ) )
4357
+
4358
+    assert result[ u"user" ].username == self.anonymous.username
4359
+    assert len( result[ u"notebooks" ] ) == 1
4360
+
4361
+    assert result[ u"login_url" ]
4362
+    assert result[ u"logout_url" ]
4363
+
4364
+    rate_plan = result[ u"rate_plan" ]
4365
+    assert rate_plan
4366
+    assert rate_plan[ u"name" ] == u"super"
4367
+    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
4368
+
4369
+    assert result[ u"conversion" ] == u"download_5000"
4370
+    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
4371
+    assert len( result[ u"startup_notes" ] ) == 1
4372
+    assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
4373
+    assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
4374
+    assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
4375
+    assert result[ u"note_read_write" ] is False
4376
+
4377
+    assert result[ u"notes" ]
4378
+    assert len( result[ u"notes" ] ) == 1
4379
+    assert result[ u"notes" ][ 0 ].title == u"thank you"
4380
+    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
4381
+    assert u"Thank you" in result[ u"notes" ][ 0 ].contents
4382
+    assert u"Luminotes Desktop" in result[ u"notes" ][ 0 ].contents
4383
+    assert u"Download" in result[ u"notes" ][ 0 ].contents
4384
+    assert VERSION in result[ u"notes" ][ 0 ].contents
4385
+
4386
+    expected_download_link = u"%s/files/download_product/access_id=%s&item_number=%s" % \
4387
+      ( self.settings[ u"global" ][ u"luminotes.https_url" ], access_id, item_number )
4388
+    assert expected_download_link in result[ u"notes" ][ 0 ].contents
4389
+
4390
+  def test_thanks_download_invalid_item_number( self ):
4391
+    access_id = u"wheeaccessid"
4392
+    item_number = u"5000abc"
4393
+    transaction_id = u"txn"
4394
+
4395
+    download_access = Download_access.create( access_id, item_number, transaction_id )
4396
+    self.database.save( download_access )
4397
+
4398
+    self.login()
4399
+
4400
+    result = self.http_post( "/users/thanks_download", dict(
4401
+      access_id = access_id,
4402
+      item_number = item_number,
4403
+    ), session_id = self.session_id )
4404
+
4405
+    assert u"error" in result
4406
+
4407
+  def test_thanks_download_none_item_number( self ):
4408
+    access_id = u"wheeaccessid"
4409
+    item_number = None
4410
+    transaction_id = u"txn"
4411
+
4412
+    download_access = Download_access.create( access_id, item_number, transaction_id )
4413
+    self.database.save( download_access )
4414
+
4415
+    self.login()
4416
+
4417
+    result = self.http_post( "/users/thanks_download", dict(
4418
+      access_id = access_id,
4419
+      item_number = item_number,
4420
+    ), session_id = self.session_id )
4421
+
4422
+    assert u"error" in result
4423
+
4424
+  def test_thanks_download_missing_item_number( self ):
4425
+    access_id = u"wheeaccessid"
4426
+    transaction_id = u"txn"
4427
+
4428
+    self.login()
4429
+
4430
+    result = self.http_post( "/users/thanks_download", dict(
4431
+      access_id = access_id,
4432
+    ), session_id = self.session_id )
4433
+
4434
+    assert u"error" in result
4435
+
4436
+  def test_thanks_download_incorrect_item_number( self ):
4437
+    access_id = u"wheeaccessid"
4438
+    item_number = u"5000"
4439
+    transaction_id = u"txn"
4440
+
4441
+    self.login()
4442
+
4443
+    download_access = Download_access.create( access_id, item_number, transaction_id )
4444
+    self.database.save( download_access )
4445
+
4446
+    result = self.http_post( "/users/thanks_download", dict(
4447
+      access_id = access_id,
4448
+      item_number = u"1234",
4449
+    ), session_id = self.session_id )
4450
+
4451
+    assert u"error" in result
4452
+
4453
+  def test_thanks_download_txn_id( self ):
4454
+    access_id = u"wheeaccessid"
4455
+    item_number = u"5000"
4456
+    transaction_id = u"txn"
4457
+
4458
+    download_access = Download_access.create( access_id, item_number, transaction_id )
4459
+    self.database.save( download_access )
4460
+
4461
+    self.login()
4462
+
4463
+    result = self.http_post( "/users/thanks_download", dict(
4464
+      txn_id = transaction_id,
4465
+      item_number = item_number,
4466
+    ), session_id = self.session_id )
4467
+
4468
+    redirect = result.get( u"redirect" )
4469
+    expected_redirect = "/users/thanks_download?access_id=%s&item_number=%s" % ( access_id, item_number )
4470
+    assert redirect == expected_redirect
4471
+
4472
+  def test_thanks_download_invalid_txn_id( self ):
4473
+    access_id = u"wheeaccessid"
4474
+    item_number = u"5000"
4475
+    transaction_id = u"invalid txn id"
4476
+
4477
+    download_access = Download_access.create( access_id, item_number, transaction_id )
4478
+    self.database.save( download_access )
4479
+
4480
+    self.login()
4481
+
4482
+    result = self.http_post( "/users/thanks_download", dict(
4483
+      txn_id = transaction_id,
4484
+      item_number = item_number,
4485
+    ), session_id = self.session_id )
4486
+
4487
+    assert u"error" in result
4488
+
4489
+  def test_thanks_download_not_yet_paid( self ):
4490
+    access_id = u"wheeaccessid"
4491
+    item_number = u"5000"
4492
+    transaction_id = u"txn"
4493
+
4494
+    self.login()
4495
+
4496
+    result = self.http_post( "/users/thanks_download", dict(
4497
+      access_id = access_id,
4498
+      item_number = item_number,
4499
+    ), session_id = self.session_id )
4500
+
4501
+    # an unknown transaction id might just mean we're still waiting for the transaction to come in,
4502
+    # so expect a retry
4503
+    assert result[ u"user" ].username == self.user.username
4504
+    assert len( result[ u"notebooks" ] ) == 5
4505
+    notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
4506
+    assert notebook.object_id == self.notebooks[ 0 ].object_id
4507
+    assert notebook.name == self.notebooks[ 0 ].name
4508
+    assert notebook.read_write == True
4509
+    assert notebook.owner == True
4510
+    assert notebook.rank == 0
4511
+
4512
+    assert result[ u"login_url" ] == None
4513
+    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"
4514
+
4515
+    rate_plan = result[ u"rate_plan" ]
4516
+    assert rate_plan
4517
+    assert rate_plan[ u"name" ] == u"super"
4518
+    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
4519
+
4520
+    assert not result.get( u"conversion" )
4521
+    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
4522
+    assert len( result[ u"startup_notes" ] ) == 1
4523
+    assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
4524
+    assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
4525
+    assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
4526
+    assert result[ u"note_read_write" ] is False
4527
+
4528
+    assert result[ u"notes" ]
4529
+    assert len( result[ u"notes" ] ) == 1
4530
+    assert u"processing" in result[ u"notes" ][ 0 ].title
4531
+    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
4532
+    assert u"being processed" in result[ u"notes" ][ 0 ].contents
4533
+    assert u"retry_count=1" in result[ u"notes" ][ 0 ].contents
4534
+
4535
+  def test_thanks_download_not_yet_paid_with_retry( self ):
4536
+    access_id = u"wheeaccessid"
4537
+    item_number = u"5000"
4538
+    transaction_id = u"txn"
4539
+
4540
+    self.login()
4541
+
4542
+    result = self.http_post( "/users/thanks_download", dict(
4543
+      access_id = access_id,
4544
+      item_number = item_number,
4545
+      retry_count = u"3",
4546
+    ), session_id = self.session_id )
4547
+
4548
+    # an unknown transaction id might just mean we're still waiting for the transaction to come in,
4549
+    # so expect a retry
4550
+    assert result[ u"user" ].username == self.user.username
4551
+    assert len( result[ u"notebooks" ] ) == 5
4552
+    notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
4553
+    assert notebook.object_id == self.notebooks[ 0 ].object_id
4554
+    assert notebook.name == self.notebooks[ 0 ].name
4555
+    assert notebook.read_write == True
4556
+    assert notebook.owner == True
4557
+    assert notebook.rank == 0
4558
+
4559
+    assert result[ u"login_url" ] == None
4560
+    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"
4561
+
4562
+    rate_plan = result[ u"rate_plan" ]
4563
+    assert rate_plan
4564
+    assert rate_plan[ u"name" ] == u"super"
4565
+    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
4566
+
4567
+    assert not result.get( u"conversion" )
4568
+    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
4569
+    assert len( result[ u"startup_notes" ] ) == 1
4570
+    assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
4571
+    assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
4572
+    assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
4573
+    assert result[ u"note_read_write" ] is False
4574
+
4575
+    assert result[ u"notes" ]
4576
+    assert len( result[ u"notes" ] ) == 1
4577
+    assert u"processing" in result[ u"notes" ][ 0 ].title
4578
+    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
4579
+    assert u"being processed" in result[ u"notes" ][ 0 ].contents
4580
+    assert u"retry_count=4" in result[ u"notes" ][ 0 ].contents
4581
+
4582
+  def test_thanks_download_not_yet_paid_with_retry_timeout( self ):
4583
+    access_id = u"wheeaccessid"
4584
+    item_number = u"5000"
4585
+    transaction_id = u"txn"
4586
+
4587
+    self.login()
4588
+
4589
+    result = self.http_post( "/users/thanks_download", dict(
4590
+      access_id = access_id,
4591
+      item_number = item_number,
4592
+      retry_count = u"16",
4593
+    ), session_id = self.session_id )
4594
+
4595
+    # an unknown transaction id might just mean we're still waiting for the transaction to come in,
4596
+    # so expect a retry
4597
+    assert result[ u"user" ].username == self.user.username
4598
+    assert len( result[ u"notebooks" ] ) == 5
4599
+    notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
4600
+    assert notebook.object_id == self.notebooks[ 0 ].object_id
4601
+    assert notebook.name == self.notebooks[ 0 ].name
4602
+    assert notebook.read_write == True
4603
+    assert notebook.owner == True
4604
+    assert notebook.rank == 0
4605
+
4606
+    assert result[ u"login_url" ] == None
4607
+    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"
4608
+
4609
+    rate_plan = result[ u"rate_plan" ]
4610
+    assert rate_plan
4611
+    assert rate_plan[ u"name" ] == u"super"
4612
+    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
4613
+
4614
+    assert not result.get( u"conversion" )
4615
+    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
4616
+    assert len( result[ u"startup_notes" ] ) == 1
4617
+    assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
4618
+    assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
4619
+    assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
4620
+    assert result[ u"note_read_write" ] is False
4621
+
4622
+    assert result[ u"notes" ]
4623
+    assert len( result[ u"notes" ] ) == 1
4624
+    assert result[ u"notes" ][ 0 ].title == u"thank you"
4625
+    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
4626
+    assert u"Thank you" in result[ u"notes" ][ 0 ].contents
4627
+    assert u"confirmation" in result[ u"notes" ][ 0 ].contents
4628
+
4629
+  def test_thanks_download_not_yet_paid_txn_id( self ):
4630
+    access_id = u"wheeaccessid"
4631
+    item_number = u"5000"
4632
+    transaction_id = u"txn"
4633
+
4634
+    self.login()
4635
+
4636
+    result = self.http_post( "/users/thanks_download", dict(
4637
+      txn_id = transaction_id,
4638
+      item_number = item_number,
4639
+    ), session_id = self.session_id )
4640
+
4641
+    # an unknown transaction id might just mean we're still waiting for the transaction to come in,
4642
+    # so expect a retry
4643
+    assert result[ u"user" ].username == self.user.username
4644
+    assert len( result[ u"notebooks" ] ) == 5
4645
+    notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
4646
+    assert notebook.object_id == self.notebooks[ 0 ].object_id
4647
+    assert notebook.name == self.notebooks[ 0 ].name
4648
+    assert notebook.read_write == True
4649
+    assert notebook.owner == True
4650
+    assert notebook.rank == 0
4651
+
4652
+    assert result[ u"login_url" ] == None
4653
+    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"
4654
+
4655
+    rate_plan = result[ u"rate_plan" ]
4656
+    assert rate_plan
4657
+    assert rate_plan[ u"name" ] == u"super"
4658
+    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
4659
+
4660
+    assert not result.get( u"conversion" )
4661
+    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
4662
+    assert len( result[ u"startup_notes" ] ) == 1
4663
+    assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
4664
+    assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
4665
+    assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
4666
+    assert result[ u"note_read_write" ] is False
4667
+
4668
+    assert result[ u"notes" ]
4669
+    assert len( result[ u"notes" ] ) == 1
4670
+    assert u"processing" in result[ u"notes" ][ 0 ].title
4671
+    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
4672
+    assert u"being processed" in result[ u"notes" ][ 0 ].contents
4673
+    assert u"retry_count=1" in result[ u"notes" ][ 0 ].contents
4674
+
4675
+  def test_thanks_download_not_yet_paid_txn_id_with_retry( self ):
4676
+    access_id = u"wheeaccessid"
4677
+    item_number = u"5000"
4678
+    transaction_id = u"txn"
4679
+
4680
+    self.login()
4681
+
4682
+    result = self.http_post( "/users/thanks_download", dict(
4683
+      txn_id = transaction_id,
4684
+      item_number = item_number,
4685
+      retry_count = u"3",
4686
+    ), session_id = self.session_id )
4687
+
4688
+    # an unknown transaction id might just mean we're still waiting for the transaction to come in,
4689
+    # so expect a retry
4690
+    assert result[ u"user" ].username == self.user.username
4691
+    assert len( result[ u"notebooks" ] ) == 5
4692
+    notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
4693
+    assert notebook.object_id == self.notebooks[ 0 ].object_id
4694
+    assert notebook.name == self.notebooks[ 0 ].name
4695
+    assert notebook.read_write == True
4696
+    assert notebook.owner == True
4697
+    assert notebook.rank == 0
4698
+
4699
+    assert result[ u"login_url" ] == None
4700
+    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"
4701
+
4702
+    rate_plan = result[ u"rate_plan" ]
4703
+    assert rate_plan
4704
+    assert rate_plan[ u"name" ] == u"super"
4705
+    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
4706
+
4707
+    assert not result.get( u"conversion" )
4708
+    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
4709
+    assert len( result[ u"startup_notes" ] ) == 1
4710
+    assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
4711
+    assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
4712
+    assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
4713
+    assert result[ u"note_read_write" ] is False
4714
+
4715
+    assert result[ u"notes" ]
4716
+    assert len( result[ u"notes" ] ) == 1
4717
+    assert u"processing" in result[ u"notes" ][ 0 ].title
4718
+    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
4719
+    assert u"being processed" in result[ u"notes" ][ 0 ].contents
4720
+    assert u"retry_count=4" in result[ u"notes" ][ 0 ].contents
4721
+
4722
+  def test_thanks_download_not_yet_paid_txn_id_with_retry_timeout( self ):
4723
+    access_id = u"wheeaccessid"
4724
+    item_number = u"5000"
4725
+    transaction_id = u"txn"
4726
+
4727
+    self.login()
4728
+
4729
+    result = self.http_post( "/users/thanks_download", dict(
4730
+      txn_id = transaction_id,
4731
+      item_number = item_number,
4732
+      retry_count = u"16",
4733
+    ), session_id = self.session_id )
4734
+
4735
+    # an unknown transaction id might just mean we're still waiting for the transaction to come in,
4736
+    # so expect a retry
4737
+    assert result[ u"user" ].username == self.user.username
4738
+    assert len( result[ u"notebooks" ] ) == 5
4739
+    notebook = [ notebook for notebook in result[ u"notebooks" ] if notebook.object_id == self.notebooks[ 0 ].object_id ][ 0 ]
4740
+    assert notebook.object_id == self.notebooks[ 0 ].object_id
4741
+    assert notebook.name == self.notebooks[ 0 ].name
4742
+    assert notebook.read_write == True
4743
+    assert notebook.owner == True
4744
+    assert notebook.rank == 0
4745
+
4746
+    assert result[ u"login_url" ] == None
4747
+    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"
4748
+
4749
+    rate_plan = result[ u"rate_plan" ]
4750
+    assert rate_plan
4751
+    assert rate_plan[ u"name" ] == u"super"
4752
+    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
4753
+
4754
+    assert not result.get( u"conversion" )
4755
+    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
4756
+    assert len( result[ u"startup_notes" ] ) == 1
4757
+    assert result[ u"startup_notes" ][ 0 ].object_id == self.startup_note.object_id
4758
+    assert result[ u"startup_notes" ][ 0 ].title == self.startup_note.title
4759
+    assert result[ u"startup_notes" ][ 0 ].contents == self.startup_note.contents
4760
+    assert result[ u"note_read_write" ] is False
4761
+
4762
+    assert result[ u"notes" ]
4763
+    assert len( result[ u"notes" ] ) == 1
4764
+    assert result[ u"notes" ][ 0 ].title == u"thank you"
4765
+    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
4766
+    assert u"Thank you" in result[ u"notes" ][ 0 ].contents
4767
+    assert u"confirmation" in result[ u"notes" ][ 0 ].contents
4768
+
4769
+  def test_thanks_download_missing_txn_id_missing_access_id( self ):
4770
+    item_number = u"5000"
4771
+
4772
+    self.login()
4773
+
4774
+    result = self.http_post( "/users/thanks_download", dict(
4775
+      item_number = item_number,
4776
+    ), session_id = self.session_id )
4777
+
4778
+    assert u"error" in result
4779
+
4780
+  def test_thanks_download_invalid_access_id( self ):
4781
+    access_id = u"invalid access id"
4782
+    item_number = u"5000"
4783
+    transaction_id = u"txn"
4784
+
4785
+    self.login()
4786
+
4787
+    result = self.http_post( "/users/thanks_download", dict(
4788
+      access_id = access_id,
4789
+      item_number = item_number,
4790
+    ), session_id = self.session_id )
4791
+
4792
+    assert u"error" in result
4793
+
4146 4794
   def test_rate_plan( self ):
4147 4795
     plan_index = 1
4148 4796
     rate_plan = cherrypy.root.users.rate_plan( plan_index )

+ 75
- 0
model/Download_access.py View File

@@ -0,0 +1,75 @@
1
+from Persistent import Persistent, quote
2
+
3
+
4
+class Download_access( Persistent ):
5
+  """
6
+  Access for a particular user to a downloadable product. This object is used to create unique
7
+  per-customer product download links without requiring the user to have a Luminotes account.
8
+  """
9
+  def __init__( self, object_id, revision = None, item_number = None, transaction_id = None ):
10
+    """
11
+    Create a download access record with the given id.
12
+
13
+    @type object_id: unicode
14
+    @param object_id: id of the download access
15
+    @type revision: datetime or NoneType
16
+    @param revision: revision timestamp of the object (optional, defaults to now)
17
+    @type item_number: unicode or NoneType
18
+    @param item_number: number of the item to which download access is granted (optional)
19
+    @type transaction_id: unicode or NoneType
20
+    @param transaction_id: payment processor id for the transaction used to pay for this download
21
+                           (optional)
22
+    @rtype: Download_access
23
+    @return: newly constructed download access object
24
+    """
25
+    Persistent.__init__( self, object_id, revision )
26
+    self.__item_number = item_number
27
+    self.__transaction_id = transaction_id
28
+
29
+  @staticmethod
30
+  def create( object_id, item_number = None, transaction_id = None ):
31
+    """
32
+    Convenience constructor for creating a new download access object.
33
+
34
+    @type item_number: unicode or NoneType
35
+    @param item_number: number of the item to which download access is granted (optional)
36
+    @type transaction_id: unicode or NoneType
37
+    @param transaction_id: payment processor id for the transaction used to pay for this download
38
+                           (optional)
39
+    @rtype: Download_access
40
+    @return: newly constructed download access object
41
+    """
42
+    return Download_access( object_id, item_number = item_number, transaction_id = transaction_id )
43
+
44
+  @staticmethod
45
+  def sql_load( object_id, revision = None ):
46
+    # download access objects don't store old revisions
47
+    if revision:
48
+      raise NotImplementedError()
49
+
50
+    return "select id, revision, item_number, transaction_id from download_access where id = %s;" % quote( object_id )
51
+
52
+  @staticmethod
53
+  def sql_load_by_transaction_id( transaction_id ):
54
+    return "select id, revision, item_number, transaction_id from download_access where transaction_id = %s;" % quote( transaction_id )
55
+
56
+  @staticmethod
57
+  def sql_id_exists( object_id, revision = None ):
58
+    if revision:
59
+      raise NotImplementedError()
60
+
61
+    return "select id from download_access where id = %s;" % quote( object_id )
62
+
63
+  def sql_exists( self ):
64
+    return Download_access.sql_id_exists( self.object_id )
65
+
66
+  def sql_create( self ):
67
+    return "insert into download_access ( id, revision, item_number, transaction_id ) values ( %s, %s, %s, %s );" % \
68
+    ( quote( self.object_id ), quote( self.revision ), quote( self.__item_number ), quote( self.__transaction_id ) )
69
+
70
+  def sql_update( self ):
71
+    return "update download_access set revision = %s, item_number = %s, transaction_id = %s where id = %s;" % \
72
+    ( quote( self.revision ), quote( self.__item_number ), quote( self.__transaction_id ), quote( self.object_id ) )
73
+
74
+  item_number = property( lambda self: self.__item_number )
75
+  transaction_id = property( lambda self: self.__transaction_id )

+ 8
- 0
model/delta/1.5.0.sql View File

@@ -0,0 +1,8 @@
1
+CREATE TABLE download_access (
2
+    id text NOT NULL,
3
+    revision timestamp with time zone NOT NULL,
4
+    item_number text,
5
+    transaction_id text
6
+);
7
+ALTER TABLE ONLY download_access ADD CONSTRAINT download_access_pkey PRIMARY KEY (id);
8
+CREATE INDEX download_access_transaction_id_index ON download_access USING btree (transaction_id);

+ 1
- 0
model/drop.sql View File

@@ -7,6 +7,7 @@ DROP TABLE note;
7 7
 DROP VIEW notebook_current;
8 8
 DROP TABLE notebook;
9 9
 DROP TABLE password_reset;
10
+DROP TABLE download_access;
10 11
 DROP TABLE user_notebook;
11 12
 DROP TABLE user_group;
12 13
 DROP TABLE invite;

+ 30
- 0
model/schema.sql View File

@@ -180,6 +180,21 @@ CREATE TABLE password_reset (
180 180
 
181 181
 ALTER TABLE public.password_reset OWNER TO luminotes;
182 182
 
183
+
184
+-- Name: download_access; Type: TABLE; Schema: public; Owner: luminotes; Tablespace: 
185
+--
186
+
187
+CREATE TABLE download_access (
188
+    id text NOT NULL,
189
+    revision timestamp with time zone NOT NULL,
190
+    item_number text,
191
+    transaction_id text
192
+);
193
+
194
+
195
+ALTER TABLE public.download_access OWNER TO luminotes;
196
+
197
+--
183 198
 --
184 199
 -- Name: user_group; Type: TABLE; Schema: public; Owner: luminotes; Tablespace: 
185 200
 --
@@ -256,6 +271,14 @@ ALTER TABLE ONLY password_reset
256 271
     ADD CONSTRAINT password_reset_pkey PRIMARY KEY (id);
257 272
 
258 273
 
274
+--
275
+-- Name: download_access_pkey; Type: CONSTRAINT; Schema: public; Owner: luminotes; Tablespace: 
276
+--
277
+
278
+ALTER TABLE ONLY download_access
279
+    ADD CONSTRAINT download_access_pkey PRIMARY KEY (id);
280
+
281
+
259 282
 --
260 283
 -- Name: user_notebook_pkey; Type: CONSTRAINT; Schema: public; Owner: luminotes; Tablespace: 
261 284
 --
@@ -327,6 +350,13 @@ CREATE INDEX note_notebook_id_title_index ON note USING btree (notebook_id, md5(
327 350
 CREATE INDEX password_reset_email_address_index ON password_reset USING btree (email_address);
328 351
 
329 352
 
353
+--
354
+-- Name: download_access_transaction_id_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace: 
355
+--
356
+
357
+CREATE INDEX download_access_transaction_id_index ON download_access USING btree (transaction_id);
358
+
359
+
330 360
 --
331 361
 -- Name: search_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace: 
332 362
 --

+ 30
- 0
model/schema.sqlite View File

@@ -132,6 +132,17 @@ CREATE TABLE password_reset (
132 132
 );
133 133
 
134 134
 
135
+-- Name: download_access; Type: TABLE; Schema: public; Owner: luminotes; Tablespace: 
136
+--
137
+
138
+CREATE TABLE download_access (
139
+    id text NOT NULL,
140
+    revision timestamp with time zone NOT NULL,
141
+    item_number text,
142
+    transaction_id text
143
+);
144
+
145
+
135 146
 --
136 147
 -- Name: user_group; Type: TABLE; Schema: public; Owner: luminotes; Tablespace: 
137 148
 --
@@ -213,6 +224,13 @@ CREATE INDEX note_notebook_id_startup_index ON note (notebook_id, startup);
213 224
 CREATE INDEX note_notebook_id_title_index ON note (notebook_id, title);
214 225
 
215 226
 
227
+--
228
+-- Name: password_reset_id_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace: 
229
+--
230
+
231
+CREATE INDEX password_reset_id_index ON password_reset (id);
232
+
233
+
216 234
 --
217 235
 -- Name: password_reset_email_address_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace: 
218 236
 --
@@ -220,6 +238,18 @@ CREATE INDEX note_notebook_id_title_index ON note (notebook_id, title);
220 238
 CREATE INDEX password_reset_email_address_index ON password_reset (email_address);
221 239
 
222 240
 
241
+-- Name: download_access_id_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace: 
242
+--
243
+
244
+CREATE INDEX download_access_id_index ON password_reset (id);
245
+
246
+
247
+-- Name: download_access_transaction_id_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace: 
248
+--
249
+
250
+CREATE INDEX download_access_transaction_id_index ON download_access (transaction_id);
251
+
252
+
223 253
 --
224 254
 -- Name: search_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace: 
225 255
 --

+ 15
- 0
model/test/Test_download_access.py View File

@@ -0,0 +1,15 @@
1
+from model.Download_access import Download_access
2
+
3
+
4
+class Test_download_access( object ):
5
+  def setUp( self ):
6
+    self.object_id = u"17"
7
+    self.item_number = u"999"
8
+    self.transaction_id = u"foooooooo234"
9
+
10
+    self.download_access = Download_access.create( self.object_id, self.item_number, self.transaction_id )
11
+
12
+  def test_create( self ):
13
+    assert self.download_access.object_id == self.object_id
14
+    assert self.download_access.item_number == self.item_number
15
+    assert self.download_access.transaction_id == self.transaction_id

+ 0
- 0
products/.empty View File


+ 15
- 1
view/Download_page.py View File

@@ -4,9 +4,12 @@ from config.Version import VERSION
4 4
 
5 5
 
6 6
 class Download_page( Product_page ):
7
-  def __init__( self, user, notebooks, first_notebook, login_url, logout_url, rate_plan, groups, download_button ):
7
+  def __init__( self, user, notebooks, first_notebook, login_url, logout_url, rate_plan, groups, download_products, upgrade = False ):
8 8
     MEGABYTE = 1024 * 1024
9 9
 
10
+    # for now, just assume there's a single download package
11
+    download_button = download_products[ 0 ].get( "button" )
12
+
10 13
     Product_page.__init__(
11 14
       self,
12 15
       user,
@@ -33,6 +36,17 @@ class Download_page( Product_page ):
33 36
             class_ = u"upgrade_subtitle",
34 37
           ),
35 38
           Div(
39
+            upgrade and P(
40
+              B( "Upgrading:" ),
41
+              u"""
42
+              If you have already purchased Luminotes Desktop and would like to download a newer
43
+              version, simply follow the link you received after your purchase. Can't find
44
+              the link or need help? Please
45
+              """,
46
+              A( u"contact support", href = u"/contact_info" ),
47
+              u"for assistance.",
48
+              class_ = u"upgrade_text",
49
+            ) or None,
36 50
             Div(
37 51
               Img( src = u"/static/images/installer_screenshot.png", width = u"350", height = u"273" ),
38 52
               class_ = u"desktop_screenshot",

+ 1
- 1
view/Header.py View File

@@ -17,7 +17,7 @@ class Header( Div ):
17 17
             A( title_image, href = u"http://luminotes.com/", target = "_new" ),
18 18
           Div(
19 19
             u"version", VERSION, u" | ",
20
-            A( u"upgrade", href = u"http://luminotes.com/pricing", target = "_new" ), u" | ",
20
+            A( u"upgrade", href = u"http://luminotes.com/download?upgrade=True", target = "_new" ), u" | ",
21 21
             A( u"support", href = u"http://luminotes.com/support", target = "_new" ), u" | ",
22 22
             A( u"blog", href = u"http://luminotes.com/blog", target = "_new" ),
23 23
             class_ = u"header_links",

+ 26
- 0
view/Processing_download_note.py View File

@@ -0,0 +1,26 @@
1
+from Tags import Html, Head, Meta, H3, P
2
+
3
+
4
+class Processing_download_note( Html ):
5
+  def __init__( self, download_access_id, item_number, retry_count ):
6
+    if not retry_count:
7
+      retry_count = 0
8
+
9
+    retry_count += 1
10
+
11
+    Html.__init__(
12
+      self,
13
+      Head(
14
+        Meta(
15
+          http_equiv = u"Refresh",
16
+          content = u"2; URL=/users/thanks_download?access_id=%s&item_number=%s&retry_count=%s" %
17
+                    ( download_access_id, item_number, retry_count ),
18
+        ),
19
+      ),
20
+      H3( u"processing..." ),
21
+      P(
22
+        """
23
+        Your payment is being processed. This shouldn't take more than a minute. Please wait...
24
+        """,
25
+      ),
26
+    )

+ 30
- 0
view/Thanks_download_error_note.py View File

@@ -0,0 +1,30 @@
1
+from Tags import Span, H3, P, A
2
+
3
+
4
+class Thanks_download_error_note( Span ):
5
+  def __init__( self ):
6
+    Span.__init__(
7
+      self,
8
+      H3( u"thank you" ),
9
+      P(
10
+        u"""
11
+        Thank you for purchasing Luminotes Desktop!
12
+        """,
13
+      ),
14
+      P(
15
+        u"""
16
+        Luminotes has not yet received confirmation of your payment. Please
17
+        check back in a few minutes by refreshing this page, or check your
18
+        email for a Luminotes Desktop download message.
19
+        """
20
+      ),
21
+      P(
22
+        """
23
+        If your payment is not received within the next few minutes, please
24
+        """,
25
+        A( u"contact support", href = u"/contact_info", target = "_top" ),
26
+        u"""
27
+        for assistance.
28
+        """,
29
+      ),
30
+    )

+ 36
- 0
view/Thanks_download_note.py View File

@@ -0,0 +1,36 @@
1
+from Tags import Span, H3, P, A
2
+from config.Version import VERSION
3
+
4
+
5
+class Thanks_download_note( Span ):
6
+  def __init__( self, download_url ):
7
+    Span.__init__(
8
+      self,
9
+      H3( u"thank you" ),
10
+      P(
11
+        u"""
12
+        Thank you for purchasing Luminotes Desktop! 
13
+        """,
14
+      ),
15
+      P(
16
+        A( u"Download Luminotes Desktop version %s" % VERSION, href = download_url ),
17
+        """
18
+        and get started taking notes with your own personal wiki.
19
+        """,
20
+      ),
21
+      P(
22
+        u"""
23
+        It's a good idea to bookmark this page so that you can download
24
+        Luminotes Desktop or upgrade to new versions as they are released.
25
+        """,
26
+      ),
27
+      P(
28
+        u"""
29
+        If you have any questions about Luminotes Desktop or your purchase, please
30
+        """,
31
+        A( u"contact support", href = u"/contact_info", target = "_top" ),
32
+        u"""
33
+        for assistance.
34
+        """,
35
+      ),
36
+    )