From 0d88a395cbccf0fc9ec843a0efc8176c875db197 Mon Sep 17 00:00:00 2001 From: Raymond Yee Date: Thu, 16 Aug 2012 14:11:32 -0700 Subject: [PATCH 001/107] First pass at stripe prototype --> /stripe/test will now let you type in a test credit card and display resulting stripeToken --- payment/forms.py | 7 +++ payment/templates/stripe.html | 84 +++++++++++++++++++++++++++++++++++ payment/urls.py | 2 + payment/views.py | 15 ++++++- 4 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 payment/forms.py create mode 100644 payment/templates/stripe.html diff --git a/payment/forms.py b/payment/forms.py new file mode 100644 index 00000000..6862b686 --- /dev/null +++ b/payment/forms.py @@ -0,0 +1,7 @@ +from django import forms +import logging + +logger = logging.getLogger(__name__) + +class StripePledgeForm(forms.Form): + stripeToken = forms.CharField(required=False, widget=forms.HiddenInput()) \ No newline at end of file diff --git a/payment/templates/stripe.html b/payment/templates/stripe.html new file mode 100644 index 00000000..8cc5dd01 --- /dev/null +++ b/payment/templates/stripe.html @@ -0,0 +1,84 @@ +{% extends "basepledge.html" %} +{% load humanize %} + +{% block title %}Stripe{% endblock %} + +{% block extra_extra_head %} + + + + + + + + + +{% endblock %} + +{% block doccontent %} +Stripe Test: + + +
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + + / + +
+ +
+ +{% endblock %} + diff --git a/payment/urls.py b/payment/urls.py index 47e302a7..818c8058 100644 --- a/payment/urls.py +++ b/payment/urls.py @@ -1,5 +1,6 @@ from django.conf.urls.defaults import * from django.conf import settings +from regluit.payment.views import StripeView urlpatterns = patterns( "regluit.payment.views", @@ -28,6 +29,7 @@ if settings.DEBUG: url(r"^testfinish", "testFinish"), url(r"^testrefund", "testRefund"), url(r"^testmodify", "testModify"), + url(r"^stripe/test", StripeView.as_view()), ) \ No newline at end of file diff --git a/payment/views.py b/payment/views.py index 1b94f551..bea0870d 100644 --- a/payment/views.py +++ b/payment/views.py @@ -2,6 +2,9 @@ from regluit.payment.manager import PaymentManager from regluit.payment.paypal import IPN from regluit.payment.models import Transaction from regluit.core.models import Campaign, Wishlist + +from regluit.payment.forms import StripePledgeForm + from django.conf import settings from django.core.urlresolvers import reverse from django.shortcuts import render_to_response @@ -13,6 +16,9 @@ from django.views.decorators.csrf import csrf_exempt from django.test.utils import setup_test_environment from django.template import RequestContext +from django.views.generic.edit import FormView +from django.views.generic.base import TemplateView + from unittest import TestResult from regluit.payment.tests import PledgeTest, AuthorizeTest import uuid @@ -322,5 +328,10 @@ def checkStatus(request): def _render(request, template, template_vars={}): return render_to_response(template, template_vars, RequestContext(request)) - - \ No newline at end of file +class StripeView(FormView): + template_name="stripe.html" + form_class = StripePledgeForm + + def form_valid(self, form): + stripeToken = form.cleaned_data["stripeToken"] + return HttpResponse("stripeToken: {0}".format(stripeToken)) \ No newline at end of file From e81a25b1bd060de4faa25942b3a7a3e7ab935cbe Mon Sep 17 00:00:00 2001 From: Raymond Yee Date: Thu, 16 Aug 2012 17:24:18 -0700 Subject: [PATCH 002/107] Now documenting more of what we need to do next with stripe if we go for a fullblown implementation --- payment/stripelib.py | 70 +++++++++++++++++++++++++++++++++++ payment/templates/stripe.html | 2 +- payment/views.py | 17 ++++++++- 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 payment/stripelib.py diff --git a/payment/stripelib.py b/payment/stripelib.py new file mode 100644 index 00000000..7428b1ce --- /dev/null +++ b/payment/stripelib.py @@ -0,0 +1,70 @@ +import stripe + +# should load the keys for Stripe from db -- but for now just hardcode here +try: + from regluit.core.models import Key + STRIPE_PK = Key.objects.get(name="STRIPE_PK").value + STRIPE_SK = Key.objects.get(name="STRIPE_SK").value + logger.info('Successful loading of STRIPE_*_KEYs') +except Exception, e: + STRIPE_PK = 'pk_0AnIkNu4WRiJYzxMKgruiUwxzXP2T' + STRIPE_SK = 'sk_0AnIvBrnrJoFpfD3YmQBVZuTUAbjs' + +stripe.api_key = STRIPE_SK + +# if you create a Customer object, then you'll be able to charge multiple times. You can create a customer with a token. + +# https://stripe.com/docs/tutorials/charges + +def create_customer (card=None, description=None, email=None, account_balance=None, plan=None, trial_end=None): + """card is a dictionary or a token""" + # https://stripe.com/docs/api?lang=python#create_customer + + customer = stripe.Customer.create( + card=card, + description=description, + email=email, + account_balance=account_balance, + plan=plan, + trial_end=trial_end + ) + + # customer.id is useful to save in db + return customer + +# if customer.id doesn't exist, create one and then charge the customer +# we probably should ask our users whether they are ok with our creating a customer id account -- or ask for credit +# card info each time.... + +def create_charge(amount, currency="usd", customer=None, card=None, description=None ): +# https://stripe.com/docs/api?lang=python#create_charge +# customer or card required but not both +# charge the Customer instead of the card +# amount in cents + charge = stripe.Charge.create( + amount=int(100*amount), # in cents + currency=currency, + customer=customer.id, + description=description + ) + + return charge + +def refund_charge(id): + # https://stripe.com/docs/api?lang=python#refund_charge + ch = stripe.Charge.retrieve(id) + ch.refund() + return ch + +def list_all_charges(count=None, offset=None, customer=None): + # https://stripe.com/docs/api?lang=python#list_charges + return stripe.Charge.all(count=count, offset=offset, customer=customer) + +# key entities: Charge, Customer, Token, Event +# IPNs/webhooks: https://stripe.com/docs/webhooks + +# charge object: https://stripe.com/docs/api?lang=python#charge_object +# need to study to figure out db schema + +# all events +# https://stripe.com/docs/api?lang=python#list_events diff --git a/payment/templates/stripe.html b/payment/templates/stripe.html index 8cc5dd01..949150bc 100644 --- a/payment/templates/stripe.html +++ b/payment/templates/stripe.html @@ -12,7 +12,7 @@ + + +{% endblock %} + diff --git a/payment/urls.py b/payment/urls.py index 818c8058..fc7dbbdd 100644 --- a/payment/urls.py +++ b/payment/urls.py @@ -1,6 +1,6 @@ from django.conf.urls.defaults import * from django.conf import settings -from regluit.payment.views import StripeView +from regluit.payment.views import StripeView, BalancedView urlpatterns = patterns( "regluit.payment.views", @@ -30,6 +30,7 @@ if settings.DEBUG: url(r"^testrefund", "testRefund"), url(r"^testmodify", "testModify"), url(r"^stripe/test", StripeView.as_view()), + url(r"^balanced/test", BalancedView.as_view()), ) \ No newline at end of file diff --git a/payment/views.py b/payment/views.py index b1017140..0c3d9425 100644 --- a/payment/views.py +++ b/payment/views.py @@ -4,7 +4,9 @@ from regluit.payment.models import Transaction from regluit.core.models import Campaign, Wishlist from regluit.payment.stripelib import STRIPE_PK -from regluit.payment.forms import StripePledgeForm +from regluit.payment.balancedlib import MARKETPLACE_URI + +from regluit.payment.forms import StripePledgeForm, BalancedPledgeForm from django.conf import settings from django.core.urlresolvers import reverse @@ -347,6 +349,24 @@ class StripeView(FormView): # e.g., tok_0C0k4jG5B2Oxox # return HttpResponse("stripeToken: {0}".format(stripeToken)) + +class BalancedView(FormView): + template_name="balanced.html" + form_class = BalancedPledgeForm + + def get_context_data(self, **kwargs): + context = super(BalancedView, self).get_context_data(**kwargs) + + context.update({ + 'MARKETPLACE_URI':MARKETPLACE_URI + }) + return context + + def form_valid(self, form): + + card_uri = form.cleaned_data["card_uri"] + return HttpResponse("card_uri: {0}".format(card_uri)) + \ No newline at end of file diff --git a/requirements.pip b/requirements.pip index 4570e060..7b45fcf0 100644 --- a/requirements.pip +++ b/requirements.pip @@ -28,3 +28,5 @@ pycrypto django-maintenancemode django-smtp-ssl django-ckeditor +stripe +balanced \ No newline at end of file From 214bc4b9e51a6f70f3f9d5430668eb7b7a115de1 Mon Sep 17 00:00:00 2001 From: Raymond Yee Date: Mon, 20 Aug 2012 15:19:27 -0700 Subject: [PATCH 004/107] A pass to get a charge iframe show up for wepay --- payment/forms.py | 5 ++++- payment/templates/wepay.html | 24 +++++++++++++++++++++++ payment/urls.py | 3 ++- payment/views.py | 22 ++++++++++++++++++++- payment/wepaylib.py | 37 ++++++++++++++++++++++++++++++++++++ requirements.pip | 3 ++- 6 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 payment/templates/wepay.html create mode 100644 payment/wepaylib.py diff --git a/payment/forms.py b/payment/forms.py index 571258fd..cb474b5e 100644 --- a/payment/forms.py +++ b/payment/forms.py @@ -7,4 +7,7 @@ class StripePledgeForm(forms.Form): stripeToken = forms.CharField(required=False, widget=forms.HiddenInput()) class BalancedPledgeForm(forms.Form): - card_uri = forms.CharField(required=False, widget=forms.HiddenInput()) \ No newline at end of file + card_uri = forms.CharField(required=False, widget=forms.HiddenInput()) + +class WepayPledgeForm(forms.Form): + pass \ No newline at end of file diff --git a/payment/templates/wepay.html b/payment/templates/wepay.html new file mode 100644 index 00000000..67e73871 --- /dev/null +++ b/payment/templates/wepay.html @@ -0,0 +1,24 @@ +{% extends "basepledge.html" %} +{% load humanize %} + +{% block title %}WePay{% endblock %} + +{% block extra_extra_head %} + + + +{% endblock %} + +{% block doccontent %} + +
+ + + + + + +{% endblock %} + diff --git a/payment/urls.py b/payment/urls.py index fc7dbbdd..7b32df85 100644 --- a/payment/urls.py +++ b/payment/urls.py @@ -1,6 +1,6 @@ from django.conf.urls.defaults import * from django.conf import settings -from regluit.payment.views import StripeView, BalancedView +from regluit.payment.views import StripeView, BalancedView, WepayView urlpatterns = patterns( "regluit.payment.views", @@ -31,6 +31,7 @@ if settings.DEBUG: url(r"^testmodify", "testModify"), url(r"^stripe/test", StripeView.as_view()), url(r"^balanced/test", BalancedView.as_view()), + url(r"^wepay/test", WepayView.as_view()), ) \ No newline at end of file diff --git a/payment/views.py b/payment/views.py index 0c3d9425..ac9d49be 100644 --- a/payment/views.py +++ b/payment/views.py @@ -5,8 +5,9 @@ from regluit.core.models import Campaign, Wishlist from regluit.payment.stripelib import STRIPE_PK from regluit.payment.balancedlib import MARKETPLACE_URI +from regluit.payment import wepaylib -from regluit.payment.forms import StripePledgeForm, BalancedPledgeForm +from regluit.payment.forms import StripePledgeForm, BalancedPledgeForm, WepayPledgeForm from django.conf import settings from django.core.urlresolvers import reverse @@ -367,6 +368,25 @@ class BalancedView(FormView): card_uri = form.cleaned_data["card_uri"] return HttpResponse("card_uri: {0}".format(card_uri)) + +class WepayView(FormView): + template_name="wepay.html" + form_class = WepayPledgeForm + + def get_context_data(self, **kwargs): + + context = super(WepayView, self).get_context_data(**kwargs) + + # compute a uri to embed in frame + # test payment of $10 + + r = wepaylib.create_checkout(10.00, "test charge of 10.00", mode='iframe') + + context.update({ + 'checkout_uri':r['checkout_uri'] + }) + return context + \ No newline at end of file diff --git a/payment/wepaylib.py b/payment/wepaylib.py new file mode 100644 index 00000000..bc4ae8e2 --- /dev/null +++ b/payment/wepaylib.py @@ -0,0 +1,37 @@ +from wepay import WePay + +# should load the keys for WePay from db -- but for now just hardcode here + +try: + from regluit.core.models import Key + WEPAY_ACCESS_TOKEN = Key.objects.get(name="WEPAY_ACCESS_TOKEN").value + WEPAY_CLIENT_SECRET = Key.objects.get(name="WEPAY_CLIENT_SECRET").value + WEPAY_ACCOUNT_ID = Key.objects.get(name="WEPAY_ACCOUNT_ID").value + logger.info('Successful loading of WEPAY_*_KEYs') +except Exception, e: + WEPAY_ACCESS_TOKEN = 'a680cfd2b814ef0e4938b865c96879136a74970ad6c9425e8c98de50b40007af' + WEPAY_CLIENT_SECRET= 'f9bf05cd50' + WEPAY_ACCOUNT_ID = 1226963 + +# set production to True for live environments +PRODUCTION = False +wepay = WePay(PRODUCTION, WEPAY_ACCESS_TOKEN) + +# collect credit card and do a delay charge to gluejar -- then to LR.... + +# Then, you can charge customers on their behalf. It?s just like charging a customer normally, except you should use their access_token and account_id instead of your own. + +def create_checkout(amount, description, account_id=WEPAY_ACCOUNT_ID, mode="iframe", type='GOODS'): + + # type: one of GOODS, SERVICE, PERSONAL, or DONATION + # create the checkout + response = wepay.call('/checkout/create', { + 'account_id': WEPAY_ACCOUNT_ID, + 'amount': amount, + 'short_description': description, + 'type': type, + 'mode': mode + }) + + # display the response + return response diff --git a/requirements.pip b/requirements.pip index 7b45fcf0..b817c5c7 100644 --- a/requirements.pip +++ b/requirements.pip @@ -29,4 +29,5 @@ django-maintenancemode django-smtp-ssl django-ckeditor stripe -balanced \ No newline at end of file +balanced +git+ssh://git@github.com/wepay/Python-SDK.git \ No newline at end of file From 599d333e34dee5e986fe428b71149b2c5a81a0d1 Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 22 Aug 2012 14:38:28 -0400 Subject: [PATCH 005/107] typo --- frontend/templates/press.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/templates/press.html b/frontend/templates/press.html index 52269827..9d936c9b 100644 --- a/frontend/templates/press.html +++ b/frontend/templates/press.html @@ -105,7 +105,7 @@ Creative Commons offers a variety of other licenses, many of them with even less Die Zeit - July 9, 2012
- Unglue.it Wants to Make a Creative Commons for Ebooks
+ Unglue.it Wants to Make a Creative Commons for Ebooks
Mashable - June 14, 2012
From aa3667d260f29a219b8ba6ada19cf9830dfaecc3 Mon Sep 17 00:00:00 2001 From: Andromeda Yelton Date: Thu, 23 Aug 2012 13:38:48 -0400 Subject: [PATCH 006/107] working on download page --- frontend/templates/work.html | 12 +++++++++++- frontend/views.py | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/templates/work.html b/frontend/templates/work.html index 6d4157d4..74e4a69a 100644 --- a/frontend/templates/work.html +++ b/frontend/templates/work.html @@ -134,6 +134,16 @@ $j(document).ready(function(){ {% else %}
{% endif %} + {% else %} + {% if status == 'SUCCESSFUL' %} +
+ {% if unglued_ebook %} + Download + {% else %} + Coming soon! + {% endif %} +
+ {% endif %} {% endif %}
@@ -415,7 +425,7 @@ $j(document).ready(function(){ {% if premium.limit == 0 or premium.limit > premium.premium_count %}
  • - ${{ premium.amount|floatformat:0|intcomma }}{% if pledged.0.premium == premium %}
    Pledged!
    {% endif %} + {% if premium.amount %}${{ premium.amount|floatformat:0|intcomma }}{% else %}Any amount{% endif %}{% if pledged.0.premium == premium %}
    Pledged!
    {% endif %} {{ premium.description }} {% ifnotequal premium.limit 0 %}
    Only {{ premium.premium_remaining }} remaining! {% endifnotequal %}
  • diff --git a/frontend/views.py b/frontend/views.py index 26bdab25..9c05f41c 100755 --- a/frontend/views.py +++ b/frontend/views.py @@ -166,6 +166,8 @@ def work(request, work_id, action='display'): countdown = "in %s minutes" % str(time_remaining.seconds/60 + 1) else: countdown = "right now" + elif work.last_campaign_status() == 'SUCCESSFUL': + unglued_ebook = work.ebooks().filter(unglued=True) if action == 'preview': work.last_campaign_status = 'ACTIVE' From 228029e8e5376c9a436c90a49c0b02d2b149159d Mon Sep 17 00:00:00 2001 From: Raymond Yee Date: Thu, 23 Aug 2012 11:08:14 -0700 Subject: [PATCH 007/107] add sysadmin/__init__.py --- sysadmin/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 sysadmin/__init__.py diff --git a/sysadmin/__init__.py b/sysadmin/__init__.py new file mode 100644 index 00000000..e69de29b From 9c602cf5306d2f58278c09043bfc94af43795701 Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 23 Aug 2012 18:24:52 -0400 Subject: [PATCH 008/107] typo --- frontend/templates/press.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/templates/press.html b/frontend/templates/press.html index 52269827..9d936c9b 100644 --- a/frontend/templates/press.html +++ b/frontend/templates/press.html @@ -105,7 +105,7 @@ Creative Commons offers a variety of other licenses, many of them with even less Die Zeit - July 9, 2012
    From 4643bf4ac5609ab7e967c749f60a95ffb1b3081d Mon Sep 17 00:00:00 2001 From: Andromeda Yelton Date: Fri, 24 Aug 2012 15:13:29 -0400 Subject: [PATCH 009/107] tweak to be compatible with download page --- static/js/wishlist.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/static/js/wishlist.js b/static/js/wishlist.js index 47435eb6..541a5c8c 100644 --- a/static/js/wishlist.js +++ b/static/js/wishlist.js @@ -4,7 +4,7 @@ $j().ready(function() { // only do the lookup once, then cache it var contentblock = $j('#content-block'); - contentblock.on("click", "div.add-wishlist", function () { + contentblock.on("click", ".add-wishlist", function () { var span = $j(this).find("span"); var id_val = span.attr('id').substring(1); var id_type = span.attr('class'); @@ -25,6 +25,11 @@ $j().ready(function() { else { span.html('a type error occurred'); } + + // prevent perversities on download page + if ($j(this).is("a")) { + $j(this).removeClass("add-wishlist").addClass("success"); + } }); contentblock.on("click", "div.remove-wishlist", function() { From 07a50bfa45636b44547440339768e026743235e1 Mon Sep 17 00:00:00 2001 From: Andromeda Yelton Date: Fri, 24 Aug 2012 15:14:08 -0400 Subject: [PATCH 010/107] we should have non-input-buttons that are styled the same for calls to action --- static/css/sitewide.css | 12 +++++++++--- static/less/sitewide.less | 8 ++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/static/css/sitewide.css b/static/css/sitewide.css index 50814df8..70da2be2 100644 --- a/static/css/sitewide.css +++ b/static/css/sitewide.css @@ -215,17 +215,22 @@ img { border: none; } input, -textarea { +textarea, +a.fakeinput { border: 2px solid #d6dde0; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; } input:focus, -textarea:focus { +textarea:focus, +a.fakeinput:focus { border: 2px solid #8dc63f; outline: none; } +a.fakeinput:hover { + text-decoration: none; +} .js-search input { -moz-border-radius: 0; -webkit-border-radius: 0; @@ -259,7 +264,8 @@ h3.jsmod-title span { padding: 26px 40px 27px 20px; display: block; } -input[type="submit"] { +input[type="submit"], +a.fakeinput { background: #8dc63f; color: white; font-weight: bold; diff --git a/static/less/sitewide.less b/static/less/sitewide.less index b549dd25..804252a0 100644 --- a/static/less/sitewide.less +++ b/static/less/sitewide.less @@ -122,7 +122,7 @@ img { border:none; } -input, textarea { +input, textarea, a.fakeinput { border: 2px solid @blue-grey; .one-border-radius(5px); @@ -132,6 +132,10 @@ input, textarea { } } +a.fakeinput:hover { + text-decoration: none; +} + .js-search input { .one-border-radius(0); } @@ -170,7 +174,7 @@ h3 { } } -input[type="submit"] { +input[type="submit"], a.fakeinput { background: @call-to-action; color: white; font-weight: bold; From a6e034a36a891c09b06b4743e9ee4f2e507c1564 Mon Sep 17 00:00:00 2001 From: Andromeda Yelton Date: Fri, 24 Aug 2012 15:14:32 -0400 Subject: [PATCH 011/107] rudimentary download page plus logic to serve it --- frontend/templates/download.html | 53 +++++++++++++ frontend/templates/work.html | 42 +---------- frontend/urls.py | 1 + frontend/views.py | 70 ++++++++++++----- static/css/download.css | 124 +++++++++++++++++++++++++++++++ static/less/download.less | 38 ++++++++++ 6 files changed, 270 insertions(+), 58 deletions(-) create mode 100644 frontend/templates/download.html create mode 100644 static/css/download.css create mode 100644 static/less/download.less diff --git a/frontend/templates/download.html b/frontend/templates/download.html new file mode 100644 index 00000000..09b4634e --- /dev/null +++ b/frontend/templates/download.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} + +{% with work.title as title %} +{% block title %} +— Downloads for {{ work.title }} +{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block extra_js %} + +{% endblock %} + +{% block content %} +
    +

    Downloads for {{ work.title }}

    +{% if unglued_ebook %} +
    +

    Get the unglued edition!

    + +
    +{% endif %} +{% if other_ebooks %} + {% if unglued_ebook %}

    Freely available editions

    {% endif %} + {% comment %} + the header is only necessary if we have an unglued edition we're distinguishing + non-unglued editions from + {% endcomment %} + {% for edition in other_ebooks %} +
    + {% with edition.url as url %} + + {{ edition.format }} + {% endwith %} +
    + {% endfor %} +{% endif %} +{% if unglued_ebook or other_ebooks %} +blah blah download instructions +{% else %} +

    There are no freely available downloads of {{ work.title }} right now. {% if not work in request.user.wishlist.works.all %}Would you like there to be? Add this book to your wishlist.{% else %}Ask your friends to add it to their wishlists!{% endif %}

    + +

    If you know of a Creative-Commons-licensed or US public domain edition of this book, you can add it through the Rights tab of the book page.

    +{% endif %} +
    +{% endblock %} + +{% endwith %} \ No newline at end of file diff --git a/frontend/templates/work.html b/frontend/templates/work.html index 74e4a69a..1a4d3ca4 100644 --- a/frontend/templates/work.html +++ b/frontend/templates/work.html @@ -28,16 +28,6 @@ $j(document).ready(function(){ $j(this).next().toggle(); }); }); -$j(document).ready(function(){ - $j('.show_more_ebooks').click(function(){ - if ($j(this).html() == '
    hide downloads') { - $j(this).html('
    more downloads...') - } else { - $j(this).html('
    hide downloads') - } - $j(this).next().toggle(); - }); -}); $j(document).ready(function(){ var img = $j('#book-detail-img'); var googimg = $j('#find-google img'); @@ -135,42 +125,14 @@ $j(document).ready(function(){
    {% endif %} {% else %} - {% if status == 'SUCCESSFUL' %} + {% if work.first_ebook %}
    - {% if unglued_ebook %} - Download - {% else %} - Coming soon! - {% endif %} + Download
    {% endif %} {% endif %}
    - {% if work.first_ebook %} -
    - - - {% for ebook in work.ebooks %} - {% if forloop.first %} - - {% endif %} - {% if forloop.counter == 2 %} - -
    More downloads...
    - - {% endif %} - {% if not forloop.first %} -
    - {% endif %} -  {{ ebook.format }} at {{ebook.provider}}{{ebook.rights}} - {% if forloop.last %} -
    - {% endif %} - {% endfor %} -
    -
    - {% endif %}
    +
    +{% if faqmenu == 'modify' %}We hope you won't, but of course you're also free to cancel your pledge.{% endif %}
    {% if transaction.ack_name %} diff --git a/frontend/urls.py b/frontend/urls.py index 8436a444..1b2c4ce8 100644 --- a/frontend/urls.py +++ b/frontend/urls.py @@ -7,7 +7,7 @@ from django.conf import settings from regluit.core.feeds import SupporterWishlistFeed from regluit.core.models import Campaign -from regluit.frontend.views import GoodreadsDisplayView, LibraryThingView, PledgeView, PledgeCompleteView, PledgeModifyView, PledgeCancelView, PledgeNeverMindView, PledgeRechargeView, FAQView +from regluit.frontend.views import GoodreadsDisplayView, LibraryThingView, PledgeView, PledgeCompleteView, PledgeCancelView, PledgeRechargeView, FAQView from regluit.frontend.views import CampaignListView, DonateView, WorkListView, UngluedListView, InfoPageView, InfoLangView, DonationView urlpatterns = patterns( @@ -55,8 +55,7 @@ urlpatterns = patterns( url(r"^pledge/(?P\d+)/$", login_required(PledgeView.as_view()), name="pledge"), url(r"^pledge/cancel/(?P\d+)$", login_required(PledgeCancelView.as_view()), name="pledge_cancel"), url(r"^pledge/complete/$", login_required(PledgeCompleteView.as_view()), name="pledge_complete"), - url(r"^pledge/nevermind/$", login_required(PledgeNeverMindView.as_view()), name="pledge_nevermind"), - url(r"^pledge/modify/(?P\d+)$", login_required(PledgeModifyView.as_view()), name="pledge_modify"), + url(r"^pledge/modify/(?P\d+)$", login_required(PledgeView.as_view()), name="pledge_modify"), url(r"^pledge/recharge/(?P\d+)$", login_required(PledgeRechargeView.as_view()), name="pledge_recharge"), url(r"^subjects/$", "subjects", name="subjects"), url(r"^librarything/$", LibraryThingView.as_view(), name="librarything"), diff --git a/frontend/views.py b/frontend/views.py index b51d934a..0944a2f3 100755 --- a/frontend/views.py +++ b/frontend/views.py @@ -50,8 +50,8 @@ from regluit.frontend.forms import EbookForm, CustomPremiumForm, EditManagersFor from regluit.frontend.forms import getTransferCreditForm from regluit.payment.manager import PaymentManager from regluit.payment.models import Transaction -from regluit.payment.parameters import TARGET_TYPE_CAMPAIGN, TARGET_TYPE_DONATION, PAYMENT_TYPE_AUTHORIZATION from regluit.payment.parameters import TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE, TRANSACTION_STATUS_CANCELED, TRANSACTION_STATUS_ERROR, TRANSACTION_STATUS_FAILED, TRANSACTION_STATUS_INCOMPLETE +from regluit.payment.parameters import PAYMENT_TYPE_AUTHORIZATION from regluit.core import goodreads from tastypie.models import ApiKey from regluit.payment.models import Transaction @@ -583,280 +583,142 @@ class DonationView(TemplateView): class PledgeView(FormView): template_name="pledge.html" form_class = CampaignPledgeForm + transaction = None + campaign = None + work = None + premiums = None + data = None - def get(self, request, *args, **kwargs): - # change the default behavior from https://code.djangoproject.com/browser/django/tags/releases/1.3.1/django/views/generic/edit.py#L129 - # don't automatically bind the data to the form on GET, only on POST - # compare with https://code.djangoproject.com/browser/django/tags/releases/1.3.1/django/views/generic/edit.py#L34 - form_class = self.get_form_class() - form = form_class() - - context_data = self.get_context_data(form=form) - # if there is already an active campaign pledge for user, redirect to the pledge modify page - if context_data.get('redirect_to_modify_pledge'): - work = context_data['work'] - return HttpResponseRedirect(reverse('pledge_modify', args=[work.id])) - else: - return self.render_to_response(context_data) - - def get_context_data(self, **kwargs): - """set up the pledge page""" - - # the following should be true since PledgeModifyView.as_view is wrapped in login_required + def get_form_kwargs(self): assert self.request.user.is_authenticated() - user = self.request.user - - context = super(PledgeView, self).get_context_data(**kwargs) - - work = get_object_or_404(models.Work, id=self.kwargs["work_id"]) - campaign = work.last_campaign() + self.work = get_object_or_404(models.Work, id=self.kwargs["work_id"]) # if there is no campaign or if campaign is not active, we should raise an error - - if campaign is None or campaign.status != 'ACTIVE': - raise Http404 - - custom_premiums = campaign.custom_premiums() - # need to also include the no-premiums default in the queryset we send the page - premiums = custom_premiums | models.Premium.objects.filter(id=150) - premium_id = self.request.REQUEST.get('premium_id', None) - preapproval_amount = self.request.REQUEST.get('preapproval_amount', None) + try: + self.campaign = self.work.last_campaign() + # TODO need to sort the premiums + self.premiums = self.campaign.custom_premiums() | models.Premium.objects.filter(id=150) + # Campaign must be ACTIVE + assert self.campaign.status == 'ACTIVE' + except Exception, e: + raise e + + transactions = self.campaign.transactions().filter(user=self.request.user, status=TRANSACTION_STATUS_ACTIVE, type=PAYMENT_TYPE_AUTHORIZATION) + if transactions.count() == 0: + premium_id = self.request.REQUEST.get('premium_id', None) + preapproval_amount = self.request.REQUEST.get('preapproval_amount', None) + premium_description = None + ack_name='' + ack_dedication='' + anonymous='' + + else: + self.transaction = transactions[0] + # what stuff do we need to pull out to populate form? + # preapproval_amount, premium_id (which we don't have stored yet) + if self.transaction.premium is not None: + premium_id = self.transaction.premium.id + premium_description = self.transaction.premium.description + else: + premium_id = None + premium_description = None + preapproval_amount = self.transaction.amount + ack_name=self.transaction.ack_name + ack_dedication=self.transaction.ack_dedication + anonymous=self.transaction.anonymous if premium_id is not None and preapproval_amount is None: try: preapproval_amount = D(models.Premium.objects.get(id=premium_id).amount) except: preapproval_amount = None - - data = {'preapproval_amount':preapproval_amount, 'premium_id':premium_id} - - form_class = self.get_form_class() - - # no validation errors, please, when we're only doing a GET - # to avoid validation errors, don't bind the form - - if preapproval_amount is not None: - form = form_class(data) + self.data = {'preapproval_amount':preapproval_amount, + 'premium_id':premium_id, 'premium_description':premium_description, + 'ack_name':ack_name, 'ack_dedication':ack_dedication, 'anonymous':anonymous} + if self.request.method == 'POST': + self.data.update(self.request.POST.dict()) + if self.request.method == 'POST' or premium_id: + return {'data':self.data} else: - form = form_class() - + return {} + + def get_context_data(self, **kwargs): + """set up the pledge page""" + + context = super(PledgeView, self).get_context_data(**kwargs) + try: - pubdate = work.publication_date[:4] + pubdate = self.work.publication_date[:4] except IndexError: pubdate = 'unknown' context.update({ - 'redirect_to_modify_pledge':False, - 'work':work,'campaign':campaign, - 'premiums':premiums, 'form':form, - 'premium_id':premium_id, - 'faqmenu': 'pledge', + 'work':self.work, + 'campaign':self.campaign, + 'premiums':self.premiums, + 'premium_id':self.data['premium_id'], + 'faqmenu': 'modify' if self.transaction else 'pledge', 'pubdate':pubdate, - 'payment_processor':settings.PAYMENT_PROCESSOR, - }) + 'transaction': self.transaction, + 'tid': self.transaction.id if self.transaction else None, + 'premium_description': self.data['premium_description'], + 'preapproval_amount':self.data['preapproval_amount'], + 'payment_processor':self.transaction.host if self.transaction else None, + }) - # check whether the user already has an ACTIVE transaction for the given campaign. - # if so, we should redirect the user to modify pledge page - # BUGBUG: but what about Completed Transactions? - transactions = campaign.transactions().filter(user=user, status=TRANSACTION_STATUS_ACTIVE) - if transactions.count() > 0: - context.update({'redirect_to_modify_pledge':True}) - else: - context.update({'redirect_to_modify_pledge':False}) - return context - def form_valid(self, form): - work_id = self.kwargs["work_id"] - preapproval_amount = form.cleaned_data["preapproval_amount"] - anonymous = form.cleaned_data["anonymous"] - ack_name = form.cleaned_data["ack_name"] - ack_link = form.cleaned_data["ack_link"] - ack_dedication = form.cleaned_data["ack_dedication"] + def get_premium(self,form): + premium_id = form.cleaned_data["premium_id"] + # confirm that the premium_id is a valid one for the campaign in question + try: + premium = models.Premium.objects.get(id=premium_id) + if not (premium.campaign is None or premium.campaign == self.campaign): + premium = None + except models.Premium.DoesNotExist, e: + premium = None + return premium + def form_valid(self, form): # right now, if there is a non-zero pledge amount, go with that. otherwise, do the pre_approval - campaign = models.Work.objects.get(id=int(work_id)).last_campaign() - premium_id = form.cleaned_data["premium_id"] - # confirm that the premium_id is a valid one for the campaign in question - try: - premium = models.Premium.objects.get(id=premium_id) - if not (premium.campaign is None or premium.campaign == campaign): - premium = None - except models.Premium.DoesNotExist, e: - premium = None - - p = PaymentManager(embedded=self.embedded) - - # PledgeView is wrapped in login_required -- so in theory, user should never be None -- but I'll keep this logic here for now. - if self.request.user.is_authenticated(): - user = self.request.user + p = PaymentManager() + if self.transaction: + # modifying the transaction... + assert self.transaction.type == PAYMENT_TYPE_AUTHORIZATION and self.transaction.status == TRANSACTION_STATUS_ACTIVE + status, url = p.modify_transaction(self.transaction, form.cleaned_data["preapproval_amount"], + premium=self.get_premium(form), + paymentReason="Unglue.it Pledge for {0}".format(self.campaign.name), + ack_name=form.cleaned_data["ack_name"], + ack_dedication=form.cleaned_data["ack_dedication"], + anonymous=form.cleaned_data["anonymous"], + ) + logger.info("status: {0}, url:{1}".format(status, url)) + + if status and url is not None: + logger.info("PledgeView (Modify): " + url) + return HttpResponseRedirect(url) + elif status and url is None: + return HttpResponseRedirect("{0}?tid={1}".format(reverse('pledge_complete'), self.transaction.id)) + else: + return HttpResponse("No modification made") else: - user = None - - if not self.embedded: - - return_url = None - nevermind_url = None - - # the recipients of this authorization is not specified here but rather by the PaymentManager. - # set the expiry date based on the campaign deadline - expiry = campaign.deadline + timedelta( days=settings.PREAPPROVAL_PERIOD_AFTER_CAMPAIGN ) - - paymentReason = "Unglue.it Pledge for {0}".format(campaign.name) - t, url = p.authorize('USD', TARGET_TYPE_CAMPAIGN, preapproval_amount, expiry=expiry, campaign=campaign, list=None, user=user, - return_url=return_url, nevermind_url=nevermind_url, anonymous=anonymous, premium=premium, - paymentReason=paymentReason, ack_name=ack_name, ack_link=ack_link, ack_dedication=ack_dedication) - else: # embedded view -- which we're not actively using right now. - # embedded view triggerws instant payment: send to the partnering RH - receiver_list = [{'email':settings.PAYPAL_NONPROFIT_PARTNER_EMAIL, 'amount':preapproval_amount}] - - return_url = None - nevermind_url = None - - t, url = p.pledge('USD', TARGET_TYPE_CAMPAIGN, receiver_list, campaign=campaign, list=None, user=user, - return_url=return_url, nevermind_url=nevermind_url, anonymous=anonymous, premium=premium, - ack_name=ack_name, ack_link=ack_link, ack_dedication=ack_dedication) - - if url: - logger.info("PledgeView url: " + url) - return HttpResponseRedirect(url) - else: - logger.error("Attempt to produce transaction id {0} failed".format(t.id)) - return HttpResponse("Our attempt to enable your transaction failed. We have logged this error.") - -class PledgeModifyView(FormView): - """ - A view to handle request to change an existing pledge - """ - - template_name="pledge.html" - form_class = CampaignPledgeForm - embedded = False - - def get_context_data(self, **kwargs): - - context = super(PledgeModifyView, self).get_context_data(**kwargs) - - # the following should be true since PledgeModifyView.as_view is wrapped in login_required - assert self.request.user.is_authenticated() - user = self.request.user - - work = get_object_or_404(models.Work, id=self.kwargs["work_id"]) - - try: - campaign = work.last_campaign() - premiums = campaign.custom_premiums() | models.Premium.objects.filter(id=150) - - # which combination of campaign and transaction status required? - # Campaign must be ACTIVE - assert campaign.status == 'ACTIVE' - - transactions = campaign.transactions().filter(user=user, status=TRANSACTION_STATUS_ACTIVE) - assert transactions.count() == 1 - transaction = transactions[0] - assert transaction.type == PAYMENT_TYPE_AUTHORIZATION and transaction.status == TRANSACTION_STATUS_ACTIVE - - except Exception, e: - raise e - - # what stuff do we need to pull out to populate form? - # preapproval_amount, premium_id (which we don't have stored yet) - if transaction.premium is not None: - premium_id = transaction.premium.id - premium_description = transaction.premium.description - else: - premium_id = None - premium_description = None - - # is there a Transaction for an ACTIVE campaign for this - # should make sure Transaction is modifiable. - - preapproval_amount = transaction.amount - data = {'preapproval_amount':preapproval_amount, 'premium_id':premium_id} - - # initialize form with the current state of the transaction if the current values empty - form = kwargs['form'] - - if not(form.is_bound): - form_class = self.get_form_class() - form = form_class(initial=data) - - context.update({ - 'work':work, - 'campaign':campaign, - 'premiums':premiums, - 'form':form, - 'preapproval_amount':preapproval_amount, - 'premium_id':premium_id, - 'premium_description': premium_description, - 'faqmenu': 'modify', - 'tid': transaction.id, - 'payment_processor':settings.PAYMENT_PROCESSOR, - 'transaction': transaction, - }) - return context - - - def form_invalid(self, form): - logger.info("form.non_field_errors: {0}".format(form.non_field_errors())) - response = self.render_to_response(self.get_context_data(form=form)) - return response - - def form_valid(self, form): - - # What are the situations we need to deal with? - # 2 main situations: if the new amount is less than max_amount, no need to go out to PayPal again - # if new amount is greater than max_amount...need to go out and get new approval. - # to start with, we can use the standard pledge_complete, pledge_cancel machinery - # might have to modify the pledge_complete, pledge_cancel because the messages are going to be - # different because we're modifying a pledge rather than a new one. - - work_id = self.kwargs["work_id"] - preapproval_amount = form.cleaned_data["preapproval_amount"] - ack_name = form.cleaned_data["ack_name"] - ack_link = form.cleaned_data["ack_link"] - ack_dedication = form.cleaned_data["ack_dedication"] - anonymous = form.cleaned_data["anonymous"] - - assert self.request.user.is_authenticated() - user = self.request.user - - # right now, if there is a non-zero pledge amount, go with that. otherwise, do the pre_approval - campaign = models.Work.objects.get(id=int(work_id)).last_campaign() - assert campaign.status == 'ACTIVE' - - premium_id = form.cleaned_data["premium_id"] - # confirm that the premium_id is a valid one for the campaign in question - try: - premium = models.Premium.objects.get(id=premium_id) - if not (premium.campaign is None or premium.campaign == campaign): - premium = None - except models.Premium.DoesNotExist, e: - premium = None - - transactions = campaign.transactions().filter(user=user, status=TRANSACTION_STATUS_ACTIVE) - assert transactions.count() == 1 - transaction = transactions[0] - assert transaction.type == PAYMENT_TYPE_AUTHORIZATION and transaction.status == TRANSACTION_STATUS_ACTIVE - - p = PaymentManager(embedded=self.embedded) - paymentReason = "Unglue.it Pledge for {0}".format(campaign.name) - status, url = p.modify_transaction(transaction=transaction, amount=preapproval_amount, premium=premium, - paymentReason=paymentReason, ack_name=ack_name, ack_link=ack_link, - ack_dedication=ack_dedication, anonymous=anonymous) - - logger.info("status: {0}, url:{1}".format(status, url)) - - if status and url is not None: - logger.info("PledgeModifyView paypal: " + url) - return HttpResponseRedirect(url) - elif status and url is None: - # let's use the pledge_complete template for now and maybe look into customizing it. - return HttpResponseRedirect("{0}?tid={1}".format(reverse('pledge_complete'), transaction.id)) - else: - return HttpResponse("No modification made") - + t, url = p.process_transaction('USD', form.cleaned_data["preapproval_amount"], + host = None, + campaign=self.campaign, + user=self.request.user, + premium=premium, + paymentReason="Unglue.it Pledge for {0}".format(self.campaign.name), + ack_name=form.cleaned_data["ack_name"], + ack_dedication=form.cleaned_data["ack_dedication"], + anonymous=form.cleaned_data["anonymous"], + ) + if url: + logger.info("PledgeView url: " + url) + return HttpResponseRedirect(url) + else: + logger.error("Attempt to produce transaction id {0} failed".format(t.id)) + return HttpResponse("Our attempt to enable your transaction failed. We have logged this error.") class PledgeRechargeView(TemplateView): @@ -869,7 +731,7 @@ class PledgeRechargeView(TemplateView): context = super(PledgeRechargeView, self).get_context_data(**kwargs) - # the following should be true since PledgeModifyView.as_view is wrapped in login_required + # the following should be true since PledgeView.as_view is wrapped in login_required assert self.request.user.is_authenticated() user = self.request.user @@ -888,18 +750,15 @@ class PledgeRechargeView(TemplateView): if transaction is not None: # the recipients of this authorization is not specified here but rather by the PaymentManager. - # set the expiry date based on the campaign deadline - expiry = campaign.deadline + timedelta( days=settings.PREAPPROVAL_PERIOD_AFTER_CAMPAIGN ) ack_name = transaction.ack_name - ack_link = transaction.ack_link ack_dedication = transaction.ack_dedication paymentReason = "Unglue.it Recharge for {0}".format(campaign.name) - p = PaymentManager(embedded=False) - t, url = p.authorize('USD', TARGET_TYPE_CAMPAIGN, transaction.amount, expiry=expiry, campaign=campaign, list=None, user=user, - return_url=return_url, nevermind_url=nevermind_url, anonymous=transaction.anonymous, premium=transaction.premium, - paymentReason=paymentReason, ack_name=ack_name, ack_link=ack_link, ack_dedication=ack_dedication) + p = PaymentManager() + t, url = p.authorize('USD', transaction.amount, campaign=campaign, list=None, user=user, + return_url=return_url, anonymous=transaction.anonymous, premium=transaction.premium, + paymentReason=paymentReason, ack_name=ack_name, ack_dedication=ack_dedication) logger.info("Recharge url: {0}".format(url)) else: url = None @@ -1090,73 +949,11 @@ class PledgeCancelView(FormView): except Exception, e: logger.error("Exception from attempt to cancel pledge for campaign id {0} for username {1}: {2}".format(campaign_id, user.username, e)) return HttpResponse("Sorry, something went wrong in canceling your campaign pledge. We have logged this error.") - - -class PledgeNeverMindView(TemplateView): - """A callback for PayPal to tell unglue.it that a payment transaction has been canceled by the user""" - template_name="pledge_nevermind.html" - - def get_context_data(self): - context = super(PledgeNeverMindView, self).get_context_data() - - if self.request.user.is_authenticated(): - user = self.request.user - else: - user = None - - # pull out the transaction id and try to get the corresponding Transaction - transaction_id = self.request.REQUEST.get("tid") - transaction = Transaction.objects.get(id=transaction_id) - - # work and campaign in question - try: - campaign = transaction.campaign - work = campaign.work - except Exception, e: - campaign = None - work = None - - # we need to check whether the user tied to the transaction is indeed the authenticated user. - - correct_user = False - try: - if user.id == transaction.user.id: - correct_user = True - except Exception, e: - pass - - # check that the user had not already approved the transaction - # do we need to first run PreapprovalDetails to check on the status - - # is it of type=PAYMENT_TYPE_AUTHORIZATION and status is NONE or ACTIVE (but approved is false) - - if transaction.type == PAYMENT_TYPE_AUTHORIZATION: - correct_transaction_type = True - else: - correct_transaction_type = False - - # status? - - # give the user an opportunity to approved the transaction again - # provide a URL to click on. - # https://www.sandbox.paypal.com/?cmd=_ap-preapproval&preapprovalkey=PA-6JV656290V840615H - try_again_url = '%s?cmd=_ap-preapproval&preapprovalkey=%s' % (settings.PAYPAL_PAYMENT_HOST, transaction.preapproval_key) - - context["transaction"] = transaction - context["correct_user"] = correct_user - context["correct_transaction_type"] = correct_transaction_type - context["try_again_url"] = try_again_url - context["work"] = work - context["campaign"] = campaign - context["faqmenu"] = "cancel" - - return context class DonateView(FormView): template_name="donate.html" form_class = DonateForm - embedded = False #def get_context_data(self, **kwargs): # context = super(DonateView, self).get_context_data(**kwargs) @@ -1173,7 +970,7 @@ class DonateView(FormView): # right now, if there is a non-zero pledge amount, go with that. otherwise, do the pre_approval campaign = None - p = PaymentManager(embedded=self.embedded) + p = PaymentManager() # we should force login at this point -- or if no account, account creation, login, and return to this spot if self.request.user.is_authenticated(): @@ -1187,8 +984,8 @@ class DonateView(FormView): #redirect the page back to campaign page on success return_url = self.request.build_absolute_uri(reverse('donate')) - t, url = p.pledge('USD', TARGET_TYPE_DONATION, receiver_list, campaign=campaign, list=None, user=user, - return_url=return_url, anonymous=anonymous, ack_name=ack_name, ack_link=ack_link, + t, url = p.pledge('USD', receiver_list, campaign=campaign, list=None, user=user, + return_url=return_url, anonymous=anonymous, ack_name=ack_name, ack_dedication=ack_dedication) if url: diff --git a/payment/credit.py b/payment/credit.py new file mode 100644 index 00000000..c5987d5b --- /dev/null +++ b/payment/credit.py @@ -0,0 +1,56 @@ +from datetime import timedelta + +from django.contrib.auth.models import User +from django.conf import settings + +from regluit.payment.parameters import * +from regluit.utils.localdatetime import now +from regluit.payment.baseprocessor import BasePaymentRequest + + +def pledge_transaction(t,user,amount): + """commit from a 's credit to a specified transaction """ + + if t.amount and t.host == PAYMENT_HOST_CREDIT: + #changing the pledge_transaction + user.credit.add_to_pledged(amount-t.amount) + else: + user.credit.add_to_pledged(amount) + t.amount=amount + t.max_amount=amount + t.host = PAYMENT_HOST_CREDIT + t.type = PAYMENT_TYPE_AUTHORIZATION + t.status=TRANSACTION_STATUS_ACTIVE + t.approved=True + now_val = now() + t.date_authorized = now_val + t.date_expired = now_val + timedelta( days=settings.PREAPPROVAL_PERIOD ) + + t.save() + + +class CancelPreapproval(BasePaymentRequest): + ''' + Cancels an exisiting token. + ''' + + def __init__(self, transaction): + self.transaction = transaction + if transaction.user.credit.add_to_pledged(-transaction.amount): + #success + transaction.status=TRANSACTION_STATUS_CANCELED + transaction.save() + else: + self.errorMessage="couldn't cancel the transaction" + self.status = 'Credit Cancel Failure' + +class PreapprovalDetails(BasePaymentRequest): + status = None + approved = None + currency = None + amount = None + def __init__(self, transaction): + self.status = transaction.status + self.approved = transaction.approved + self.currency = transaction.currency + self.amount = transaction.amount diff --git a/payment/manager.py b/payment/manager.py index 27e742a5..017bfe2a 100644 --- a/payment/manager.py +++ b/payment/manager.py @@ -5,6 +5,9 @@ from django.core.urlresolvers import reverse from django.conf import settings from regluit.payment.parameters import * from regluit.payment.signals import transaction_charged, pledge_modified, pledge_created +from regluit.payment import credit + +from regluit.payment.baseprocessor import Pay, Finish, Preapproval, ProcessIPN, CancelPreapproval, PaymentDetails, PreapprovalDetails, RefundPayment import uuid import traceback @@ -14,6 +17,8 @@ import logging from decimal import Decimal as D from xml.dom import minidom import urllib, urlparse +from datetime import timedelta + from django.conf import settings @@ -531,58 +536,28 @@ class PaymentManager( object ): logger.info("Cancel Transaction " + str(transaction.id) + " Failed with error: " + p.error_string()) return False - def authorize(self, currency, target, amount, expiry=None, campaign=None, list=None, user=None, - return_url=None, nevermind_url=None, anonymous=False, premium=None, - paymentReason="unglue.it Pledge", ack_name=None, ack_link=None, ack_dedication=None, - modification=False): + def authorize(self, transaction, expiry= None, return_url=None, paymentReason="unglue.it Pledge", modification=False): ''' authorize authorizes a set amount of money to be collected at a later date - currency: a 3-letter paypal currency code, i.e. USD - target: a defined target type, i.e. TARGET_TYPE_CAMPAIGN, TARGET_TYPE_LIST, TARGET_TYPE_NONE - amount: the amount to authorize - campaign: optional campaign object(to be set with TARGET_TYPE_CAMPAIGN) - list: optional list object(to be set with TARGET_TYPE_LIST) - user: optional user object return_url: url to redirect supporter to after a successful PayPal transaction - nevermind_url: url to send supporter to if support hits cancel while in middle of PayPal transaction - anonymous: whether this pledge is anonymous - premium: the premium selected by the supporter for this transaction paymentReason: a memo line that will show up in the Payer's Amazon (and Paypal?) account modification: whether this authorize call is part of a modification of an existing pledge - ack_name, ack_link, ack_dedication: how the user will be credited in the unglued ebook, if applicable return value: a tuple of the new transaction object and a re-direct url. If the process fails, the redirect url will be None - ''' - - t = Transaction.objects.create(amount=amount, - max_amount=amount, - type=PAYMENT_TYPE_AUTHORIZATION, - execution = EXECUTE_TYPE_CHAINED_DELAYED, - target=target, - currency=currency, - status='NONE', - campaign=campaign, - list=list, - user=user, - anonymous=anonymous, - premium=premium, - ack_name=ack_name, - ack_link=ack_link, - ack_dedication=ack_dedication - ) + ''' - # we might want to not allow for a return_url or nevermind_url to be passed in but calculated + if host==None: + #TODO send user to select a payment processor + pass + + # we might want to not allow for a return_url to be passed in but calculated # here because we have immediate access to the Transaction object. - if nevermind_url is None: - nevermind_path = "{0}?{1}".format(reverse('pledge_nevermind'), - urllib.urlencode({'tid':t.id})) - nevermind_url = urlparse.urljoin(settings.BASE_URL, nevermind_path) if return_url is None: return_path = "{0}?{1}".format(reverse('pledge_complete'), @@ -590,7 +565,7 @@ class PaymentManager( object ): return_url = urlparse.urljoin(settings.BASE_URL, return_path) method = getattr(t.get_payment_class(), "Preapproval") - p = method(t, amount, expiry, return_url=return_url, nevermind_url=nevermind_url, paymentReason=paymentReason) + p = method(transaction, transaction.max_amount, expiry, return_url=return_url, paymentReason=paymentReason) # Create a response for this envelope = p.envelope() @@ -600,11 +575,11 @@ class PaymentManager( object ): correlation_id = p.correlation_id(), timestamp = p.timestamp(), info = p.raw_response, - transaction=t) + transaction=transaction) if p.success() and not p.error(): - t.preapproval_key = p.key() - t.save() + transaction.preapproval_key = p.key() + transaction.save() url = p.next_url() @@ -624,16 +599,69 @@ class PaymentManager( object ): # send the notice here for now # this is actually premature since we're only about to send the user off to the payment system to # authorize a charge - pledge_created.send(sender=self, transaction=t) + pledge_created.send(sender=self, transaction=transaction) - return t, url + return transaction, url else: - t.error = p.error_string() - t.save() + transaction.error = p.error_string() + transaction.save() logger.info("Authorize Error: " + p.error_string()) + return transaction, None + + def process_transaction(self, currency, amount, host=None, campaign=None, user=None, + return_url=None, anonymous=False, premium=None, + paymentReason="unglue.it Pledge", ack_name=None, ack_dedication=None, + modification=False): + ''' + process + + saves and processes a proposed transaction; decides if the transaction should be processed immediately. + + currency: a 3-letter currency code, i.e. USD + amount: the amount to authorize + host: the name of the processing module; if none, send user back to decide! + campaign: required campaign object + user: optional user object + return_url: url to redirect supporter to after a successful transaction + anonymous: whether this pledge is anonymous + premium: the premium selected by the supporter for this transaction + paymentReason: a memo line that will show up in the Payer's Amazon (and Paypal?) account + modification: whether this authorize call is part of a modification of an existing pledge + ack_name, ack_dedication: how the user will be credited in the unglued ebook, if applicable + + return value: a tuple of the new transaction object and a re-direct url. If the process fails, + the redirect url will be None + ''' + # set the expiry date based on the campaign deadline + expiry = campaign.deadline + timedelta( days=settings.PREAPPROVAL_PERIOD_AFTER_CAMPAIGN ) + + t = Transaction.objects.create(amount=0, + max_amount=amount, + currency=currency, + status='NONE', + campaign=campaign, + user=user, + anonymous=anonymous, + premium=premium, + ack_name=ack_name, + ack_dedication=ack_dedication + ) + + # does user have enough credit to pledge now? + if user.credit.available >= amount : + # YES! + credit.pledge_transaction(t,user,amount) + return_path = "{0}?{1}".format(reverse('pledge_complete'), + urllib.urlencode({'tid':t.id})) + return_url = urlparse.urljoin(settings.BASE_URL, return_path) + pledge_created.send(sender=self, transaction=t) + return t, return_url + else: + #TODO send user to choose payment path return t, None + def cancel_related_transaction(self, transaction, status=TRANSACTION_STATUS_ACTIVE, campaign=None): ''' @@ -677,26 +705,29 @@ class PaymentManager( object ): return canceled - def modify_transaction(self, transaction, amount, expiry=None, anonymous=None, premium=None, + def modify_transaction(self, transaction, amount, expiry=None, premium=None, return_url=None, nevermind_url=None, paymentReason=None, - ack_name=None, ack_link=None, ack_dedication=None): + ack_name=None, ack_dedication=None, anonymous=None): ''' modify - Modifies a transaction. The only type of modification allowed is to the amount and expiration date + Modifies a transaction. + 2 main situations: if the new amount is less than max_amount, no need to go out to PayPal again + if new amount is greater than max_amount...need to go out and get new approval. + to start with, we can use the standard pledge_complete, pledge_cancel machinery + might have to modify the pledge_complete, pledge_cancel because the messages are going to be + different because we're modifying a pledge rather than a new one. amount: the new amount expiry: the new expiration date, or if none the current expiration date will be used anonymous: new anonymous value; if None, then keep old value premium: new premium selected; if None, then keep old value return_url: the return URL after the preapproval(if needed) - nevermind_url: the cancel url after the preapproval(if needed) paymentReason: a memo line that will show up in the Payer's Amazon (and Paypal?) account return value: True if successful, False otherwise. An optional second parameter for the forward URL if a new authorhization is needed ''' - # Can only modify the amount of a preapproval for now if transaction.type != PAYMENT_TYPE_AUTHORIZATION: logger.info("Error, attempt to modify an invalid transaction type") return False, None @@ -707,34 +738,50 @@ class PaymentManager( object ): logger.info("Error, attempt to modify a transaction that is not active") return False, None - # if any of expiry, anonymous, or premium is None, use the existing value - if expiry is None: - expiry = transaction.date_expired - if anonymous is None: - anonymous = transaction.anonymous - if premium is None: - premium = transaction.premium - - if amount > transaction.max_amount or expiry != transaction.date_expired: + if transaction.host == PAYMENT_HOST_CREDIT: + # does user have enough credit to pledge now? + if transaction.user.credit.available >= amount-transaction.amount : + # YES! + transaction.anonymous = anonymous + transaction.premium = premium + transaction.ack_name = ack_name + transaction.ack_dedication = ack_dedication + credit.pledge_transaction(transaction,transaction.user,amount) + return_path = "{0}?{1}".format(reverse('pledge_complete'), + urllib.urlencode({'tid':transaction.id})) + return_url = urlparse.urljoin(settings.BASE_URL, return_path) - # Start a new authorization for the new amount + logger.info("Updated amount of transaction to %f" % amount) + pledge_modified.send(sender=self, transaction=transaction,up_or_down="decreased" if amount-transaction.amount<0 else "increased") + return transaction, return_url + else: + #TODO send user to choose payment path + return t, None + elif amount > transaction.max_amount or expiry != transaction.date_expired: + + # set the expiry date based on the campaign deadline + expiry = transaction.campaign.deadline + timedelta( days=settings.PREAPPROVAL_PERIOD_AFTER_CAMPAIGN ) + + # Start a new transaction for the new amount + t = Transaction.objects.create(amount=amount, + max_amount=amount, + host=transaction.host, + currency=currency, + status=TRANSACTION_STATUS_CREATED, + campaign=transaction.campaign, + user=transaction.user, + anonymous=anonymous if anonymous!=None else transaction.anonymous, + premium=premium if premium != None else transaction.premium, + ack_name=ack_name, + ack_dedication=ack_dedication + ) - t, url = self.authorize(transaction.currency, - transaction.target, - amount, - expiry, - transaction.campaign, - transaction.list, - transaction.user, - return_url, - nevermind_url, - anonymous, - premium, - paymentReason, - ack_name, - ack_link, - ack_dedication, - True) + t, url = self.authorize(transaction, + expiry=expiry if expiry else transaction.date_expired, + return_url=return_url, + paymentReason=paymentReason, + modification=True + ) if t and url: # Need to re-direct to approve the transaction @@ -762,7 +809,6 @@ class PaymentManager( object ): transaction.anonymous = anonymous transaction.premium = premium transaction.ack_name = ack_name - transaction.ack_link = ack_link transaction.ack_dedication = ack_dedication transaction.save() @@ -820,16 +866,15 @@ class PaymentManager( object ): logger.info("Refund Transaction " + str(transaction.id) + " Failed with error: " + p.error_string()) return False - def pledge(self, currency, target, receiver_list, campaign=None, list=None, user=None, + def pledge(self, currency, receiver_list, campaign=None, user=None, return_url=None, nevermind_url=None, anonymous=False, premium=None, ack_name=None, - ack_link=None, ack_dedication=None): + ack_dedication=None): ''' pledge Performs an instant payment currency: a 3-letter paypal currency code, i.e. USD - target: a defined target type, i.e. TARGET_TYPE_CAMPAIGN, TARGET_TYPE_LIST, TARGET_TYPE_NONE receiver_list: a list of receivers for the transaction, in this format: [ @@ -837,8 +882,7 @@ class PaymentManager( object ): {'email':'email-2', 'amount':amount2} ] - campaign: optional campaign object(to be set with TARGET_TYPE_CAMPAIGN) - list: optional list object(to be set with TARGET_TYPE_LIST) + campaign: required campaign object user: optional user object return_url: url to redirect supporter to after a successful PayPal transaction nevermind_url: url to send supporter to if support hits cancel while in middle of PayPal transaction @@ -863,13 +907,11 @@ class PaymentManager( object ): currency=currency, status='NONE', campaign=campaign, - list=list, user=user, date_payment=now(), anonymous=anonymous, premium=premium, ack_name=ack_name, - ack_link=ack_link, ack_dedication=ack_dedication ) diff --git a/payment/migrations/0008_auto__add_credit__add_creditlog__del_field_transaction_target__del_fie.py b/payment/migrations/0008_auto__add_credit__add_creditlog__del_field_transaction_target__del_fie.py new file mode 100644 index 00000000..664f4ec6 --- /dev/null +++ b/payment/migrations/0008_auto__add_credit__add_creditlog__del_field_transaction_target__del_fie.py @@ -0,0 +1,224 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'Credit' + db.create_table('payment_credit', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='credit', unique=True, to=orm['auth.User'])), + ('balance', self.gf('django.db.models.fields.DecimalField')(default='0.00', max_digits=14, decimal_places=2)), + ('pledged', self.gf('django.db.models.fields.DecimalField')(default='0.00', max_digits=14, decimal_places=2)), + ('last_activity', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + )) + db.send_create_signal('payment', ['Credit']) + + # Adding model 'CreditLog' + db.create_table('payment_creditlog', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True)), + ('amount', self.gf('django.db.models.fields.DecimalField')(default='0.00', max_digits=14, decimal_places=2)), + ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + ('action', self.gf('django.db.models.fields.CharField')(max_length=16)), + )) + db.send_create_signal('payment', ['CreditLog']) + + # Deleting field 'Transaction.target' + db.delete_column('payment_transaction', 'target') + + # Deleting field 'Transaction.list' + db.delete_column('payment_transaction', 'list_id') + + # Adding field 'Transaction.ack_name' + db.add_column('payment_transaction', 'ack_name', self.gf('django.db.models.fields.CharField')(max_length=64, null=True), keep_default=False) + + # Adding field 'Transaction.ack_dedication' + db.add_column('payment_transaction', 'ack_dedication', self.gf('django.db.models.fields.CharField')(max_length=140, null=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting model 'Credit' + db.delete_table('payment_credit') + + # Deleting model 'CreditLog' + db.delete_table('payment_creditlog') + + # Adding field 'Transaction.target' + db.add_column('payment_transaction', 'target', self.gf('django.db.models.fields.IntegerField')(default=0), keep_default=False) + + # Adding field 'Transaction.list' + db.add_column('payment_transaction', 'list', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['core.Wishlist'], null=True), keep_default=False) + + # Deleting field 'Transaction.ack_name' + db.delete_column('payment_transaction', 'ack_name') + + # Deleting field 'Transaction.ack_dedication' + db.delete_column('payment_transaction', 'ack_dedication') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 8, 31, 2, 10, 24, 467332)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 8, 31, 2, 10, 24, 467190)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'core.campaign': { + 'Meta': {'object_name': 'Campaign'}, + 'activated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'amazon_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deadline': ('django.db.models.fields.DateTimeField', [], {}), + 'description': ('ckeditor.fields.RichTextField', [], {'null': 'True'}), + 'details': ('ckeditor.fields.RichTextField', [], {'null': 'True', 'blank': 'True'}), + 'edition': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'null': 'True', 'to': "orm['core.Edition']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'left': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}), + 'license': ('django.db.models.fields.CharField', [], {'default': "'CC BY-NC-ND'", 'max_length': '255'}), + 'managers': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'campaigns'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '500', 'null': 'True'}), + 'paypal_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'INITIALIZED'", 'max_length': '15', 'null': 'True'}), + 'target': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'to': "orm['core.Work']"}) + }, + 'core.edition': { + 'Meta': {'object_name': 'Edition'}, + 'cover_image': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'public_domain': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'publication_date': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'publisher': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'editions'", 'null': 'True', 'to': "orm['core.Work']"}) + }, + 'core.premium': { + 'Meta': {'object_name': 'Premium'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '0'}), + 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'premiums'", 'null': 'True', 'to': "orm['core.Campaign']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'limit': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '2'}) + }, + 'core.work': { + 'Meta': {'ordering': "['title']", 'object_name': 'Work'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '2'}), + 'num_wishes': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'openlibrary_lookup': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}) + }, + 'payment.credit': { + 'Meta': {'object_name': 'Credit'}, + 'balance': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_activity': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'pledged': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'credit'", 'unique': 'True', 'to': "orm['auth.User']"}) + }, + 'payment.creditlog': { + 'Meta': {'object_name': 'CreditLog'}, + 'action': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'payment.paymentresponse': { + 'Meta': {'object_name': 'PaymentResponse'}, + 'api': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'correlation_id': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'timestamp': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'transaction': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['payment.Transaction']"}) + }, + 'payment.receiver': { + 'Meta': {'object_name': 'Receiver'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'currency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'email': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'local_status': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'primary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'transaction': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['payment.Transaction']"}), + 'txn_id': ('django.db.models.fields.CharField', [], {'max_length': '64'}) + }, + 'payment.transaction': { + 'Meta': {'object_name': 'Transaction'}, + 'ack_dedication': ('django.db.models.fields.CharField', [], {'max_length': '140', 'null': 'True'}), + 'ack_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'approved': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Campaign']", 'null': 'True'}), + 'currency': ('django.db.models.fields.CharField', [], {'default': "'USD'", 'max_length': '10', 'null': 'True'}), + 'date_authorized': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'date_executed': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_expired': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'date_payment': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'error': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True'}), + 'execution': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'host': ('django.db.models.fields.CharField', [], {'default': "'none'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'local_status': ('django.db.models.fields.CharField', [], {'default': "'NONE'", 'max_length': '32', 'null': 'True'}), + 'max_amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'pay_key': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'preapproval_key': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'premium': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Premium']", 'null': 'True'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'receipt': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True'}), + 'secret': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'None'", 'max_length': '32'}), + 'type': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + } + } + + complete_apps = ['payment'] diff --git a/payment/models.py b/payment/models.py index e339d567..1016f5e7 100644 --- a/payment/models.py +++ b/payment/models.py @@ -4,8 +4,11 @@ from django.conf import settings from regluit.core.models import Campaign, Wishlist, Premium from regluit.payment.parameters import * from regluit.payment.signals import credit_balance_added -from decimal import Decimal +from decimal import Decimal, NaN import uuid +import urllib +import logging +logger = logging.getLogger(__name__) class Transaction(models.Model): @@ -14,16 +17,13 @@ class Transaction(models.Model): type = models.IntegerField(default=PAYMENT_TYPE_NONE, null=False) # host: the payment processor. Named after the payment module that hosts the payment processing functions - host = models.CharField(default=settings.PAYMENT_PROCESSOR, max_length=32, null=False) - - # target: e.g, TARGET_TYPE_CAMPAIGN, TARGET_TYPE_LIST -- defined in parameters.py - target = models.IntegerField(default=TARGET_TYPE_NONE, null=False) - + host = models.CharField(default=PAYMENT_HOST_NONE, max_length=32, null=False) + #execution: e.g. EXECUTE_TYPE_CHAINED_INSTANT, EXECUTE_TYPE_CHAINED_DELAYED, EXECUTE_TYPE_PARALLEL execution = models.IntegerField(default=EXECUTE_TYPE_NONE, null=False) # status: general status constants defined in parameters.py - status = models.CharField(max_length=32, default='None', null=False) + status = models.CharField(max_length=32, default=TRANSACTION_STATUS_NONE, null=False) # local_status: status code specific to the payment processor local_status = models.CharField(max_length=32, default='NONE', null=True) @@ -75,14 +75,14 @@ class Transaction(models.Model): # how to acknowledge the user on the supporter page of the campaign ebook ack_name = models.CharField(max_length=64, null=True) - ack_link = models.URLField(null=True) ack_dedication = models.CharField(max_length=140, null=True) # whether the user wants to be not listed publicly anonymous = models.BooleanField(null=False) - - # list: makes allowance for pledging against a Wishlist: not currently in use - list = models.ForeignKey(Wishlist, null=True) + + @property + def ack_link(self): + return 'https://unglue.it/supporter/%s'%urllib.urlencode(self.user.username) def save(self, *args, **kwargs): if not self.secret: @@ -149,10 +149,17 @@ class Receiver(models.Model): def __unicode__(self): return u"Receiver -- email: {0} status: {1} transaction: {2}".format(self.email, self.status, unicode(self.transaction)) +class CreditLog(models.Model): + # a write only record of Donation Credit Transactions + user = models.ForeignKey(User, null=True) + amount = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99 + timestamp = models.DateTimeField(auto_now=True) + action = models.CharField(max_length=16) + class Credit(models.Model): user = models.OneToOneField(User, related_name='credit') - balance = models.IntegerField(default=0) - pledged = models.IntegerField(default=0) + balance = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99 + pledged = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99 last_activity = models.DateTimeField(auto_now=True) @property @@ -165,17 +172,29 @@ class Credit(models.Model): else: self.balance = self.balance + num_credits self.save() - credit_balance_added.send(sender=self, amount=num_credits) + try: # bad things can happen here if you don't return True + CreditLog(user = self.user, amount = num_credits, action="add_to_balance").save() + except: + logger.exception("failed to log add_to_balance of %s", num_credits) + try: + credit_balance_added.send(sender=self, amount=num_credits) + except: + logger.exception("credit_balance_added failed of %s", num_credits) return True def add_to_pledged(self, num_credits): - if not isinstance( num_credits, int): + num_credits=Decimal(num_credits) + if num_credits is NaN: return False if self.balance - self.pledged < num_credits : return False else: self.pledged=self.pledged + num_credits self.save() + try: # bad things can happen here if you don't return True + CreditLog(user = self.user, amount = num_credits, action="add_to_pledged").save() + except: + logger.exception("failed to log add_to_pledged of %s", num_credits) return True def use_pledge(self, num_credits): @@ -187,6 +206,10 @@ class Credit(models.Model): self.pledged=self.pledged - num_credits self.balance = self.balance - num_credits self.save() + try: + CreditLog(user = self.user, amount = - num_credits, action="use_pledge").save() + except: + logger.exception("failed to log use_pledge of %s", num_credits) return True def transfer_to(self, receiver, num_credits): diff --git a/payment/parameters.py b/payment/parameters.py index 872f07a6..829afa3a 100644 --- a/payment/parameters.py +++ b/payment/parameters.py @@ -5,17 +5,15 @@ PAYMENT_TYPE_AUTHORIZATION = 2 PAYMENT_HOST_NONE = "none" PAYMENT_HOST_PAYPAL = "paypal" PAYMENT_HOST_AMAZON = "amazon" +PAYMENT_HOST_TEST = "test" +PAYMENT_HOST_CREDIT = "credit" +PAYMENT_HOST_STRIPE = "stripe" EXECUTE_TYPE_NONE = 0 EXECUTE_TYPE_CHAINED_INSTANT = 1 EXECUTE_TYPE_CHAINED_DELAYED = 2 EXECUTE_TYPE_PARALLEL = 3 -TARGET_TYPE_NONE = 0 -TARGET_TYPE_CAMPAIGN = 1 -TARGET_TYPE_LIST = 2 -TARGET_TYPE_DONATION = 3 - # The default status for a transaction that is newly created TRANSACTION_STATUS_NONE = 'None' @@ -47,6 +45,3 @@ TRANSACTION_STATUS_REFUNDED = 'Refunded' # The transaction was refused/denied TRANSACTION_STATUS_FAILED = 'Failed' -# these two following parameters are probably extraneous since I think we will compute dynamically where to return each time. -COMPLETE_URL = '/paymentcomplete' -NEVERMIND_URL = '/paymentnevermind' diff --git a/payment/tests.py b/payment/tests.py index b86e3eed..0edd5d1f 100644 --- a/payment/tests.py +++ b/payment/tests.py @@ -190,7 +190,7 @@ class PledgeTest(TestCase): # Note, set this to 1-5 different receivers with absolute amounts for each receiver_list = [{'email':settings.PAYPAL_GLUEJAR_EMAIL, 'amount':20.00}] - t, url = p.pledge('USD', TARGET_TYPE_NONE, receiver_list, campaign=None, list=None, user=None) + t, url = p.pledge('USD', receiver_list, campaign=None, list=None, user=None) self.validateRedirect(t, url, 1) @@ -220,7 +220,7 @@ class PledgeTest(TestCase): receiver_list = [{'email':settings.PAYPAL_GLUEJAR_EMAIL, 'amount':20.00}, {'email':settings.PAYPAL_TEST_RH_EMAIL, 'amount':10.00}] - t, url = p.pledge('USD', TARGET_TYPE_NONE, receiver_list, campaign=None, list=None, user=None) + t, url = p.pledge('USD', receiver_list, campaign=None, list=None, user=None) self.validateRedirect(t, url, 2) @@ -244,7 +244,7 @@ class PledgeTest(TestCase): # Note, set this to 1-5 different receivers with absolute amounts for each receiver_list = [{'email':settings.PAYPAL_GLUEJAR_EMAIL, 'amount':50000.00}] - t, url = p.pledge('USD', TARGET_TYPE_NONE, receiver_list, campaign=None, list=None, user=None) + t, url = p.pledge('USD', receiver_list, campaign=None, list=None, user=None) self.validateRedirect(t, url, 1) @@ -284,7 +284,7 @@ class AuthorizeTest(TestCase): # Note, set this to 1-5 different receivers with absolute amounts for each - t, url = p.authorize('USD', TARGET_TYPE_NONE, 100.0, campaign=None, list=None, user=None) + t, url = p.authorize('USD', 100.0, campaign=None, list=None, user=None) self.validateRedirect(t, url) @@ -388,7 +388,7 @@ class BasicGuiTest(TestCase): def suite(): #testcases = [PledgeTest, AuthorizeTest, TransactionTest] - testcases = [TransactionTest] + testcases = [TransactionTest, CreditTest] suites = unittest.TestSuite([unittest.TestLoader().loadTestsFromTestCase(testcase) for testcase in testcases]) return suites diff --git a/payment/urls.py b/payment/urls.py index d50a346e..c91c6dd4 100644 --- a/payment/urls.py +++ b/payment/urls.py @@ -17,8 +17,6 @@ if settings.DEBUG: url(r"^testexecute", "testExecute"), url(r"^testcancel", "testCancel"), url(r"^querycampaign", "queryCampaign"), - url(r"^runtests", "runTests"), - url(r"^paymentcomplete","paymentcomplete"), url(r"^checkstatus", "checkStatus"), url(r"^testfinish", "testFinish"), url(r"^testrefund", "testRefund"), diff --git a/payment/views.py b/payment/views.py index 33e5b384..b3bdb12d 100644 --- a/payment/views.py +++ b/payment/views.py @@ -13,7 +13,8 @@ from django.test.utils import setup_test_environment from django.template import RequestContext from unittest import TestResult -from regluit.payment.tests import PledgeTest, AuthorizeTest + + import uuid from decimal import Decimal as D @@ -112,12 +113,8 @@ def testAuthorize(request): receiver_list = [{'email': TEST_RECEIVERS[0], 'amount':20.00}, {'email': TEST_RECEIVERS[1], 'amount':10.00}] - if campaign_id: - campaign = Campaign.objects.get(id=int(campaign_id)) - t, url = p.authorize('USD', TARGET_TYPE_CAMPAIGN, amount, campaign=campaign, return_url=None, list=None, user=None) - - else: - t, url = p.authorize('USD', TARGET_TYPE_NONE, amount, campaign=None, return_url=None, list=None, user=None) + campaign = Campaign.objects.get(id=int(campaign_id)) + t, url = p.authorize('USD', amount, campaign=campaign, return_url=None, list=None, user=None) if url: logger.info("testAuthorize: " + url) @@ -248,12 +245,9 @@ def testPledge(request): else: receiver_list = [{'email':TEST_RECEIVERS[0], 'amount':78.90}, {'email':TEST_RECEIVERS[1], 'amount':34.56}] - if campaign_id: - campaign = Campaign.objects.get(id=int(campaign_id)) - t, url = p.pledge('USD', TARGET_TYPE_CAMPAIGN, receiver_list, campaign=campaign, list=None, user=user, return_url=None) + campaign = Campaign.objects.get(id=int(campaign_id)) + t, url = p.pledge('USD', receiver_list, campaign=campaign, list=None, user=user, return_url=None) - else: - t, url = p.pledge('USD', TARGET_TYPE_NONE, receiver_list, campaign=None, list=None, user=user, return_url=None) if url: logger.info("testPledge: " + url) @@ -264,33 +258,6 @@ def testPledge(request): logger.info("testPledge: Error " + str(t.error)) return HttpResponse(response) -def runTests(request): - - try: - # Setup the test environement. We need to run these tests on a live server - # so our code can receive IPN notifications from paypal - setup_test_environment() - result = TestResult() - - # Run the authorize test - test = AuthorizeTest('test_authorize') - test.run(result) - - # Run the pledge test - test = PledgeTest('test_pledge_single_receiver') - test.run(result) - - # Run the pledge failure test - test = PledgeTest('test_pledge_too_much') - test.run(result) - - output = "Tests Run: " + str(result.testsRun) + str(result.errors) + str(result.failures) - logger.info(output) - - return HttpResponse(output) - - except: - traceback.print_exc() @csrf_exempt def handleIPN(request, module): @@ -303,11 +270,6 @@ def handleIPN(request, module): return HttpResponse("ipn") -def paymentcomplete(request): - # pick up all get and post parameters and display - output = "payment complete" - output += request.method + "\n" + str(request.REQUEST.items()) - return HttpResponse(output) def checkStatus(request): # Check the status of all PAY transactions and flag any errors From 7afedcd0b6b89d80b67eadffc9148e8ea9823915 Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 31 Aug 2012 10:58:57 -0400 Subject: [PATCH 036/107] ack_link now considers anonymous [#16582483] --- payment/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payment/models.py b/payment/models.py index 1016f5e7..54015824 100644 --- a/payment/models.py +++ b/payment/models.py @@ -82,7 +82,7 @@ class Transaction(models.Model): @property def ack_link(self): - return 'https://unglue.it/supporter/%s'%urllib.urlencode(self.user.username) + return 'https://unglue.it/supporter/%s'%urllib.urlencode(self.user.username) if self.anonymous else '' def save(self, *args, **kwargs): if not self.secret: From c20e6299bb728d37f986248f848489d798236375 Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 31 Aug 2012 11:05:00 -0400 Subject: [PATCH 037/107] off to a great start this morning [#16582483] --- payment/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payment/models.py b/payment/models.py index 54015824..486b24fc 100644 --- a/payment/models.py +++ b/payment/models.py @@ -82,7 +82,7 @@ class Transaction(models.Model): @property def ack_link(self): - return 'https://unglue.it/supporter/%s'%urllib.urlencode(self.user.username) if self.anonymous else '' + return 'https://unglue.it/supporter/%s'%urllib.urlencode(self.user.username) if not self.anonymous else '' def save(self, *args, **kwargs): if not self.secret: From f2e9559a3ab0fb59872b2356bcec74ad32b0a60d Mon Sep 17 00:00:00 2001 From: Andromeda Yelton Date: Fri, 31 Aug 2012 13:41:16 -0400 Subject: [PATCH 038/107] beginning of a notification to RHs when someone wishlists their work --- core/models.py | 3 +- core/signals.py | 77 +++++-------------- .../notification/new_wisher/full.txt | 4 + .../notification/new_wisher/notice.html | 9 +++ .../notification/new_wisher/notice.txt | 3 + .../notification/new_wisher/short.txt | 1 + 6 files changed, 39 insertions(+), 58 deletions(-) create mode 100644 frontend/templates/notification/new_wisher/full.txt create mode 100644 frontend/templates/notification/new_wisher/notice.html create mode 100644 frontend/templates/notification/new_wisher/notice.txt create mode 100644 frontend/templates/notification/new_wisher/short.txt diff --git a/core/models.py b/core/models.py index a7b1faf3..db8924ce 100755 --- a/core/models.py +++ b/core/models.py @@ -790,7 +790,8 @@ class Wishlist(models.Model): w = Wishes.objects.get(wishlist=self,work=work) except: Wishes.objects.create(source=source,wishlist=self,work=work) - work.update_num_wishes() + work.update_num_wishes() + wishlist_added.send(sender=self, work=work, supporter=self.user) def remove_work(self, work): w = Wishes.objects.filter(wishlist=self, work=work) diff --git a/core/signals.py b/core/signals.py index 01f7a262..a3713eb5 100644 --- a/core/signals.py +++ b/core/signals.py @@ -96,6 +96,11 @@ def create_notice_types(app, created_models, verbosity, **kwargs): notification.create_notice_type("rights_holder_created", _("Agreement Accepted"), _("You have become a verified Unglue.it rights holder.")) notification.create_notice_type("rights_holder_claim_approved", _("Claim Accepted"), _("A claim you've entered has been accepted.")) notification.create_notice_type("wishlist_unsuccessful_amazon", _("Campaign shut down"), _("An ungluing campaign that you supported had to be shut down due to an Amazon Payments policy change.")) +<<<<<<< HEAD +======= + notification.create_notice_type("pledge_donation_credit", _("Donation Credit Balance"), _("You have a donation credit balance")) + notification.create_notice_type("new_wisher", _("New wisher"), _("Someone new has wished for a book that you're the rightsholder for")) +>>>>>>> 2da4ba6... starting a notification for RHs when their books are wished for signals.post_syncdb.connect(create_notice_types, sender=notification) @@ -212,60 +217,18 @@ def handle_wishlist_unsuccessful_amazon(campaign, **kwargs): amazon_suspension.connect(handle_wishlist_unsuccessful_amazon) -# The notification templates need some context; I'm making a note of that here -# This can be removed as the relevant functions are written -# RIGHTS_HOLDER_CLAIM_APPROVED: -# 'site': (site) -# 'claim': (claim) -# RIGHTS_HOLDER_CREATED: (no context needed) -# note -- it might be that wishlist_near_target and wishlist_near_deadline would -# both be triggered at about the same time -- do we want to prevent supporters -# from getting both within a small time frame? if so which supersedes? -# WISHLIST_NEAR_DEADLINE: -# 'site': (site) -# 'campaign': (the campaign) -# 'pledged': (true if the supporter has pledged, false otherwise) -# 'amount': (amount the supporter has pledged; only needed if pledged is true) -# WISHLIST_NEAR_TARGET: -# 'site': (site) -# 'campaign': (the campaign) -# 'pledged': (true if the supporter has pledged, false otherwise) -# 'amount': (amount the supporter has pledged; only needed if pledged is true) -# WISHLIST_PREMIUM_LIMITED_SUPPLY: -# (note we should not send this to people who have already claimed this premium) -# should we only send this to people who haven't pledged at all, or whose pledge -# is smaller than the amount of this premium? (don't want to encourage people to -# lower their pledge) -# the text assumes there is only 1 left -- if we're going to send at some other -# threshhold this will need to be revised -# 'campaign': (campaign) -# 'premium': (the premium in question) -# 'site': (site) -# WISHLIST_PRICE_DROP: -# 'campaign' -# 'pledged': (true if recipient has pledged to campaign, otherwise false) -# 'amount': (amount recipient has pledged, only needed if pledged=True) -# 'site' -# WISHLIST_SUCCESSFUL: -# 'pledged' -# 'campaign' -# 'site' -# WISHLIST_UNSUCCESSFUL: -# 'campaign' -# 'site' -# WISHLIST_UPDATED: -# I'd like to provide the actual text of the update in the message but I don't think -# we can reasonably do that, since campaign.description may contain HTML and we are -# sending notifications in plaintext. If we can send the update information in the -# email, though, let's do that. -# 'campaign' -# 'site' -# WISHLIST_WORK_CLAIMED -# does this trigger when someone claims a work, or when the claim is approved? -# I've written the text assuming it is the latter (sending notifications on the -# former seems too sausage-making, and might make people angry if they get -# notifications about claims they believe are false). If it's the former, the -# text should be revisited. -# 'claim' -# 'site' -# 'rightsholder' (the name of the rightsholder) \ No newline at end of file +wishlist_added = Signal(providing_args=["supporter", "work"]) + +def handle_wishlist_added(supporter, work, **kwargs): + """send notification to confirmed rights holder when someone wishes for their work""" + claim = work.claim.filter(status="active") + if claim: + notification.queue([claim.user], "new_wisher", { + 'supporter': supporter, + 'work': work + }, True) + + from regluit.core.tasks import emit_notifications + emit_notifications.delay() + +wishlist_added.connect(handle_wishlist_added) diff --git a/frontend/templates/notification/new_wisher/full.txt b/frontend/templates/notification/new_wisher/full.txt new file mode 100644 index 00000000..56b6fe04 --- /dev/null +++ b/frontend/templates/notification/new_wisher/full.txt @@ -0,0 +1,4 @@ +{{ supporter }} has wished for a work you hold rights to, {{ work.title }}. Hooray! There are now {{ work.num_wishes }} people wishing for this work. + + +The Unglue.it team \ No newline at end of file diff --git a/frontend/templates/notification/new_wisher/notice.html b/frontend/templates/notification/new_wisher/notice.html new file mode 100644 index 00000000..0a11d20c --- /dev/null +++ b/frontend/templates/notification/new_wisher/notice.html @@ -0,0 +1,9 @@ +{% extends "notification/notice_template.html" %} + +{% block comments_book %} + cover image for {{ work.title }} +{% endblock %} + +{% block comments_graphical %} + {{ supporter }} has wished for a work you hold rights to, {{ work.title }}. Hooray! +{% endblock %} \ No newline at end of file diff --git a/frontend/templates/notification/new_wisher/notice.txt b/frontend/templates/notification/new_wisher/notice.txt new file mode 100644 index 00000000..ebd00571 --- /dev/null +++ b/frontend/templates/notification/new_wisher/notice.txt @@ -0,0 +1,3 @@ +{{ supporter }} has wished for a work you hold rights to, {{ work.title }}. Hooray! There are now {{ work.num_wishes }} people wishing for this work. + + diff --git a/frontend/templates/notification/new_wisher/short.txt b/frontend/templates/notification/new_wisher/short.txt new file mode 100644 index 00000000..9233e50b --- /dev/null +++ b/frontend/templates/notification/new_wisher/short.txt @@ -0,0 +1 @@ +Someone new has wished for your work at Unglue.it \ No newline at end of file From 49f36e659e57999f1d216136ff91ed4e36842ad4 Mon Sep 17 00:00:00 2001 From: Andromeda Yelton Date: Fri, 31 Aug 2012 13:47:31 -0400 Subject: [PATCH 039/107] whoops resolving merge --- core/signals.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/signals.py b/core/signals.py index a3713eb5..3c2711f0 100644 --- a/core/signals.py +++ b/core/signals.py @@ -96,11 +96,8 @@ def create_notice_types(app, created_models, verbosity, **kwargs): notification.create_notice_type("rights_holder_created", _("Agreement Accepted"), _("You have become a verified Unglue.it rights holder.")) notification.create_notice_type("rights_holder_claim_approved", _("Claim Accepted"), _("A claim you've entered has been accepted.")) notification.create_notice_type("wishlist_unsuccessful_amazon", _("Campaign shut down"), _("An ungluing campaign that you supported had to be shut down due to an Amazon Payments policy change.")) -<<<<<<< HEAD -======= notification.create_notice_type("pledge_donation_credit", _("Donation Credit Balance"), _("You have a donation credit balance")) notification.create_notice_type("new_wisher", _("New wisher"), _("Someone new has wished for a book that you're the rightsholder for")) ->>>>>>> 2da4ba6... starting a notification for RHs when their books are wished for signals.post_syncdb.connect(create_notice_types, sender=notification) From cc8c0758426a5fb3e897daadba58e5af2937ee16 Mon Sep 17 00:00:00 2001 From: Raymond Yee Date: Fri, 31 Aug 2012 11:19:05 -0700 Subject: [PATCH 040/107] show error from bad card with stripe call --- payment/stripelib.py | 59 ++++++++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/payment/stripelib.py b/payment/stripelib.py index ac397019..3179ba5c 100644 --- a/payment/stripelib.py +++ b/payment/stripelib.py @@ -51,14 +51,14 @@ TEST_CARDS = ( ('3566002020360505','JCB') ) -ERROR_TESTING = ( - ('ADDRESS1_ZIP_FAIL', '4000000000000010', 'address_line1_check and address_zip_check will both fail'), - ('ADDRESS1_FAIL', '4000000000000028', 'address_line1_check will fail.'), - ('ADDRESS_ZIP_FAIL', '4000000000000036', 'address_zip_check will fail.'), - ('CVC_CHECK_FAIL', '4000000000000101', 'cvc_check will fail.'), - ('CHARGE_FAIL', '4000000000000341', 'Attaching this card to a Customer object will succeed, but attempts to charge the customer will fail.'), - ('CHARGE_DECLINE', '4000000000000002', 'Charges with this card will always be declined.') -) +ERROR_TESTING = dict(( + ('ADDRESS1_ZIP_FAIL', ('4000000000000010', 'address_line1_check and address_zip_check will both fail')), + ('ADDRESS1_FAIL', ('4000000000000028', 'address_line1_check will fail.')), + ('ADDRESS_ZIP_FAIL', ('4000000000000036', 'address_zip_check will fail.')), + ('CVC_CHECK_FAIL', ('4000000000000101', 'cvc_check will fail.')), + ('CHARGE_FAIL', ('4000000000000341', 'Attaching this card to a Customer object will succeed, but attempts to charge the customer will fail.')), + ('CHARGE_DECLINE', ('4000000000000002', 'Charges with this card will always be declined.')) +)) #card_declined: Use this special card number - 4000000000000002. #incorrect_number: Use a number that fails the Luhn check, e.g. 4242424242424241. @@ -162,25 +162,46 @@ class StripeClient(object): # what to work through? #pledge scenario -# +# bad card -- what types of erros to handle? +# https://stripe.com/docs/api#errors + +# what errors are handled in the python library and how? + + +# pending payments? +# how to tell whether money transferred to bank account yet +# best practices for calling Events -- not too often. class PledgeScenarioTest(TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): print "in setUp" - self.sc = StripeClient(api_key=STRIPE_SK) + cls._sc = StripeClient(api_key=STRIPE_SK) + + # valid card card0 = card() - self.good_cust = self.sc.create_customer(card=card0, description="test good customer", email="raymond.yee@gmail.com") - # set up a Customer with a good card tied to Gluejar + cls._good_cust = cls._sc.create_customer(card=card0, description="test good customer", email="raymond.yee@gmail.com") + + # bad card + test_card_num_to_get_charge_fail = ERROR_TESTING['CHARGE_FAIL'][0] + card1 = card(number=test_card_num_to_get_charge_fail) + cls._cust_bad_card = cls._sc.create_customer(card=card1, description="test bad customer", email="rdhyee@gluejar.com") + def test_charge_good_cust(self): - charge = self.sc.create_charge(10, customer=self.good_cust, description="$10 for good cust") + charge = self._sc.create_charge(10, customer=self._good_cust, description="$10 for good cust") print charge - def tearDown(self): + + def test_charge_bad_cust(self): + self.assertRaises(stripe.CardError, self._sc.create_charge, 10, + customer = self._cust_bad_card, description="$10 for bad cust") + @classmethod + def tearDownClass(cls): # clean up stuff we create in test print "in tearDown" - self.good_cust.delete() - print "list of customers", self.sc.customer.all() - print "list of charges", self.sc.charge.all() - print "list of events", self.sc.event.all() + cls._good_cust.delete() + print "list of customers", cls._sc.customer.all() + print "list of charges", cls._sc.charge.all() + print "list of events", cls._sc.event.all() def suite(): From e8e6a510684c56801bedfa72c9208441a3f9d8e2 Mon Sep 17 00:00:00 2001 From: Raymond Yee Date: Fri, 31 Aug 2012 22:59:22 -0700 Subject: [PATCH 041/107] added a bit of handling of stripe Transfers --- payment/stripelib.py | 47 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/payment/stripelib.py b/payment/stripelib.py index 3179ba5c..027b54e3 100644 --- a/payment/stripelib.py +++ b/payment/stripelib.py @@ -1,6 +1,9 @@ # https://github.com/stripe/stripe-python # https://stripe.com/docs/api?lang=python#top +from datetime import datetime +from pytz import utc + import stripe try: @@ -56,10 +59,12 @@ ERROR_TESTING = dict(( ('ADDRESS1_FAIL', ('4000000000000028', 'address_line1_check will fail.')), ('ADDRESS_ZIP_FAIL', ('4000000000000036', 'address_zip_check will fail.')), ('CVC_CHECK_FAIL', ('4000000000000101', 'cvc_check will fail.')), - ('CHARGE_FAIL', ('4000000000000341', 'Attaching this card to a Customer object will succeed, but attempts to charge the customer will fail.')), + ('BAD_ATTACHED_CARD', ('4000000000000341', 'Attaching this card to a Customer object will succeed, but attempts to charge the customer will fail.')), ('CHARGE_DECLINE', ('4000000000000002', 'Charges with this card will always be declined.')) )) +# types of errors / when they can be handled + #card_declined: Use this special card number - 4000000000000002. #incorrect_number: Use a number that fails the Luhn check, e.g. 4242424242424241. #invalid_expiry_month: Use an invalid month e.g. 13. @@ -67,6 +72,7 @@ ERROR_TESTING = dict(( #invalid_cvc: Use a two digit number e.g. 99. + def filter_none(d): return dict([(k,v) for (k,v) in d.items() if v is not None]) @@ -110,10 +116,15 @@ class StripeClient(object): @property def token(self): return stripe.Token(api_key=self.api_key) + + @property + def transfer(self): + return stripe.Transfer(api_key=self.api_key) @property def event(self): return stripe.Event(api_key=self.api_key) + def create_token(self, card): return stripe.Token(api_key=self.api_key).create(card=card) @@ -161,12 +172,23 @@ class StripeClient(object): # what to work through? +# can't test Transfer in test mode: "There are no transfers in test mode." + #pledge scenario # bad card -- what types of erros to handle? # https://stripe.com/docs/api#errors # what errors are handled in the python library and how? +# Account? + +# events of interest +# charge.succeeded, charge.failed, charge.refunded, charge.disputed +# customer.created, customer.updated, customer.deleted +# transfer.created, transfer.updated, transfer.failed + +# When will the money I charge with Stripe end up in my bank account? +# Every day, we transfer the money that you charged seven days previously?that is, you receive the money for your March 1st charges on March 8th. # pending payments? # how to tell whether money transferred to bank account yet @@ -183,15 +205,22 @@ class PledgeScenarioTest(TestCase): cls._good_cust = cls._sc.create_customer(card=card0, description="test good customer", email="raymond.yee@gmail.com") # bad card - test_card_num_to_get_charge_fail = ERROR_TESTING['CHARGE_FAIL'][0] - card1 = card(number=test_card_num_to_get_charge_fail) + test_card_num_to_get_BAD_ATTACHED_CARD = ERROR_TESTING['BAD_ATTACHED_CARD'][0] + card1 = card(number=test_card_num_to_get_BAD_ATTACHED_CARD) cls._cust_bad_card = cls._sc.create_customer(card=card1, description="test bad customer", email="rdhyee@gluejar.com") def test_charge_good_cust(self): charge = self._sc.create_charge(10, customer=self._good_cust, description="$10 for good cust") - print charge + print charge.id + + def test_error_creating_customer_with_declined_card(self): + # should get a CardError upon attempt to create Customer with this card + _card = card(number=card(ERROR_TESTING['CHARGE_DECLINE'][0])) + self.assertRaises(stripe.CardError, self._sc.create_customer, card=_card) + def test_charge_bad_cust(self): + # expect the card to be declined -- and for us to get CardError self.assertRaises(stripe.CardError, self._sc.create_charge, 10, customer = self._cust_bad_card, description="$10 for bad cust") @classmethod @@ -199,9 +228,15 @@ class PledgeScenarioTest(TestCase): # clean up stuff we create in test print "in tearDown" cls._good_cust.delete() - print "list of customers", cls._sc.customer.all() - print "list of charges", cls._sc.charge.all() + print "list of customers" + print [(i, c.id, c.description, datetime.fromtimestamp(c.created, tz=utc), c.account_balance) for(i, c) in enumerate(cls._sc.customer.all()["data"])] + + print "list of charges" + print [(i, c.id, c.amount, c.currency, c.description, datetime.fromtimestamp(c.created, tz=utc), c.paid, c.fee, c.disputed, c.amount_refunded, c.failure_message, c.card.fingerprint, c.card.last4) for (i, c) in enumerate(cls._sc.charge.all()['data'])] + + # can retrieve events since a certain time print "list of events", cls._sc.event.all() + # [(i, e.id, e.type, e.created, e.pending_webhooks, e.data) for (i,e) in enumerate(s.event.all()['data'])] def suite(): From 9ccfb4be48f2f857e8e943e4ff616dc1d42696e6 Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 5 Sep 2012 11:17:27 -0400 Subject: [PATCH 042/107] Ed's tired of getting unglue.it bug reports --- settings/dev.py | 4 +++- settings/jenkins.py | 4 +++- settings/just.py | 1 - settings/please.py | 1 - settings/prod.py | 1 - 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/settings/dev.py b/settings/dev.py index 4ad6a07a..51db212a 100644 --- a/settings/dev.py +++ b/settings/dev.py @@ -10,7 +10,9 @@ IS_PREVIEW = False SITE_ID = 3 ADMINS = ( - ('Ed Summers', 'ehs@pobox.com'), + ('Raymond Yee', 'rdhyee+ungluebugs@gluejar.com'), + ('Eric Hellman', 'eric@gluejar.com'), + ('Andromeda Yelton', 'andromeda@gluejar.com'), ) MANAGERS = ADMINS diff --git a/settings/jenkins.py b/settings/jenkins.py index 00eabf02..975da099 100644 --- a/settings/jenkins.py +++ b/settings/jenkins.py @@ -4,7 +4,9 @@ DEBUG = True TEMPLATE_DEBUG = DEBUG ADMINS = ( - ('Ed Summers', 'ehs@pobox.com'), + ('Raymond Yee', 'rdhyee+ungluebugs@gluejar.com'), + ('Eric Hellman', 'eric@gluejar.com'), + ('Andromeda Yelton', 'andromeda@gluejar.com'), ) MANAGERS = ADMINS diff --git a/settings/just.py b/settings/just.py index 05f2d051..c071108e 100644 --- a/settings/just.py +++ b/settings/just.py @@ -6,7 +6,6 @@ TEMPLATE_DEBUG = DEBUG SITE_ID = 5 ADMINS = ( - ('Ed Summers', 'ed.summers@gmail.com'), ('Raymond Yee', 'rdhyee+ungluebugs@gluejar.com'), ('Eric Hellman', 'eric@gluejar.com'), ('Andromeda Yelton', 'andromeda@gluejar.com'), diff --git a/settings/please.py b/settings/please.py index b5738930..7da2c37d 100644 --- a/settings/please.py +++ b/settings/please.py @@ -6,7 +6,6 @@ TEMPLATE_DEBUG = DEBUG SITE_ID = 2 ADMINS = ( - ('Ed Summers', 'ed.summers@gmail.com'), ('Raymond Yee', 'rdhyee+ungluebugs@gluejar.com'), ('Eric Hellman', 'eric@gluejar.com'), ('Andromeda Yelton', 'andromeda@gluejar.com'), diff --git a/settings/prod.py b/settings/prod.py index 94627192..6f93aedd 100644 --- a/settings/prod.py +++ b/settings/prod.py @@ -8,7 +8,6 @@ IS_PREVIEW = False SITE_ID = 1 ADMINS = ( - ('Ed Summers', 'ed.summers@gmail.com'), ('Raymond Yee', 'rdhyee+ungluebugs@gluejar.com'), ('Eric Hellman', 'eric@gluejar.com'), ('Andromeda Yelton', 'andromeda@gluejar.com'), From bcbc75ed3d303800c8dd6b74eac291c6fbd49816 Mon Sep 17 00:00:00 2001 From: Raymond Yee Date: Wed, 5 Sep 2012 16:11:32 -0700 Subject: [PATCH 043/107] More analysis of what we need to figure out still about stripe. --- payment/stripelib.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/payment/stripelib.py b/payment/stripelib.py index 027b54e3..db01753a 100644 --- a/payment/stripelib.py +++ b/payment/stripelib.py @@ -13,12 +13,14 @@ except: from django.test import TestCase from django.utils import unittest - # if customer.id doesn't exist, create one and then charge the customer # we probably should ask our users whether they are ok with our creating a customer id account -- or ask for credit # card info each time.... # should load the keys for Stripe from db -- but for now just hardcode here +# moving towards not having the stripe api key for the non profit partner in the unglue.it code -- but in a logically +# distinct application + try: from regluit.core.models import Key STRIPE_PK = Key.objects.get(name="STRIPE_PK").value @@ -179,13 +181,18 @@ class StripeClient(object): # https://stripe.com/docs/api#errors # what errors are handled in the python library and how? +# # Account? -# events of interest -# charge.succeeded, charge.failed, charge.refunded, charge.disputed +# https://stripe.com/docs/api#event_types +# events of interest -- especially ones that do not directly arise immediately (synchronously) from something we do -- I think +# especially: charge.disputed +# I think following (charge.succeeded, charge.failed, charge.refunded) pretty much sychronous to our actions # customer.created, customer.updated, customer.deleted -# transfer.created, transfer.updated, transfer.failed + +# transfer +# I expect the ones related to transfers all happen asynchronously: transfer.created, transfer.updated, transfer.failed # When will the money I charge with Stripe end up in my bank account? # Every day, we transfer the money that you charged seven days previously?that is, you receive the money for your March 1st charges on March 8th. @@ -248,7 +255,6 @@ def suite(): return suites - # IPNs/webhooks: https://stripe.com/docs/webhooks # how to use pending_webhooks ? From 8eeadc99d3d9efe100b217c383272d60ee578b7e Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 6 Sep 2012 01:01:17 -0400 Subject: [PATCH 044/107] woo hoo, credit and donation scaffolding works end to end [#21784207] --- frontend/forms.py | 24 ++- frontend/templates/donation_credit.html | 42 ++++ frontend/templates/fund_the_pledge.html | 57 +++++ frontend/templates/nonprofit.html | 46 ++++ frontend/templates/nonprofit_base.html | 200 ++++++++++++++++++ frontend/urls.py | 7 +- frontend/views.py | 107 +++++++++- payment/credit.py | 21 +- payment/manager.py | 6 +- ...uto__add_sent__add_field_creditlog_sent.py | 200 ++++++++++++++++++ payment/models.py | 25 ++- settings/common.py | 6 +- 12 files changed, 709 insertions(+), 32 deletions(-) create mode 100644 frontend/templates/donation_credit.html create mode 100644 frontend/templates/fund_the_pledge.html create mode 100644 frontend/templates/nonprofit.html create mode 100644 frontend/templates/nonprofit_base.html create mode 100644 payment/migrations/0009_auto__add_sent__add_field_creditlog_sent.py diff --git a/frontend/forms.py b/frontend/forms.py index 3f76ab1f..041bab07 100644 --- a/frontend/forms.py +++ b/frontend/forms.py @@ -335,20 +335,26 @@ class CampaignPledgeForm(forms.Form): return cleaned_data -class DonateForm(forms.Form): - donation_amount = forms.DecimalField( +class CCForm(forms.Form): + username = forms.CharField(max_length=30, required=True ) + work_id = forms.IntegerField(required=False, widget=forms.HiddenInput() ) + preapproval_amount= forms.DecimalField( required=False, min_value=D('1.00'), max_value=D('100000.00'), - decimal_places=2, - label="Donation", + decimal_places=0, + label="Pledge", ) - anonymous = forms.BooleanField(required=False, label=_("Don't display my username in the donors' list")) - - def clean(self): - cleaned_data = self.cleaned_data - return cleaned_data + + +class DonateForm(forms.Form): + preapproval_amount = forms.DecimalField( widget=forms.HiddenInput() ) + username = forms.CharField(max_length=30, required=True, widget=forms.HiddenInput() ) + work_id = forms.IntegerField(required=False, widget=forms.HiddenInput() ) + title = forms.CharField(max_length=30, required=False, widget=forms.HiddenInput() ) + + class GoodreadsShelfLoadingForm(forms.Form): goodreads_shelf_name_number = forms.CharField(widget=forms.Select(choices=( ('all','all'), diff --git a/frontend/templates/donation_credit.html b/frontend/templates/donation_credit.html new file mode 100644 index 00000000..09db1dde --- /dev/null +++ b/frontend/templates/donation_credit.html @@ -0,0 +1,42 @@ +{% extends "basepledge.html" %} +{% load humanize %} + +{% block title %}Fund Your Pledge{% endblock %} + +{% block extra_extra_head %} + + + + +{% endblock %} + +{% block doccontent %} +
    + +
    +
    + +

    Congratulations, your donation credits have been registered!

    +
    + + {{ envelope.amount }}
    + {{ envelope.username }}
    + {{ envelope.work_id }}
    + {{ envelope.sent }}
    + {{ error }}
    + {{ transaction }}
    +

    +You have a balance of {{ request.user.credit.balance }} donation credits.
    +You have pledged {{ request.user.credit.pledged }} donation credits to ungluing campaigns.
    +You have {{ request.user.credit.available }} donation credits available to pledge or transfer.
    +

    + + +
    +
    + +
    +
    +{% endblock %} + + diff --git a/frontend/templates/fund_the_pledge.html b/frontend/templates/fund_the_pledge.html new file mode 100644 index 00000000..feb422a0 --- /dev/null +++ b/frontend/templates/fund_the_pledge.html @@ -0,0 +1,57 @@ +{% extends "basepledge.html" %} +{% load humanize %} + +{% block title %}Fund Your Pledge{% endblock %} + +{% block extra_extra_head %} + + + + +{% endblock %} + +{% block doccontent %} +
    + +
    +
    + +

    Funding Your Pledge

    +
    We're so happy that you've decided to join this campaign. We have two ways we can fund your pledge of ${{preapproval_amount|intcomma}}. +
      +
    1. You can make a donation now to {{nonprofit.name}}, a non-profit charity that's working with Unglue.it to give books to the world.
    2. +
    3. You can give us your credit card information now; we'll charge your card only if the campaign succeeds.
    4. +
    +
    +
    + +
    +

    Pledge by Credit Card

    +

    Unglue.it uses Stripe to securely manage your Credit Card information. +

    +
    +
    + {% csrf_token %} + {{ form.non_field_errors }} + {{form}} + +
    +
    +
    + +
    +
    +{% endblock %} + + diff --git a/frontend/templates/nonprofit.html b/frontend/templates/nonprofit.html new file mode 100644 index 00000000..a2d4c48c --- /dev/null +++ b/frontend/templates/nonprofit.html @@ -0,0 +1,46 @@ +{% extends "nonprofit_base.html" %} +{% load humanize %} + +{% block title %}Donate to {{nonprofit.name}}{% endblock %} + +{% block extra_extra_head %} + + + + +{% endblock %} + +{% block doccontent %} +
    + +
    +
    + +

    Supporting {{ get.title }} through {{nonprofit.name}}

    +

    {{nonprofit.name}} is cooperating with Unglue.it to make books free to the world. On this page, you can make a donation to us, and we'll hold the money until the ungluing campaign you've selected succeeds. Then we'll make a payment to the rightsholder along side unglue.it. Once you've made the donation, we'll ad donation credits to your unglue.it account, {{get.username}}. +

    +

    + To fund the pledge of ${{get.preapproval_amount|intcomma}} that you've made at unglue.it, you'll need to make a donation of at least that amount. You can donate more than that of course- you'll get additional donation credits to use on other campaigns. The larger your donation, the less percentage-wise that gets eaten up by processing fees. +

    +

    Any interest we earn will be used for our public purpose of helping libraries remain relevant into the future. If your unglue.it donation credits don't get used for 5 years, we'll use it in the same way. Your donation to {{nonprofit.name}} is deductible as a Charitable donation in the US.

    +
    +
    + + +
    +
    +{% endblock %} + + diff --git a/frontend/templates/nonprofit_base.html b/frontend/templates/nonprofit_base.html new file mode 100644 index 00000000..9e1b29cf --- /dev/null +++ b/frontend/templates/nonprofit_base.html @@ -0,0 +1,200 @@ + + + + + + + + + +{% block title %}Join the Movement | Library Renewal{% endblock %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{% block extra_extra_head %}{% endblock %} + + + + + + + + +
    +
    + + + + +
    + +{% block doccontent %}{% endblock %} + +
    +
      +
    • Delicious
    • +
    • Digg
    • +
    • E-Mail
    • +
    • Facebook
    • +
    • Google Buzz
    • +
    • LinkedIn
    • +
    • StumbleUpon
    • +
    • Twitter
    • + +
    +
    + +
    + +
    +
    + + + + + + + +
    + +
    + +
      + +
    • + +
    • + +
    • + +
    + +
    + +
    + + + + + + + + + + + + + + + + + + diff --git a/frontend/urls.py b/frontend/urls.py index 1b2c4ce8..02115ea7 100644 --- a/frontend/urls.py +++ b/frontend/urls.py @@ -3,12 +3,14 @@ from django.views.generic.simple import direct_to_template from django.views.generic.base import TemplateView from django.views.generic import ListView, DetailView from django.contrib.auth.decorators import login_required +from django.views.decorators.csrf import csrf_exempt from django.conf import settings from regluit.core.feeds import SupporterWishlistFeed from regluit.core.models import Campaign from regluit.frontend.views import GoodreadsDisplayView, LibraryThingView, PledgeView, PledgeCompleteView, PledgeCancelView, PledgeRechargeView, FAQView -from regluit.frontend.views import CampaignListView, DonateView, WorkListView, UngluedListView, InfoPageView, InfoLangView, DonationView +from regluit.frontend.views import CampaignListView, DonateView, WorkListView, UngluedListView, InfoPageView, InfoLangView, DonationView, FundPledgeView +from regluit.frontend.views import NonprofitCampaign, DonationCredit urlpatterns = patterns( "regluit.frontend.views", @@ -52,11 +54,14 @@ urlpatterns = patterns( url(r"^new_edition/(?P\d*)/(?P\d*)$", "new_edition", name="new_edition"), url(r"^googlebooks/(?P.+)/$", "googlebooks", name="googlebooks"), url(r"^donation/$", login_required(DonationView.as_view()), name="donation"), + url(r"^donation/credit/(?P.+)/$", login_required(DonationCredit.as_view()), name="donation_credit"), url(r"^pledge/(?P\d+)/$", login_required(PledgeView.as_view()), name="pledge"), url(r"^pledge/cancel/(?P\d+)$", login_required(PledgeCancelView.as_view()), name="pledge_cancel"), url(r"^pledge/complete/$", login_required(PledgeCompleteView.as_view()), name="pledge_complete"), url(r"^pledge/modify/(?P\d+)$", login_required(PledgeView.as_view()), name="pledge_modify"), + url(r"^pledge/fund/(?P\d+)$", login_required(FundPledgeView.as_view()), name="fund_pledge"), url(r"^pledge/recharge/(?P\d+)$", login_required(PledgeRechargeView.as_view()), name="pledge_recharge"), + url(r"^donate_to_campaign/$", csrf_exempt(NonprofitCampaign.as_view()), name="nonprofit"), url(r"^subjects/$", "subjects", name="subjects"), url(r"^librarything/$", LibraryThingView.as_view(), name="librarything"), url(r"^librarything/load/$","librarything_load", name="librarything_load"), diff --git a/frontend/views.py b/frontend/views.py index 0944a2f3..18718210 100755 --- a/frontend/views.py +++ b/frontend/views.py @@ -17,6 +17,7 @@ import oauth2 as oauth from django import forms from django.conf import settings from django.contrib.auth.models import User +from django.core import signing from django.core.urlresolvers import reverse from django.core.exceptions import ObjectDoesNotExist from django.contrib import messages @@ -47,14 +48,15 @@ from regluit.frontend.forms import UserData, UserEmail, ProfileForm, CampaignPle from regluit.frontend.forms import RightsHolderForm, UserClaimForm, LibraryThingForm, OpenCampaignForm from regluit.frontend.forms import getManageCampaignForm, DonateForm, CampaignAdminForm, EmailShareForm, FeedbackForm from regluit.frontend.forms import EbookForm, CustomPremiumForm, EditManagersForm, EditionForm, PledgeCancelForm -from regluit.frontend.forms import getTransferCreditForm +from regluit.frontend.forms import getTransferCreditForm, CCForm from regluit.payment.manager import PaymentManager from regluit.payment.models import Transaction -from regluit.payment.parameters import TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE, TRANSACTION_STATUS_CANCELED, TRANSACTION_STATUS_ERROR, TRANSACTION_STATUS_FAILED, TRANSACTION_STATUS_INCOMPLETE +from regluit.payment.parameters import TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE, TRANSACTION_STATUS_CANCELED, TRANSACTION_STATUS_ERROR, TRANSACTION_STATUS_FAILED, TRANSACTION_STATUS_INCOMPLETE, TRANSACTION_STATUS_NONE from regluit.payment.parameters import PAYMENT_TYPE_AUTHORIZATION +from regluit.payment.credit import credit_transaction from regluit.core import goodreads from tastypie.models import ApiKey -from regluit.payment.models import Transaction +from regluit.payment.models import Transaction, Sent, CreditLog from notification import models as notification @@ -707,7 +709,7 @@ class PledgeView(FormView): host = None, campaign=self.campaign, user=self.request.user, - premium=premium, + premium=self.get_premium(form), paymentReason="Unglue.it Pledge for {0}".format(self.campaign.name), ack_name=form.cleaned_data["ack_name"], ack_dedication=form.cleaned_data["ack_dedication"], @@ -720,7 +722,104 @@ class PledgeView(FormView): logger.error("Attempt to produce transaction id {0} failed".format(t.id)) return HttpResponse("Our attempt to enable your transaction failed. We have logged this error.") +class FundPledgeView(FormView): + template_name="fund_the_pledge.html" + form_class = CCForm + transaction = None + def get_form_kwargs(self): + assert self.request.user.is_authenticated() + if self.transaction is None: + self.transaction = get_object_or_404(Transaction, id=self.kwargs["t_id"]) + return {'data':{'preapproval_amount':self.transaction.max_amount, + 'username':self.request.user.username, + 'work_id':self.transaction.campaign.work.id, + 'title':self.transaction.campaign.work.title} } + + def get_context_data(self, **kwargs): + context = super(FundPledgeView, self).get_context_data(**kwargs) + context['preapproval_amount']=self.transaction.max_amount + context['transaction']=self.transaction + context['nonprofit'] = settings.NONPROFIT + context['donate_form'] = DonateForm(**self.get_form_kwargs()) + return context + +class NonprofitCampaign(FormView): + template_name="nonprofit.html" + form_class = CCForm + + def get_context_data(self, **kwargs): + context = super(NonprofitCampaign, self).get_context_data(**kwargs) + context['nonprofit'] = settings.NONPROFIT + context['get'] = self.request.GET + return context + + def get_form_kwargs(self): + if self.request.method == 'POST': + return {'data':self.request.POST} + else: + return {'initial':self.request.GET } + + + + def form_valid(self, form): + username=form.cleaned_data['username'] + forward={'username':username} + forward['work_id']= form.cleaned_data['work_id'] + forward['amount']= int(form.cleaned_data['preapproval_amount']) + forward['sent']= Sent.objects.create(user=username,amount=form.cleaned_data['preapproval_amount']).pk + token=signing.dumps(forward) + return HttpResponseRedirect(settings.BASE_URL + reverse('donation_credit',kwargs={'token':token})) + +class DonationCredit(TemplateView): + template_name="donation_credit.html" + + def get_context_data(self, **kwargs): + context = super(DonationCredit, self).get_context_data(**kwargs) + try: + envelope=signing.loads(kwargs['token']) + context['envelope']=envelope + except signing.BadSignature: + self.template_name="donation_error.html" + return context + try: + work = models.Work.objects.get(id=envelope['work_id']) + campaign=work.last_campaign() + except models.Work.DoesNotExist: + campaign = None + context['work']=work + try: + user = User.objects.get(username=envelope['username']) + except User.DoesNotExist: + self.template_name="donation_user_error.html" + context['error']='user does not exist' + return context + if user != self.request.user: + self.template_name="donation_user_error.html" + context['error']='wrong user logged in' + return context + try: + # check token not used + CreditLog.objects.get(sent=envelope['sent']) + context['error']='credit already registered' + return context + except CreditLog.DoesNotExist: + #not used yet! + CreditLog.objects.create(user=user,amount=envelope['amount'],action='deposit',sent=envelope['sent']) + ts=Transaction.objects.filter(user=user,campaign=campaign,status=TRANSACTION_STATUS_NONE) + if ts.count()>0: + t=ts[0] + credit_transaction(t,user, envelope['amount']) + for t in ts[1:]: + t.status=TRANSACTION_STATUS_CANCELED + t.save() + context['transaction']=t + return context + else: + user.credit.add_to_balance(envelope['amount']) + return context + + class PledgeRechargeView(TemplateView): """ a view to allow for recharge of a transaction for failed transactions or ones with errors diff --git a/payment/credit.py b/payment/credit.py index c5987d5b..d4c8b712 100644 --- a/payment/credit.py +++ b/payment/credit.py @@ -4,7 +4,6 @@ from django.contrib.auth.models import User from django.conf import settings from regluit.payment.parameters import * -from regluit.utils.localdatetime import now from regluit.payment.baseprocessor import BasePaymentRequest @@ -16,19 +15,19 @@ def pledge_transaction(t,user,amount): user.credit.add_to_pledged(amount-t.amount) else: user.credit.add_to_pledged(amount) - t.amount=amount t.max_amount=amount - t.host = PAYMENT_HOST_CREDIT - t.type = PAYMENT_TYPE_AUTHORIZATION - t.status=TRANSACTION_STATUS_ACTIVE - t.approved=True - now_val = now() - t.date_authorized = now_val - t.date_expired = now_val + timedelta( days=settings.PREAPPROVAL_PERIOD ) - - t.save() + t.set_credit_approved(amount) +def credit_transaction(t,user,amount): + '''user has new credit, use it to fund the transaction''' + # first, credit the user's account + user.credit.add_to_balance(amount) + # now pledge to the transaction + pledge_amount = t.max_amount if t.max_amount <= amount else amount + user.credit.add_to_pledged(pledge_amount) + t.set_credit_approved(pledge_amount) + class CancelPreapproval(BasePaymentRequest): ''' Cancels an exisiting token. diff --git a/payment/manager.py b/payment/manager.py index 017bfe2a..00029ca5 100644 --- a/payment/manager.py +++ b/payment/manager.py @@ -640,7 +640,7 @@ class PaymentManager( object ): t = Transaction.objects.create(amount=0, max_amount=amount, currency=currency, - status='NONE', + status=TRANSACTION_STATUS_NONE, campaign=campaign, user=user, anonymous=anonymous, @@ -659,8 +659,8 @@ class PaymentManager( object ): pledge_created.send(sender=self, transaction=t) return t, return_url else: - #TODO send user to choose payment path - return t, None + # send user to choose payment path + return t, reverse('fund_pledge', args=[t.id]) def cancel_related_transaction(self, transaction, status=TRANSACTION_STATUS_ACTIVE, campaign=None): diff --git a/payment/migrations/0009_auto__add_sent__add_field_creditlog_sent.py b/payment/migrations/0009_auto__add_sent__add_field_creditlog_sent.py new file mode 100644 index 00000000..f6cc1c11 --- /dev/null +++ b/payment/migrations/0009_auto__add_sent__add_field_creditlog_sent.py @@ -0,0 +1,200 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'Sent' + db.create_table('payment_sent', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.CharField')(max_length=32, null=True)), + ('amount', self.gf('django.db.models.fields.DecimalField')(default='0.00', max_digits=14, decimal_places=2)), + ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + )) + db.send_create_signal('payment', ['Sent']) + + # Adding field 'CreditLog.sent' + db.add_column('payment_creditlog', 'sent', self.gf('django.db.models.fields.IntegerField')(null=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting model 'Sent' + db.delete_table('payment_sent') + + # Deleting field 'CreditLog.sent' + db.delete_column('payment_creditlog', 'sent') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 9, 5, 23, 7, 10, 823074)'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 9, 5, 23, 7, 10, 822938)'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'core.campaign': { + 'Meta': {'object_name': 'Campaign'}, + 'activated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'amazon_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'deadline': ('django.db.models.fields.DateTimeField', [], {}), + 'description': ('ckeditor.fields.RichTextField', [], {'null': 'True'}), + 'details': ('ckeditor.fields.RichTextField', [], {'null': 'True', 'blank': 'True'}), + 'edition': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'null': 'True', 'to': "orm['core.Edition']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'left': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}), + 'license': ('django.db.models.fields.CharField', [], {'default': "'CC BY-NC-ND'", 'max_length': '255'}), + 'managers': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'campaigns'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '500', 'null': 'True'}), + 'paypal_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'INITIALIZED'", 'max_length': '15', 'null': 'True'}), + 'target': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'to': "orm['core.Work']"}) + }, + 'core.edition': { + 'Meta': {'object_name': 'Edition'}, + 'cover_image': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'public_domain': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'publication_date': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'publisher': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}), + 'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'editions'", 'null': 'True', 'to': "orm['core.Work']"}) + }, + 'core.premium': { + 'Meta': {'object_name': 'Premium'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '0'}), + 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'premiums'", 'null': 'True', 'to': "orm['core.Campaign']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'limit': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '2'}) + }, + 'core.work': { + 'Meta': {'ordering': "['title']", 'object_name': 'Work'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '2'}), + 'num_wishes': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'openlibrary_lookup': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}) + }, + 'payment.credit': { + 'Meta': {'object_name': 'Credit'}, + 'balance': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_activity': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'pledged': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'credit'", 'unique': 'True', 'to': "orm['auth.User']"}) + }, + 'payment.creditlog': { + 'Meta': {'object_name': 'CreditLog'}, + 'action': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'sent': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'payment.paymentresponse': { + 'Meta': {'object_name': 'PaymentResponse'}, + 'api': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'correlation_id': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'timestamp': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'transaction': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['payment.Transaction']"}) + }, + 'payment.receiver': { + 'Meta': {'object_name': 'Receiver'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'currency': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'email': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'local_status': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'primary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'transaction': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['payment.Transaction']"}), + 'txn_id': ('django.db.models.fields.CharField', [], {'max_length': '64'}) + }, + 'payment.sent': { + 'Meta': {'object_name': 'Sent'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}) + }, + 'payment.transaction': { + 'Meta': {'object_name': 'Transaction'}, + 'ack_dedication': ('django.db.models.fields.CharField', [], {'max_length': '140', 'null': 'True'}), + 'ack_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'approved': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Campaign']", 'null': 'True'}), + 'currency': ('django.db.models.fields.CharField', [], {'default': "'USD'", 'max_length': '10', 'null': 'True'}), + 'date_authorized': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'date_executed': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_expired': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'date_payment': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'error': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True'}), + 'execution': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'host': ('django.db.models.fields.CharField', [], {'default': "'none'", 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'local_status': ('django.db.models.fields.CharField', [], {'default': "'NONE'", 'max_length': '32', 'null': 'True'}), + 'max_amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}), + 'pay_key': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'preapproval_key': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'premium': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Premium']", 'null': 'True'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'receipt': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True'}), + 'secret': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'None'", 'max_length': '32'}), + 'type': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + } + } + + complete_apps = ['payment'] diff --git a/payment/models.py b/payment/models.py index 486b24fc..15a901c5 100644 --- a/payment/models.py +++ b/payment/models.py @@ -4,6 +4,7 @@ from django.conf import settings from regluit.core.models import Campaign, Wishlist, Premium from regluit.payment.parameters import * from regluit.payment.signals import credit_balance_added +from regluit.utils.localdatetime import now from decimal import Decimal, NaN import uuid import urllib @@ -108,8 +109,18 @@ class Transaction(models.Model): else: mod = __import__("regluit.payment." + self.host, fromlist=[str(self.host)]) return mod - - + + def set_credit_approved(self, amount): + self.amount=amount + self.host = PAYMENT_HOST_CREDIT + self.type = PAYMENT_TYPE_AUTHORIZATION + self.status=TRANSACTION_STATUS_ACTIVE + self.approved=True + now_val = now() + self.date_authorized = now_val + self.date_expired = now_val + timedelta( days=settings.PREAPPROVAL_PERIOD ) + self.save() + class PaymentResponse(models.Model): # The API used api = models.CharField(max_length=64, null=False) @@ -155,6 +166,8 @@ class CreditLog(models.Model): amount = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99 timestamp = models.DateTimeField(auto_now=True) action = models.CharField(max_length=16) + # used to record the sent id when action = 'deposit' + sent=models.IntegerField(null=True) class Credit(models.Model): user = models.OneToOneField(User, related_name='credit') @@ -224,7 +237,13 @@ class Credit(models.Model): return False else: return False - + +class Sent(models.Model): + '''used by donation view to record donations it has sent''' + user = models.CharField(max_length=32, null=True) + amount = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99 + timestamp = models.DateTimeField(auto_now=True) + from django.db.models.signals import post_save, post_delete import regluit.payment.manager diff --git a/settings/common.py b/settings/common.py index 644478f5..50715f4c 100644 --- a/settings/common.py +++ b/settings/common.py @@ -49,7 +49,7 @@ STATIC_URL = '/static/' # URL prefix for admin static files -- CSS, JavaScript and images. # Make sure to use a trailing slash. # Examples: "http://foo.com/static/admin/", "/static/admin/". -ADMIN_MEDIA_PREFIX = '/static/admin/' +# ADMIN_MEDIA_PREFIX = '/static/admin/' # Additional locations of static files STATICFILES_DIRS = ( @@ -282,3 +282,7 @@ SECRET_KEY = '' MAINTENANCE_MODE = False # Sequence of URL path regexes to exclude from the maintenance mode. MAINTENANCE_IGNORE_URLS = {} + +class NONPROFIT: + name = 'Library Renewal' + link = 'http://127.0.0.1:8000/donate_to_campaign/' \ No newline at end of file From 9395cd058b6a97ef59b37168d9477a76a0a02e8f Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 6 Sep 2012 11:36:13 -0400 Subject: [PATCH 045/107] now donating cents works, too --- frontend/forms.py | 2 +- frontend/templates/donation_credit.html | 2 +- frontend/templates/fund_the_pledge.html | 4 ++-- frontend/views.py | 11 +++++++---- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/frontend/forms.py b/frontend/forms.py index 041bab07..c096ef64 100644 --- a/frontend/forms.py +++ b/frontend/forms.py @@ -342,7 +342,7 @@ class CCForm(forms.Form): required=False, min_value=D('1.00'), max_value=D('100000.00'), - decimal_places=0, + decimal_places=2, label="Pledge", ) diff --git a/frontend/templates/donation_credit.html b/frontend/templates/donation_credit.html index 09db1dde..94f8af27 100644 --- a/frontend/templates/donation_credit.html +++ b/frontend/templates/donation_credit.html @@ -19,7 +19,7 @@

    Congratulations, your donation credits have been registered!

    - {{ envelope.amount }}
    + {{ envelope.amount }}.{{ envelope.cents }}
    {{ envelope.username }}
    {{ envelope.work_id }}
    {{ envelope.sent }}
    diff --git a/frontend/templates/fund_the_pledge.html b/frontend/templates/fund_the_pledge.html index feb422a0..abff2f6d 100644 --- a/frontend/templates/fund_the_pledge.html +++ b/frontend/templates/fund_the_pledge.html @@ -17,7 +17,7 @@

    Funding Your Pledge

    -
    We're so happy that you've decided to join this campaign. We have two ways we can fund your pledge of ${{preapproval_amount|intcomma}}. +
    We're so happy that you've decided to join this campaign. We have two ways we can fund your pledge of ${{preapproval_amount}}.
    1. You can make a donation now to {{nonprofit.name}}, a non-profit charity that's working with Unglue.it to give books to the world.
    2. You can give us your credit card information now; we'll charge your card only if the campaign succeeds.
    3. @@ -26,7 +26,7 @@