Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

initial commit

  • Loading branch information...
commit 1d8ef43b3e8eadb700a06d95dce04468135f738b 0 parents
Peter Georgeson authored
39 README.md
@@ -0,0 +1,39 @@
+Great Motivator
+===============
+This fun application is designed to motivate an author by applying a real financial penalty if a target word count isn't reached.
+It's been developed to demonstrate the PayPal preapproval API and was developed in conjunction with the x.com tutorial series.
+It's just the demonstration of an idea and not production ready.
+
+See [http://www.supernifty.com.au/blog/](http://www.supernifty.com.au/blog/) for further details.
+
+The following technologies were used to build this sample online market:
+
+* PayPal Adaptive Payments
+* Google App Engine
+* Python
+
+License
+=======
+
+Copyright 2012 Supernifty. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are
+permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice, this list of
+ conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list
+ of conditions and the following disclaimer in the documentation and/or other materials
+ provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY SUPERNIFTY ''AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SUPERNIFTY OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
11 app/app.yaml
@@ -0,0 +1,11 @@
+application: motivator
+version: 1
+runtime: python
+api_version: 1
+
+handlers:
+- url: /static
+ static_dir: static
+
+- url: /.*
+ script: main.py
187 app/main.py
@@ -0,0 +1,187 @@
+import cgi
+import datetime
+import decimal
+import logging
+import os
+import random
+import re
+import simplejson
+
+from google.appengine.api import channel
+from google.appengine.api import users
+from google.appengine.ext import db
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp import template
+from google.appengine.ext.webapp.util import login_required
+from google.appengine.ext.webapp.util import run_wsgi_app
+
+import model
+import paypal
+import settings
+import util
+
+class Home(webapp.RequestHandler):
+ def get(self):
+ '''initialize the main auction page'''
+ # ensure logged in
+ user = users.get_current_user()
+ if not user:
+ self.redirect(users.create_login_url(self.request.uri))
+ return
+
+ data = {
+ 'user': user,
+ 'profile': model.Profile.find(user),
+ }
+ path = os.path.join(os.path.dirname(__file__), 'templates/main.htm')
+ self.response.out.write(template.render(path, data))
+
+ def post(self):
+ user = users.get_current_user()
+ if not user:
+ self.redirect(users.create_login_url(self.request.uri))
+
+ profile = model.Profile.find( user )
+ # count words
+ words = self.request.get( "words" )
+ count = len( re.split( '\s+', words ) )
+
+ # add to words
+ if profile.words == None:
+ profile.words = ''
+ profile.words += "<hr/>" + words
+ if profile.current_count == None:
+ profile.current_count = 0
+ profile.current_count += count
+ profile.save()
+
+ path = os.path.join(os.path.dirname(__file__), 'templates/main.htm')
+ self.response.out.write(template.render(path, { 'message': '%i words were added to your masterpiece' % count, 'user': user, 'profile': profile } ))
+
+class Goal(webapp.RequestHandler):
+ def get(self):
+ user = users.get_current_user()
+ if not user:
+ self.redirect(users.create_login_url(self.request.uri))
+ profile = model.Profile.find( user )
+ data = { 'user': user, 'profile': profile }
+ path = os.path.join(os.path.dirname(__file__), 'templates/goal.htm')
+ self.response.out.write(template.render(path, data))
+
+ def post(self):
+ '''start preapproval'''
+ user = users.get_current_user()
+ if not user:
+ self.redirect(users.create_login_url(self.request.uri))
+
+ profile = model.Profile.find( user )
+
+ # update goal details
+ profile.goal_name = self.request.get( "name" )
+ profile.goal_count = int(self.request.get( "count" ))
+ ( m, d, y ) = [ int( x ) for x in self.request.get( "date" ).split( '/' ) ]
+ profile.goal_date = datetime.datetime( year=y, month=m, day=d )
+ profile.save()
+
+ amount = float(self.request.get( "amount" ))
+ item = model.Preapproval( user=user, status="NEW", secret=util.random_alnum(16), amount=int(amount*100) )
+ item.put()
+ # get key
+ preapproval = paypal.Preapproval(
+ amount=amount,
+ return_url="%s/success/%s/%s/" % ( self.request.uri, item.key(), item.secret ),
+ cancel_url=self.request.uri,
+ remote_address=self.request.remote_addr )
+
+ item.debug_request = preapproval.raw_request
+ item.debug_response = preapproval.raw_response
+ item.put()
+
+ if preapproval.status() == 'Success':
+ item.status = 'CREATED'
+ item.preapproval_key = preapproval.key()
+ item.put()
+ self.redirect( preapproval.next_url() ) # go to paypal
+ else:
+ item.status = 'ERROR'
+ item.status_detail = 'Preapproval status was "%s"' % preapproval.status()
+ item.put()
+
+ path = os.path.join(os.path.dirname(__file__), 'templates/main.htm')
+ self.response.out.write(template.render(path, { 'message': 'An error occurred connecting to PayPal', 'user': user, 'profile': profile } ))
+
+class Success (webapp.RequestHandler):
+ def get(self, key, secret):
+ logging.info( "returned from paypal" )
+
+ item = model.Preapproval.get( key )
+
+ # validation
+ if item == None: # no key
+ self.error(404)
+ return
+
+ if item.status != 'CREATED':
+ item.status_detail = 'Unexpected status %s' % item.status
+ item.status = 'ERROR'
+ item.put()
+ self.error(501)
+ return
+
+ if item.secret != secret:
+ item.status_detail = 'Incorrect secret %s' % secret
+ item.status = 'ERROR'
+ item.put()
+ self.error(501)
+ return
+
+ # looks ok
+ profile = model.Profile.find( item.user )
+ profile.preapproval_amount = item.amount
+ profile.preapproval_expiry = datetime.datetime.utcnow() + datetime.timedelta( days=settings.PREAPPROVAL_PERIOD )
+ profile.preapproval_key = item.preapproval_key
+ profile.goal_active = True
+ profile.put()
+ item.status = 'COMPLETED'
+ item.put()
+
+ path = os.path.join(os.path.dirname(__file__), 'templates/main.htm')
+ self.response.out.write(template.render(path, { 'message': 'Your preapproved limit was updated.', 'user': item.user, 'profile': profile } ))
+
+class Words(webapp.RequestHandler):
+ def get(self):
+ user = users.get_current_user()
+ if not user:
+ self.redirect(users.create_login_url(self.request.uri))
+ profile = model.Profile.find( user )
+ self.response.out.write( "<html><body>%s</body></html>" % profile.words )
+
+class Check(webapp.RequestHandler):
+ def get(self):
+ # find active users which have expired
+ ( failed, total ) = model.Profile.check_expired()
+
+ path = os.path.join(os.path.dirname(__file__), 'templates/main.htm')
+ self.response.out.write(template.render(path, { 'message': 'Payment was taken from %i users, from %i who finished.' % ( failed, total ) } ))
+
+class NotFound (webapp.RequestHandler):
+ def get(self):
+ self.error(404)
+
+application = webapp.WSGIApplication( [
+ ('/', Home),
+ ('/goal', Goal),
+ ('/check', Check),
+ ('/words', Words),
+ ('/goal/success/([^/]*)/([^/]*)/.*', Success),
+ ('/.*', NotFound),
+ ],
+ debug=True)
+
+def main():
+ logging.getLogger().setLevel(logging.DEBUG)
+ run_wsgi_app(application)
+
+if __name__ == "__main__":
+ main()
+
70 app/model.py
@@ -0,0 +1,70 @@
+import datetime
+import logging
+
+from google.appengine.ext import db
+
+import paypal
+import settings
+
+class Profile( db.Model ):
+ owner = db.UserProperty()
+
+ preapproval_amount = db.IntegerProperty() # cents
+ preapproval_expiry = db.DateTimeProperty()
+ preapproval_key = db.StringProperty()
+
+ goal_active = db.BooleanProperty()
+ goal_name = db.StringProperty()
+ goal_count = db.IntegerProperty()
+ goal_date = db.DateTimeProperty()
+
+ current_count = db.IntegerProperty()
+
+ words = db.TextProperty()
+
+ def amount_dollars( self ):
+ return self.preapproval_amount / 100
+
+ @staticmethod
+ def find( user ):
+ profile = Profile.all().filter( 'owner =', user ).get()
+ if profile == None:
+ profile = Profile( owner=user, preapproval_amount=0, current_count=0, goal_active=False, words="" )
+ profile.save()
+ return profile
+
+ @staticmethod
+ def check_expired():
+ candidates = Profile.all().filter( 'goal_active =', True ).filter( 'goal_date <', datetime.datetime.now() )
+ total = 0
+ failed = 0
+ for candidate in candidates:
+ if candidate.current_count < candidate.goal_count: # didn't make it
+ failed += 1
+ # take preapproval amount
+ logging.info( "settling transaction..." )
+ pay = paypal.PayWithPreapproval( amount=candidate.amount_dollars(), preapproval_key=candidate.preapproval_key )
+ if pay.status() == 'COMPLETED':
+ logging.info( "settling transaction: done" )
+ else:
+ logging.info( "settling transaction: failed" )
+
+ candidate.goal_active = False
+ candidate.save()
+ total += 1
+
+ return ( failed, total )
+
+
+class Preapproval( db.Model ):
+ '''track interaction with paypal'''
+ user = db.UserProperty()
+ created = db.DateTimeProperty(auto_now_add=True)
+ status = db.StringProperty( choices=( 'NEW', 'CREATED', 'ERROR', 'CANCELLED', 'COMPLETED' ) )
+ status_detail = db.StringProperty()
+ secret = db.StringProperty() # to verify return_url
+ debug_request = db.TextProperty()
+ debug_response = db.TextProperty()
+ preapproval_key = db.StringProperty()
+ amount = db.IntegerProperty() # cents
+
237 app/paypal.py
@@ -0,0 +1,237 @@
+import datetime
+import decimal
+import logging
+import urllib
+import urllib2
+
+from google.appengine.api import urlfetch
+urlfetch.set_default_fetch_deadline(60)
+
+# hack to enable urllib to work with Python
+import os
+os.environ['foo_proxy'] = 'bar'
+
+from django.utils import simplejson as json
+
+import settings
+
+class Pay( object ):
+ def __init__( self, amount, return_url, cancel_url, remote_address, secondary_receiver=None, ipn_url=None, shipping=False ):
+ headers = {
+ 'X-PAYPAL-SECURITY-USERID': settings.PAYPAL_USERID,
+ 'X-PAYPAL-SECURITY-PASSWORD': settings.PAYPAL_PASSWORD,
+ 'X-PAYPAL-SECURITY-SIGNATURE': settings.PAYPAL_SIGNATURE,
+ 'X-PAYPAL-REQUEST-DATA-FORMAT': 'JSON',
+ 'X-PAYPAL-RESPONSE-DATA-FORMAT': 'JSON',
+ 'X-PAYPAL-APPLICATION-ID': settings.PAYPAL_APPLICATION_ID,
+ 'X-PAYPAL-DEVICE-IPADDRESS': remote_address,
+ }
+
+ data = {
+ 'currencyCode': 'USD',
+ 'returnUrl': return_url,
+ 'cancelUrl': cancel_url,
+ 'requestEnvelope': { 'errorLanguage': 'en_US' },
+ }
+
+ if shipping:
+ data['actionType'] = 'CREATE'
+ else:
+ data['actionType'] = 'PAY'
+
+ if secondary_receiver == None: # simple payment
+ data['receiverList'] = { 'receiver': [ { 'email': settings.PAYPAL_EMAIL, 'amount': '%f' % amount } ] }
+ else: # chained
+ commission = amount * settings.PAYPAL_COMMISSION
+ data['receiverList'] = { 'receiver': [
+ { 'email': settings.PAYPAL_EMAIL, 'amount': '%0.2f' % amount, 'primary': 'true' },
+ { 'email': secondary_receiver, 'amount': '%0.2f' % ( amount - commission ), 'primary': 'false' },
+ ]
+ }
+
+ if ipn_url != None:
+ data['ipnNotificationUrl'] = ipn_url
+
+ self.raw_request = json.dumps(data)
+ #request = urllib2.Request( "%s%s" % ( settings.PAYPAL_ENDPOINT, "Pay" ), data=self.raw_request, headers=headers )
+ #self.raw_response = urllib2.urlopen( request ).read()
+ self.raw_response = url_request( "%s%s" % ( settings.PAYPAL_ENDPOINT, "Pay" ), data=self.raw_request, headers=headers ).content()
+ logging.debug( "response was: %s" % self.raw_response )
+ self.response = json.loads( self.raw_response )
+
+ if shipping:
+ # generate setpaymentoptions request
+ options_raw_request = json.dumps( {
+ 'payKey': self.paykey(),
+ 'senderOptions': { 'requireShippingAddressSelection': 'true', 'shareAddress': 'true' },
+ 'requestEnvelope': { 'errorLanguage': 'en_US' }
+ } )
+ options_raw_response = url_request( "%s%s" % ( settings.PAYPAL_ENDPOINT, "SetPaymentOptions" ), data=options_raw_request, headers=headers ).content()
+ logging.debug( 'SetPaymentOptions response: %s' % options_raw_response )
+ # TODO check response was OK
+
+ def status( self ):
+ if self.response.has_key( 'paymentExecStatus' ):
+ return self.response['paymentExecStatus']
+ else:
+ return None
+
+ def amount( self ):
+ return decimal.Decimal(self.results[ 'payment_gross' ])
+
+ def paykey( self ):
+ return self.response['payKey']
+
+ def next_url( self ):
+ return '%s?cmd=_ap-payment&paykey=%s' % ( settings.PAYPAL_PAYMENT_HOST, self.response['payKey'] )
+
+class IPN( object ):
+ def __init__( self, request ):
+ # verify that the request is paypal's
+ self.error = None
+ #verify_request = urllib2.Request( "%s?cmd=_notify-validate" % settings.PAYPAL_PAYMENT_HOST, data=urllib.urlencode( request.POST.copy() ) )
+ #verify_response = urllib2.urlopen( verify_request )
+ verify_response = url_request( "%s?cmd=_notify-validate" % settings.PAYPAL_PAYMENT_HOST, data=urllib.urlencode( request.POST.copy() ) )
+ # check code
+ if verify_response.code() != 200:
+ self.error = 'PayPal response code was %i' % verify_response.code()
+ return
+ # check response
+ raw_response = verify_response.content()
+ if raw_response != 'VERIFIED':
+ self.error = 'PayPal response was "%s"' % raw_response
+ return
+ # check payment status
+ if request.get('status') != 'COMPLETED':
+ self.error = 'PayPal status was "%s"' % request.get('status')
+ return
+
+ (currency, amount) = request.get( "transaction[0].amount" ).split(' ')
+ if currency != 'USD':
+ self.error = 'Incorrect currency %s' % currency
+ return
+
+ self.amount = decimal.Decimal(amount)
+
+ def success( self ):
+ return self.error == None
+
+class ShippingAddress( object ):
+ def __init__( self, paykey, remote_address ):
+ headers = {
+ 'X-PAYPAL-SECURITY-USERID': settings.PAYPAL_USERID,
+ 'X-PAYPAL-SECURITY-PASSWORD': settings.PAYPAL_PASSWORD,
+ 'X-PAYPAL-SECURITY-SIGNATURE': settings.PAYPAL_SIGNATURE,
+ 'X-PAYPAL-REQUEST-DATA-FORMAT': 'JSON',
+ 'X-PAYPAL-RESPONSE-DATA-FORMAT': 'JSON',
+ 'X-PAYPAL-APPLICATION-ID': settings.PAYPAL_APPLICATION_ID,
+ 'X-PAYPAL-DEVICE-IPADDRESS': remote_address,
+ }
+
+ data = {
+ 'key': paykey,
+ 'requestEnvelope': { 'errorLanguage': 'en_US' },
+ }
+
+ self.raw_request = json.dumps(data)
+ self.raw_response = url_request( "%s%s" % ( settings.PAYPAL_ENDPOINT, "GetShippingAddresses" ), data=self.raw_request, headers=headers ).content()
+ logging.debug( "response was: %s" % self.raw_response )
+ self.response = json.loads( self.raw_response )
+
+class url_request( object ):
+ '''wrapper for urlfetch'''
+ def __init__( self, url, data=None, headers={} ):
+ # urlfetch - validated
+ #self.response = urlfetch.fetch( url, payload=data, headers=headers, method=urlfetch.POST, validate_certificate=True )
+ self.response = urlfetch.fetch( url, payload=data, headers=headers, method=urlfetch.POST, validate_certificate=False )
+ # urllib - not validated
+ #request = urllib2.Request(url, data=data, headers=headers)
+ #self.response = urllib2.urlopen( request )
+
+ def content( self ):
+ return self.response.content
+
+ def code( self ):
+ return self.response.status_code
+
+class Preapproval( object ):
+ def __init__( self, amount, return_url, cancel_url, remote_address ):
+ headers = {
+ 'X-PAYPAL-SECURITY-USERID': settings.PAYPAL_USERID,
+ 'X-PAYPAL-SECURITY-PASSWORD': settings.PAYPAL_PASSWORD,
+ 'X-PAYPAL-SECURITY-SIGNATURE': settings.PAYPAL_SIGNATURE,
+ 'X-PAYPAL-REQUEST-DATA-FORMAT': 'JSON',
+ 'X-PAYPAL-RESPONSE-DATA-FORMAT': 'JSON',
+ 'X-PAYPAL-APPLICATION-ID': settings.PAYPAL_APPLICATION_ID,
+ 'X-PAYPAL-DEVICE-IPADDRESS': remote_address,
+ }
+
+ now = datetime.datetime.utcnow()
+ expiry = now + datetime.timedelta( days=settings.PREAPPROVAL_PERIOD )
+ data = {
+ 'endingDate': expiry.isoformat() + "+00:00",
+ 'startingDate': now.isoformat() + "+00:00",
+ 'maxTotalAmountOfAllPayments': '%.2f' % amount,
+ 'currencyCode': 'USD',
+ 'returnUrl': return_url,
+ 'cancelUrl': cancel_url,
+ 'requestEnvelope': { 'errorLanguage': 'en_US' },
+ }
+
+ self.raw_request = json.dumps(data)
+ self.raw_response = url_request( "%s%s" % ( settings.PAYPAL_ENDPOINT, "Preapproval" ), data=self.raw_request, headers=headers ).content()
+ logging.debug( "response was: %s" % self.raw_response )
+ self.response = json.loads( self.raw_response )
+
+ def key( self ):
+ if self.response.has_key( 'preapprovalKey' ):
+ return self.response['preapprovalKey']
+ else:
+ return None
+
+ def next_url( self ):
+ return '%s?cmd=_ap-preapproval&preapprovalkey=%s' % ( settings.PAYPAL_PAYMENT_HOST, self.response['preapprovalKey'] )
+
+ def status( self ):
+ if self.response.has_key( 'responseEnvelope' ) and self.response['responseEnvelope'].has_key( 'ack' ):
+ return self.response['responseEnvelope']['ack']
+ else:
+ return None
+
+class PayWithPreapproval( object ):
+ def __init__( self, amount, preapproval_key ):
+ headers = {
+ 'X-PAYPAL-SECURITY-USERID': settings.PAYPAL_USERID,
+ 'X-PAYPAL-SECURITY-PASSWORD': settings.PAYPAL_PASSWORD,
+ 'X-PAYPAL-SECURITY-SIGNATURE': settings.PAYPAL_SIGNATURE,
+ 'X-PAYPAL-REQUEST-DATA-FORMAT': 'JSON',
+ 'X-PAYPAL-RESPONSE-DATA-FORMAT': 'JSON',
+ 'X-PAYPAL-APPLICATION-ID': settings.PAYPAL_APPLICATION_ID,
+ 'X-PAYPAL-DEVICE-IPADDRESS': '127.0.0.1', #remote_address,
+ }
+
+ data = {
+ 'actionType': 'PAY',
+ 'amount': '%.2f' % amount,
+ 'preapprovalKey': preapproval_key,
+ 'currencyCode': 'USD',
+ 'returnUrl': 'http://dummy',
+ 'cancelUrl': 'http://dummy',
+ 'requestEnvelope': { 'errorLanguage': 'en_US' },
+ 'receiverList': { 'receiver': [ { 'email': settings.PAYPAL_EMAIL, 'amount': '%.2f' % amount } ] }
+ }
+
+ self.raw_request = json.dumps(data)
+ self.raw_response = url_request( "%s%s" % ( settings.PAYPAL_ENDPOINT, "Pay" ), data=self.raw_request, headers=headers ).content()
+ logging.debug( "response was: %s" % self.raw_response )
+ self.response = json.loads( self.raw_response )
+
+ def status( self ):
+ if self.response.has_key( 'paymentExecStatus' ):
+ return self.response['paymentExecStatus']
+ else:
+ return None
+
+ def paykey( self ):
+ return self.response['payKey']
+
15 app/settings.py
@@ -0,0 +1,15 @@
+# settings for app
+
+PAYPAL_ENDPOINT = 'https://svcs.sandbox.paypal.com/AdaptivePayments/' # sandbox
+#PAYPAL_ENDPOINT = 'https://svcs.paypal.com/AdaptivePayments/' # production
+
+PAYPAL_PAYMENT_HOST = 'https://www.sandbox.paypal.com/au/cgi-bin/webscr' # sandbox
+#PAYPAL_PAYMENT_HOST = 'https://www.paypal.com/webscr' # production
+
+PAYPAL_USERID = '*** REQUIRED ***'
+PAYPAL_PASSWORD = '*** REQUIRED ***'
+PAYPAL_SIGNATURE = '*** REQUIRED ***'
+PAYPAL_APPLICATION_ID = 'APP-80W284485P519543T' # sandbox only
+PAYPAL_EMAIL = '*** REQUIRED ***'
+
+PREAPPROVAL_PERIOD = 182 # days to ask for in a preapproval
32 app/static/style.css
@@ -0,0 +1,32 @@
+body {
+ font-family: Helvetica, Arial, sans-serif;
+}
+
+#header {
+}
+
+#footer {
+ background-color: #ccf;
+ padding: 8px;
+}
+
+#message {
+ background-color: #fcc;
+ padding: 8px;
+}
+
+#sell_form {
+ background-color: #eee;
+}
+
+label {
+ width: 5em;
+ float: left;
+}
+
+#history {
+}
+
+#log {
+ color: #aaa;
+}
27 app/templates/base.htm
@@ -0,0 +1,27 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <link media="screen" href="/static/style.css" type="text/css" rel="stylesheet">
+ <title>{% block title %}{% endblock %}</title>
+ {% block head %}{% endblock %}
+</head>
+<body>
+ <div id="header">
+ <a href="/">Home</a>
+ <h2>{% block heading %}{% endblock %}</h2>
+ </div>
+{% if message %}
+ <div id="message">{{ message }}</div>
+{% endif %}
+
+ <div id="content">
+ {% block content %}
+ {% endblock %}
+ </div>
+ <br/>
+ <div id="footer">
+Sample application by <a href="http://www.supernifty.com.au/">Supernifty</a>.
+ </div>
+</body>
+</html>
20 app/templates/goal.htm
@@ -0,0 +1,20 @@
+{% extends "base.htm" %}
+
+{% block title %}Great Motivator{% endblock %}
+{% block heading %}Great Motivator{% endblock %}
+
+{% block head %}
+{% endblock %}
+
+{% block content %}
+<h2>Set a goal</h2>
+<form action="/goal" method="post">
+<table>
+<tr><td>Name of Goal: </td><td><input type="text" name="name"/><br/>
+<tr><td>Target Word Count: </td><td><input type="text" name="count"/></td></tr>
+<tr><td>Motivator ($): </td><td><input type="text" name="amount"/></td></tr>
+<tr><td>Target Date (m/d/y): </td><td><input type="text" name="date"/></td></tr>
+<tr><td><input type="submit"/></td></tr>
+</table>
+</form>
+{% endblock %}
32 app/templates/main.htm
@@ -0,0 +1,32 @@
+{% extends "base.htm" %}
+
+{% block title %}Great Motivator{% endblock %}
+{% block heading %}Great Motivator{% endblock %}
+
+{% block head %}
+{% endblock %}
+
+{% block content %}
+<h2>Welcome to the great motivator</h2>
+{% if profile.goal_active %}
+<p>
+Your current goal is to complete <b>{{ profile.goal_count }} words</b> by <b>{{ profile.goal_date }}</b>.<br/>
+Otherwise, the cost will be <b>${{ profile.amount_dollars }}</b>.
+</p>
+<p>
+So far, you have entered <b>{{ profile.current_count }} words</b>. <a href="/words">View</a>
+</p>
+<p>
+<b>Add to your literary masterpiece</b><br/>
+<form method="post" action="/">
+<textarea name="words" style="width: 100%; height: 240px;">
+</textarea>
+<input type="submit" value="Add to masterpiece"/>
+</form>
+</p>
+{% else %}
+<p>
+You haven't set a goal. <a href="/goal">Set a goal</a>.
+</p>
+{% endif %}
+{% endblock %}
17 app/util.py
@@ -0,0 +1,17 @@
+import logging
+import random
+import simplejson
+import string
+
+from google.appengine.api import channel
+from google.appengine.api import users
+
+import model
+
+def random_alnum( count ):
+ chars = string.letters + string.digits
+ result = ''
+ for i in range(count):
+ result += random.choice(chars)
+ return result
+
Please sign in to comment.
Something went wrong with that request. Please try again.