Merge branch 'master' into collect_address_info
Conflicts: static/css/sitewide.css static/less/sitewide.lesspull/1/head
commit
839ce0496f
|
@ -470,6 +470,9 @@ def merge_works(w1, w2, user=None):
|
|||
for campaign in w2.campaigns.all():
|
||||
campaign.work = w1
|
||||
campaign.save()
|
||||
for claim in w2.claim.all():
|
||||
claim.work = w1
|
||||
claim.save()
|
||||
for wishlist in models.Wishlist.objects.filter(works__in=[w2]):
|
||||
w2source = wishlist.work_source(w2)
|
||||
wishlist.remove_work(w2)
|
||||
|
|
|
@ -228,7 +228,7 @@ class CustomPremiumForm(forms.ModelForm):
|
|||
model = Premium
|
||||
fields = 'campaign', 'amount', 'description', 'type', 'limit'
|
||||
widgets = {
|
||||
'description': forms.Textarea(attrs={'cols': 80, 'rows': 2}),
|
||||
'description': forms.Textarea(attrs={'cols': 80, 'rows': 4}),
|
||||
'campaign': forms.HiddenInput,
|
||||
'type': forms.HiddenInput(attrs={'value':'XX'}),
|
||||
'limit': forms.TextInput(attrs={'value':'0'}),
|
||||
|
|
|
@ -161,6 +161,19 @@ Please fix the following before launching your campaign:
|
|||
<p>Above all, be engaging. The point here is not to tell ungluers everything about your book; it's to remind them why they love it.</p>
|
||||
|
||||
<p>Looking for inspiration? Check out the all-time most-funded projects on crowdfunding sites <a href="http://www.kickstarter.com/discover/most-funded">Kickstarter</a> or <a href="http://www.indiegogo.com/projects?filter_quick=most_funded">IndieGogo</a>, or have a look at <a href="http://www.kickstarter.com/discover/categories/publishing">Kickstarter's Publishing category</a> or <a href="http://www.indiegogo.com/projects?filter_category=Writing">IndieGogo's Writing category</a>.</p>
|
||||
|
||||
<h4>How to add video</h4>
|
||||
<p>We strongly encourage you to include video that communicates directly with your supporters. To add a video:</p>
|
||||
<ul>
|
||||
<li>Upload it to YouTube.</li>
|
||||
<li>Underneath the video, click Share, then Embed.</li>
|
||||
<li>In the embed options: click Use HTTPS.</li>
|
||||
<li>In the Custom sizing area, enter a width of 445.</li>
|
||||
<li>In the editor toolbar below, click Source.</li>
|
||||
<li>Copy/paste the embed code from YouTube into your campaign below.</li>
|
||||
<li>Click Source again to get back to the normal editing mode.</li>
|
||||
</ul>
|
||||
<p>You'll see an IFRAME code in the editor where your video will go. The video will display normally on your campaign page.</p>
|
||||
{{ form.description.errors }}{{ form.description }}
|
||||
<h3>Edition and Rights Details</h3>
|
||||
<p>This will be displayed on the Rights tab for your work. It's the fine print for your campaign. Make sure to disclose any ways the unglued edition will differ from the existing edition; for example:
|
||||
|
@ -185,7 +198,7 @@ Please fix the following before launching your campaign:
|
|||
|
||||
{{ form.target.errors }}${{ form.target }}
|
||||
<h3>License being offered</h3>
|
||||
<p> This is the license you are offering to use once the campaign succeeds. For more information on the licenses you can use, see <a href="http://creativecommons.org/licenses">Creative Commons: About the Licenses</a>. Once your campaign is active, you'll be able to switch to a less restrictive license, but not a more restrictive one.</p>
|
||||
<p> This is the license you are offering to use once the campaign succeeds. For more information on the licenses you can use, see <a href="http://creativecommons.org/licenses">Creative Commons: About the Licenses</a>. Once your campaign is active, you'll be able to switch to a less restrictive license, but not a more restrictive one. We encourage you to pick the least restrictive license you are comfortable with, as this will increase the ways people can use your unglued ebook and motivate more people to donate.</p>
|
||||
{{ form.license.errors }}{{ form.license }}
|
||||
<h3>Ending date</h3>
|
||||
<p> This is the ending date of your campaign. Once you launch the campaign, you won't be able to change it.</p>
|
||||
|
@ -252,18 +265,29 @@ Please fix the following before launching your campaign:
|
|||
</form>
|
||||
</div>
|
||||
<h4>Add a premium for this campaign</h4>
|
||||
<form action="#" method="POST">
|
||||
|
||||
<form action="#" method="POST" id="premium_add">
|
||||
{% csrf_token %}
|
||||
Pledge Amount: {{ premium_form.amount.errors }}${{ premium_form.amount }}<br />
|
||||
Premium Description: {{ premium_form.description.errors }}{{ premium_form.description }}<br />
|
||||
Number Available (0 if no limit): {{ premium_form.limit.errors }}{{ premium_form.limit }}<br />
|
||||
<span class="premium_add_label">Pledge Amount ($2000 maximum):</span> {{ premium_form.amount.errors }}<span class="premium_field_label">$</span>{{ premium_form.amount }}<br /><br />
|
||||
<span class="premium_add_label">Premium Description:</span> {{ premium_form.description.errors }}{{ premium_form.description }}<br /><br />
|
||||
<span class="premium_add_label">Number Available (0 if no limit):</span> {{ premium_form.limit.errors }}{{ premium_form.limit }}<br /><br />
|
||||
{{ premium_form.campaign }}
|
||||
{{ premium_form.type.errors }}{{ premium_form.type }}
|
||||
<br />
|
||||
<input type="submit" name="add_premium" value="Add Premium" />
|
||||
</form>
|
||||
</form><br /><br />
|
||||
|
||||
<h4>Editing premiums</h4>
|
||||
<p>At this time, you can't edit a premium you've created. But you can deactivate a premium and add a new one to replace it. So be sure to double-check your spelling before adding it.</p>
|
||||
|
||||
<p>If you deactivate a premium that people have pledged toward, you are <b>still responsible for delivering it</b> to those pledgers.</p>
|
||||
|
||||
<p>A few things to keep in mind:</p>
|
||||
<ul>
|
||||
<li>Are your premiums cumulative? That is, if you have a $10 and a $25 premium, does the $25 pledger get everything that the $10 pledger gets also? Either cumulative or not-cumulative is fine, but make sure you've communicated clearly</li>
|
||||
<li>Adding new premiums during your campaign is a great way to build momentum. If you do, make sure to leave a comment in the Comments tab of your campaign page to tell supporters (it will be automatically emailed to them). Some of them may want to change (hopefully increase) their pledge to take advantage of it.</li>
|
||||
<li>Also make sure to think about how your new premiums interact with old ones. If you add a new premium at $10, will people who have already pledged $25 be automatically eligible for it or not? Again, you can choose whatever you want; just be sure to communicate clearly.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% ifequal campaign_status 'INITIALIZED' %}
|
||||
|
@ -299,14 +323,14 @@ Please fix the following before launching your campaign:
|
|||
<h3>What to do next</h3>
|
||||
<ul>
|
||||
<li>Tell your friends, relatives, media contacts, professional organizations, social media networks -- everyone!</li>
|
||||
<li>Check in with your campaign frequently. Use comments, description updates, and maybe new custom premiums to spark additional interest, keep supporters engaged, and keep the momentum going.</li>
|
||||
<li>Check in with your campaign frequently. Use comments, description updates, and maybe new premiums to spark additional interest, keep supporters engaged, and keep the momentum going.</li>
|
||||
<li>Watch media and social networks for mentions of your campaign, and engage in those conversations.</li>
|
||||
<li>Need help doing any of this? Talk to us.</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<h3>Campaign Summary</h3>
|
||||
<p> You can see what tiers people have pledged for (or earned) by looking at the <a href="{% url work_acks campaign.work.id %}">sample acknowledgement page</a>.
|
||||
After your campaign succeeds, you can used this page to generate epub code!
|
||||
<h3>Acknowledgements</h3>
|
||||
<p>You can see who your biggest supporters are by looking at the <a href="{% url work_acks campaign.work.id %}">sample acknowledgement page</a>.
|
||||
After your campaign succeeds, you can used this page to generate epub code for the acknowledgements section of your unglued ebook.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -28,17 +28,17 @@
|
|||
<a id="latest"></a><h2>Latest Press</h2>
|
||||
<div class="pressarticles">
|
||||
<div>
|
||||
<a href="http://www.bookbusinessmag.com/article/unstuck-chatting-eric-hellman-book-rights-crowd-sourcing-site-unglueit-creative-commons-passionate-authors-life-after-amazon/1">Unstuck: Chatting with Eric Hellman of book rights crowd-sourcing site Unglue.it about ebooks, the creative commons, passionate authors and life after Amazon</a><br />
|
||||
Book Business - October 3, 2012<br />
|
||||
</div>
|
||||
<div>
|
||||
<a href="http://www.thedigitalshift.com/2012/09/roy-tennant-digital-libraries/first-book-comes-unglued/">First Book Comes Unglued</a><br />
|
||||
The Digital Shift (Library Journal/School Library Journal) - September 12, 2012<br />
|
||||
</div><div>
|
||||
<a href="http://www.guardian.co.uk/books/2012/aug/12/unglueit-resurrect-lost-classics-ebooks">Unglue.it: a crafty new way to resurrect lost classics</a><br />
|
||||
The Guardian - August 12, 2012<br />
|
||||
</div>
|
||||
<div>
|
||||
<a href="http://www.wired.com/business/2012/08/crowdfunding-gets-sticky-for-amazon-startups-payments-halted/">Crowdfunding Gets Tricky for Amazon</a><br />
|
||||
Wired - August 10, 2012<br />
|
||||
</div>
|
||||
<div>
|
||||
<a href="http://paidcontent.org/2012/08/09/ebook-site-unglue-it-hits-bump-after-amazon-ends-crowdfunding-payment-support/">Ebook site Unglue.it hits bump after Amazon ends crowdfunding payment support</a><br />
|
||||
paidContent - August 9, 2012<br />
|
||||
</div></div>
|
||||
|
||||
<a id="overview"></a><h2>Overview</h2>
|
||||
<dl>
|
||||
|
@ -73,6 +73,11 @@ Creative Commons offers a variety of other licenses, many of them with even less
|
|||
</dl>
|
||||
<a id="press"></a><h2>Press Coverage</h2>
|
||||
<div class="pressarticles">
|
||||
<div>
|
||||
<a href="http://live.wsj.com/video/readmill-about-to-make-reading-more-social/79A86D48-6476-4AD9-BFD8-5E54E3E9558D.html#!79A86D48-6476-4AD9-BFD8-5E54E3E9558D">Readmill's About to Make Reading More Social</a><br />
|
||||
Wall Street Journal - September 1, 2011<br />
|
||||
<I>Unglue.it announced a partnership with Readmill on October 5, 2012.</I><br />
|
||||
</div>
|
||||
<div>
|
||||
<a href="http://www.guardian.co.uk/books/2012/aug/12/unglueit-resurrect-lost-classics-ebooks">Unglue.it: a crafty new way to resurrect lost classics</a><br />
|
||||
The Guardian - August 12, 2012<br />
|
||||
|
@ -181,6 +186,10 @@ Creative Commons offers a variety of other licenses, many of them with even less
|
|||
|
||||
<a id="blogs"></a><h2>Blog Coverage (Highlights)</h2>
|
||||
<div class="pressarticles">
|
||||
<div>
|
||||
<a href="http://www.bookbusinessmag.com/article/unstuck-chatting-eric-hellman-book-rights-crowd-sourcing-site-unglueit-creative-commons-passionate-authors-life-after-amazon/1">Unstuck: Chatting with Eric Hellman of book rights crowd-sourcing site Unglue.it about ebooks, the creative commons, passionate authors and life after Amazon</a><br />
|
||||
Book Business - October 3, 2012<br />
|
||||
</div>
|
||||
<div>
|
||||
<a href="http://boingboing.net/2012/09/20/words-on-the-water.html">Words on the Water</a><br />
|
||||
Boing Boing - September 20, 2012<br />
|
||||
|
@ -193,6 +202,10 @@ Creative Commons offers a variety of other licenses, many of them with even less
|
|||
<a href="http://www.thedigitalshift.com/2012/09/roy-tennant-digital-libraries/first-book-comes-unglued/">First Book Comes Unglued</a><br />
|
||||
The Digital Shift (Library Journal/School Library Journal) - September 12, 2012<br />
|
||||
</div>
|
||||
<div>
|
||||
<a href="http://www.crowdfundit.com.au/2012/08/29/giving-ebooks-to-the-world/">Giving ebooks to the world – Unglue.it</a><br />
|
||||
Crowdfund it! - August 29, 2012<br />
|
||||
</div>
|
||||
<div>
|
||||
<a href="http://davidbrin.blogspot.com/2012/07/transparency-secrecy-and-copyright-for.html">Transparency, Secrecy, and Copyright for the Modern Age</a><br />
|
||||
Contrary Brin - July 1, 2012<br />
|
||||
|
|
|
@ -126,45 +126,26 @@ Any questions not covered here? Please email us at <a href="mailto:rights@gluej
|
|||
<h2>How to launch an Unglue.it campaign</h2>
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
In order to launch a campaign, the rights holder must first sign and submit an Unglue.it Platform Services Agreement.
|
||||
Unglue.it staff will review the credentials of the rights holder and enter it into the system, along with an email address and the username for the person who will use the Unglue.it rights holder tools on behalf of the rights holder.
|
||||
If your unglue.it account has been associated with a rights holder, the name and contact address of that rights holder should appear above.
|
||||
</li>
|
||||
<li>
|
||||
The next step for a rights holder is to claim works from the Unglue.it database. Find the work by adding it to you wishlist.
|
||||
On the "details" tab of the work's page, you will find a form that allows you to enter a claim.
|
||||
When you enter a claim, you will be asked to agree to the website terms of use, in which you agree that you're making the claim in good faith, and that you can substantiate that you have legal control over rights to the work.
|
||||
If you claim a work by mistake, please contact Unglue.it rights holder relations immediately: rights@gluejar.com
|
||||
</li>
|
||||
<li>
|
||||
Once a claim is made, it will be reviewed by Unglue.it staff.
|
||||
We'll make sure that the the claim is in order, and we may contact you at {{ request.user.email }} if we have any questions.
|
||||
We may want to look over publishing contracts, etc., to make sure you have sufficient rights to unglue the book.
|
||||
</li>
|
||||
<li>
|
||||
When a claim has been approved, you're ready to open a campaign for the work.
|
||||
A form will appear with the claim listing, above.
|
||||
You'll need to pick one or more managers for the campaign- someone who has an Unglue.it username.
|
||||
The campaign managers will be able to set more parameters for the campaign.
|
||||
</li>
|
||||
<li>
|
||||
If you've been appointed as a campaign manager, a list of campaigns will appear on your rights holder tools page.
|
||||
Click on the link for the campaign you want to manage.
|
||||
</li>
|
||||
<li>
|
||||
On the campaign management page, you'll be able to configure the campaign.
|
||||
When you're ready to launch the campaign, click the launch button.
|
||||
The system will check that the campaign goal and and ending data are within allowed parameters.
|
||||
Once the campaign is launched, you're off and running. Good Luck!
|
||||
</li>
|
||||
{% if not request.user.is_authenticated %}<li>{% else %}<li class="checked">{% endif %}Set up an Unglue.it account (start at the Sign Up button at the top of the page).</li>
|
||||
{% if not request.user.rights_holder.count %}<li>{% else %}<li class="checked">{% endif %}Email rights@gluejar.com about becoming an authorized rights holder.</li>
|
||||
{% if not request.user.rights_holder.count %}<li>{% else %}<li class="checked">{% endif %}After we review your credentials, sign a Platform Services Agreement (available from us).</li>
|
||||
<li>Claim your work(s):</li>
|
||||
<ul>
|
||||
<li>Find them through the search box at the top of every page.</li>
|
||||
<li>Use the Claim option on the Rights tab of each book's page.</li>
|
||||
<li>Agree to our <a href="{{ termsurl }}">Terms</a> on the following page. This includes agreeing that you are making the claim in good faith and can substantiate that you have legal control over worldwide electronic rights to the work.</li>
|
||||
<li>If you have any questions or you claim a work by mistake, email us.</li>
|
||||
<li>We will review your claim. We may contact you at {{ request.user.email }} if we have any questions. If this is the wrong email address, please <a href="/accounts/edit">change the email address</a> for your account.</li>
|
||||
</ul>
|
||||
<li>Once your claim is approved, you can set up a campaign for it. All the campaigns you can manage will be listed on this page.</li>
|
||||
<li>You may optionally add other Unglue.it users as campaign managers, if you'd like them to be able to edit your campaign. That option will also appear on this page.</li>
|
||||
</ol>
|
||||
|
||||
<h2>Rewards</h2>
|
||||
|
||||
<p>Campaigns can have rewards as a way to motivate and thank supporters. You are strongly encouraged to add rewards - they are given special prominence on the campaign page.</p>
|
||||
|
||||
<p>What should you add as rewards? Anything you think you can reasonably deliver that will get supporters excited about the book. For example: other books, whether electronic or physical; artwork or multimedia relating to the book, its author, or its themes; in-person or online chats with the author; memorabilia.</p>
|
||||
<p>What should you add as rewards? Anything (legal) that you think you can reasonably deliver that will get supporters excited about the book. For example: other books, whether electronic or physical; artwork or multimedia relating to the book, its author, or its themes; in-person or online chats with the author; memorabilia.</p>
|
||||
|
||||
<h2>Acknowledgements</h2>
|
||||
<p>Here are the standard acknowledgements. These automatically combine with your rewards. For example, if you offer a $30 reward, ungluers who pledge $30 will receive the $25 acknowledgement as well.</p>
|
||||
|
@ -176,5 +157,5 @@ Any questions not covered here? Please email us at <a href="mailto:rights@gluej
|
|||
</ul>
|
||||
|
||||
<h2>More Questions</h2>
|
||||
<p> Check the FAQ to the left, or <a href="/feedback">Send us feedback.</a>
|
||||
<p> Check the FAQ to the left, or <a href="/feedback">send us feedback.</a>
|
||||
{% endblock %}
|
|
@ -51,6 +51,7 @@ from regluit.frontend.forms import EbookForm, CustomPremiumForm, EditManagersFor
|
|||
from regluit.frontend.forms import getTransferCreditForm, CCForm, CloneCampaignForm
|
||||
from regluit.payment.manager import PaymentManager
|
||||
from regluit.payment.models import Transaction, Account
|
||||
from regluit.payment import baseprocessor
|
||||
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, TRANSACTION_STATUS_MODIFIED
|
||||
from regluit.payment.parameters import PAYMENT_TYPE_AUTHORIZATION, PAYMENT_TYPE_INSTANT
|
||||
from regluit.payment.parameters import PAYMENT_HOST_STRIPE, PAYMENT_HOST_NONE
|
||||
|
@ -789,8 +790,8 @@ class FundPledgeView(FormView):
|
|||
try:
|
||||
account = p.make_account(transaction.user, stripe_token, host=transaction.host)
|
||||
logger.info('account.id: {0}'.format(account.id))
|
||||
except Exception, e:
|
||||
raise e
|
||||
except baseprocessor.ProcessorError as e:
|
||||
return HttpResponse("baseprocessor.ProcessorError: {0}".format(e))
|
||||
|
||||
# GOAL: deactivate any older accounts associated with user
|
||||
|
||||
|
@ -1117,15 +1118,22 @@ def claim(request):
|
|||
form = UserClaimForm(request.user, data=data, prefix='claim')
|
||||
if form.is_valid():
|
||||
# make sure we're not creating a duplicate claim
|
||||
if not models.Claim.objects.filter(work=data['claim-work'], rights_holder=data['claim-rights_holder'], status='pending').count():
|
||||
if not models.Claim.objects.filter(work=form.cleaned_data['work'], rights_holder=form.cleaned_data['rights_holder']).exclude(status='release').count():
|
||||
form.save()
|
||||
return HttpResponseRedirect(reverse('work', kwargs={'work_id': data['claim-work']}))
|
||||
return HttpResponseRedirect(reverse('work', kwargs={'work_id': form.cleaned_data['work'].id}))
|
||||
else:
|
||||
work = models.Work.objects.get(id=data['claim-work'])
|
||||
try:
|
||||
work = models.Work.objects.get(id=data['claim-work'])
|
||||
except models.Work.DoesNotExist:
|
||||
try:
|
||||
work = models.WasWork.objects.get(was = data['claim-work']).work
|
||||
except models.WasWork.DoesNotExist:
|
||||
raise Http404
|
||||
rights_holder = models.RightsHolder.objects.get(id=data['claim-rights_holder'])
|
||||
active_claims = work.claim.exclude(status = 'release')
|
||||
context = {'form': form, 'work': work, 'rights_holder':rights_holder , 'active_claims':active_claims}
|
||||
return render(request, "claim.html", context)
|
||||
|
||||
|
||||
def rh_tools(request):
|
||||
if not request.user.is_authenticated() :
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from regluit.payment.models import PaymentResponse
|
||||
from regluit.payment.models import PaymentResponse
|
||||
|
||||
from django.http import HttpResponseForbidden
|
||||
from datetime import timedelta
|
||||
|
@ -7,6 +7,13 @@ from regluit.utils.localdatetime import now, zuluformat
|
|||
import datetime
|
||||
import time
|
||||
|
||||
class ProcessorError(Exception):
|
||||
"""An abstraction around payment processor exceptions"""
|
||||
def __init__(self, message=None, original_exception=None):
|
||||
super(ProcessorError, self).__init__(message)
|
||||
self.original_exception = original_exception
|
||||
|
||||
|
||||
|
||||
class BasePaymentRequest:
|
||||
'''
|
||||
|
|
|
@ -17,7 +17,7 @@ import stripe
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class StripeError(Exception):
|
||||
class StripelibError(baseprocessor.ProcessorError):
|
||||
pass
|
||||
|
||||
try:
|
||||
|
@ -75,6 +75,8 @@ ERROR_TESTING = dict((
|
|||
('CHARGE_DECLINE', ('4000000000000002', 'Charges with this card will always be declined.'))
|
||||
))
|
||||
|
||||
CARD_FIELDS_TO_COMPARE = ('exp_month', 'exp_year', 'name', 'address_line1', 'address_line2', 'address_zip', 'address_state')
|
||||
|
||||
# types of errors / when they can be handled
|
||||
|
||||
#card_declined: Use this special card number - 4000000000000002.
|
||||
|
@ -89,16 +91,36 @@ def filter_none(d):
|
|||
|
||||
# if you create a Customer object, then you'll be able to charge multiple times. You can create a customer with a token.
|
||||
|
||||
# http://en.wikipedia.org/wiki/Luhn_algorithm#Implementation_of_standard_Mod_10
|
||||
|
||||
def luhn_checksum(card_number):
|
||||
def digits_of(n):
|
||||
return [int(d) for d in str(n)]
|
||||
digits = digits_of(card_number)
|
||||
odd_digits = digits[-1::-2]
|
||||
even_digits = digits[-2::-2]
|
||||
checksum = 0
|
||||
checksum += sum(odd_digits)
|
||||
for d in even_digits:
|
||||
checksum += sum(digits_of(d*2))
|
||||
return checksum % 10
|
||||
|
||||
def is_luhn_valid(card_number):
|
||||
return luhn_checksum(card_number) == 0
|
||||
|
||||
|
||||
# https://stripe.com/docs/tutorials/charges
|
||||
|
||||
def card (number=TEST_CARDS[0][0], exp_month='01', exp_year='2020', cvc=None, name=None,
|
||||
def card (number=TEST_CARDS[0][0], exp_month=1, exp_year=2020, cvc=None, name=None,
|
||||
address_line1=None, address_line2=None, address_zip=None, address_state=None, address_country=None):
|
||||
|
||||
"""Note: there is no place to enter address_city in the API"""
|
||||
|
||||
card = {
|
||||
"number": number,
|
||||
"exp_month": str(exp_month),
|
||||
"exp_year": str(exp_year),
|
||||
"cvc": str(cvc) if cvc is not None else None,
|
||||
"exp_month": int(exp_month),
|
||||
"exp_year": int(exp_year),
|
||||
"cvc": int(cvc) if cvc is not None else None,
|
||||
"name": name,
|
||||
"address_line1": address_line1,
|
||||
"address_line2": address_line2,
|
||||
|
@ -140,7 +162,7 @@ class StripeClient(object):
|
|||
def create_token(self, card):
|
||||
return stripe.Token(api_key=self.api_key).create(card=card)
|
||||
|
||||
def create_customer (self, card=None, description=None, email=None, account_balance=None, plan=None, trial_end=None):
|
||||
def create_customer(self, 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
|
||||
|
||||
|
@ -183,7 +205,6 @@ class StripeClient(object):
|
|||
# https://stripe.com/docs/api?lang=python#list_charges
|
||||
return stripe.Charge(api_key=self.api_key).all(count=count, offset=offset, customer=customer)
|
||||
|
||||
# what to work through?
|
||||
|
||||
# can't test Transfer in test mode: "There are no transfers in test mode."
|
||||
|
||||
|
@ -191,10 +212,6 @@ class StripeClient(object):
|
|||
# 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?
|
||||
|
||||
# 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
|
||||
|
@ -212,6 +229,168 @@ class StripeClient(object):
|
|||
# how to tell whether money transferred to bank account yet
|
||||
# best practices for calling Events -- not too often.
|
||||
|
||||
|
||||
# Errors we still need to catch:
|
||||
#
|
||||
# * invalid_number -- can't get stripe to generate for us. What it means:
|
||||
#
|
||||
# * that the card has been cancelled (or never existed to begin with
|
||||
#
|
||||
# * the card is technically correct (Luhn valid?)
|
||||
#
|
||||
# * the first 6 digits point to a valid bank
|
||||
#
|
||||
# * but the account number (the rest of the digits) doesn't correspond to a credit account with that bank
|
||||
#
|
||||
# * Brian of stripe.com suggests we could treat it the same way as we'd treat card_declined
|
||||
#
|
||||
# * processing_error:
|
||||
#
|
||||
# * means: something went wrong when stripe tried to make the charge (it could be that the card's issuing bank is down, or our connection to the bank isn't working properly)
|
||||
# * we can retry -- e.g., a minute later, then 30 minutes, then an hour, 3 hours, a day.
|
||||
# * we shouldn't see processing_error very often
|
||||
#
|
||||
# * expired_card -- also not easily simulatable in test mode
|
||||
|
||||
|
||||
class StripeErrorTest(TestCase):
|
||||
"""Make sure the exceptions returned by stripe act as expected"""
|
||||
|
||||
def test_cc_test_numbers_luhn_valid(self):
|
||||
"""Show that the test CC numbers supplied for testing as valid numbers are indeed Luhn valid"""
|
||||
self.assertTrue(all([is_luhn_valid(c[0]) for c in ERROR_TESTING.values()]))
|
||||
|
||||
def test_good_token(self):
|
||||
""" verify normal operation """
|
||||
sc = StripeClient()
|
||||
card1 = card(number=TEST_CARDS[0][0], exp_month=1, exp_year='2020', cvc='123', name='Don Giovanni',
|
||||
address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None) # good card
|
||||
token1 = sc.create_token(card=card1)
|
||||
# use the token id -- which is what we get from JavaScript api -- and retrieve the token
|
||||
token2 = sc.token.retrieve(id=token1.id)
|
||||
self.assertEqual(token2.id, token1.id)
|
||||
# make sure token id has a form tok_
|
||||
self.assertEqual(token2.id[:4], "tok_")
|
||||
|
||||
# should be only test mode
|
||||
self.assertEqual(token2.livemode, False)
|
||||
# token hasn't been used yet
|
||||
self.assertEqual(token2.used, False)
|
||||
# test that card info matches up with what was fed in.
|
||||
for k in CARD_FIELDS_TO_COMPARE:
|
||||
self.assertEqual(token2.card[k], card1[k])
|
||||
# last4
|
||||
self.assertEqual(token2.card.last4, TEST_CARDS[0][0][-4:])
|
||||
# fingerprint
|
||||
self.assertGreaterEqual(len(token2.card.fingerprint), 16)
|
||||
|
||||
# now charge the token
|
||||
charge1 = sc.create_charge(10, 'usd', card=token2.id)
|
||||
self.assertEqual(charge1.amount, 1000)
|
||||
self.assertEqual(charge1.id[:3], "ch_")
|
||||
# disputed, failure_message, fee, fee_details
|
||||
self.assertEqual(charge1.disputed,False)
|
||||
self.assertEqual(charge1.failure_message,None)
|
||||
self.assertEqual(charge1.fee,59)
|
||||
self.assertEqual(charge1.refunded,False)
|
||||
|
||||
|
||||
def test_error_creating_customer_with_declined_card(self):
|
||||
"""Test whether we can get a charge decline error"""
|
||||
sc = StripeClient()
|
||||
card1 = card(number=ERROR_TESTING['CHARGE_DECLINE'][0])
|
||||
try:
|
||||
cust1 = sc.create_customer(card=card1, description="This card should fail")
|
||||
self.fail("Attempt to create customer did not throw expected exception.")
|
||||
except stripe.CardError as e:
|
||||
self.assertEqual(e.code, "card_declined")
|
||||
self.assertEqual(e.message, "Your card was declined.")
|
||||
|
||||
def test_charge_bad_cust(self):
|
||||
# expect the card to be declined -- and for us to get CardError
|
||||
sc = StripeClient()
|
||||
# bad card
|
||||
card1 = card(number=ERROR_TESTING['BAD_ATTACHED_CARD'][0])
|
||||
# attaching card should be ok
|
||||
cust1 = sc.create_customer(card=card1, description="test bad customer", email="rdhyee@gluejar.com")
|
||||
# trying to charge the card should fail
|
||||
self.assertRaises(stripe.CardError, sc.create_charge, 10,
|
||||
customer = cust1.id, description="$10 for bad cust")
|
||||
|
||||
def test_bad_cc_number(self):
|
||||
"""send a bad cc and should get an error when trying to create a token"""
|
||||
BAD_CC_NUM = '4242424242424241'
|
||||
|
||||
# reason for decline is number is not Luhn valid
|
||||
self.assertFalse(is_luhn_valid(BAD_CC_NUM))
|
||||
|
||||
sc = StripeClient()
|
||||
card1 = card(number=BAD_CC_NUM, exp_month=1, exp_year=2020, cvc='123', name='Don Giovanni',
|
||||
address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None) # good card
|
||||
|
||||
try:
|
||||
token1 = sc.create_token(card=card1)
|
||||
self.fail("Attempt to create token with bad cc number did not throw expected exception.")
|
||||
except stripe.CardError as e:
|
||||
self.assertEqual(e.code, "incorrect_number")
|
||||
self.assertEqual(e.message, "Your card number is incorrect")
|
||||
|
||||
def test_invalid_expiry_month(self):
|
||||
"""Use an invalid month e.g. 13."""
|
||||
|
||||
sc = StripeClient()
|
||||
card1 = card(number=TEST_CARDS[0][0], exp_month=13, exp_year=2020, cvc='123', name='Don Giovanni',
|
||||
address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None)
|
||||
|
||||
try:
|
||||
token1 = sc.create_token(card=card1)
|
||||
self.fail("Attempt to create token with invalid expiry month did not throw expected exception.")
|
||||
except stripe.CardError as e:
|
||||
self.assertEqual(e.code, "invalid_expiry_month")
|
||||
self.assertEqual(e.message, "Your card's expiration month is invalid")
|
||||
|
||||
def test_invalid_expiry_year(self):
|
||||
"""Use a year in the past e.g. 1970."""
|
||||
|
||||
sc = StripeClient()
|
||||
card1 = card(number=TEST_CARDS[0][0], exp_month=12, exp_year=1970, cvc='123', name='Don Giovanni',
|
||||
address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None)
|
||||
|
||||
try:
|
||||
token1 = sc.create_token(card=card1)
|
||||
self.fail("Attempt to create token with invalid expiry year did not throw expected exception.")
|
||||
except stripe.CardError as e:
|
||||
self.assertEqual(e.code, "invalid_expiry_year")
|
||||
self.assertEqual(e.message, "Your card's expiration year is invalid")
|
||||
|
||||
def test_invalid_cvc(self):
|
||||
"""Use a two digit number e.g. 99."""
|
||||
|
||||
sc = StripeClient()
|
||||
card1 = card(number=TEST_CARDS[0][0], exp_month=12, exp_year=2020, cvc='99', name='Don Giovanni',
|
||||
address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None)
|
||||
|
||||
try:
|
||||
token1 = sc.create_token(card=card1)
|
||||
self.fail("Attempt to create token with invalid cvc did not throw expected exception.")
|
||||
except stripe.CardError as e:
|
||||
self.assertEqual(e.code, "invalid_cvc")
|
||||
self.assertEqual(e.message, "Your card's security code is invalid")
|
||||
|
||||
def test_missing_card(self):
|
||||
"""There is no card on a customer that is being charged"""
|
||||
|
||||
sc = StripeClient()
|
||||
# create a Customer with no attached card
|
||||
cust1 = sc.create_customer(description="test cust w/ no card")
|
||||
try:
|
||||
sc.create_charge(10, customer = cust1.id, description="$10 for cust w/ no card")
|
||||
except stripe.CardError as e:
|
||||
self.assertEqual(e.code, "missing")
|
||||
self.assertEqual(e.message, "Cannot charge a customer that has no active card")
|
||||
|
||||
|
||||
|
||||
class PledgeScenarioTest(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
@ -244,7 +423,6 @@ class PledgeScenarioTest(TestCase):
|
|||
self.assertRaises(stripe.CardError, self._sc.create_charge, 10,
|
||||
customer = self._cust_bad_card.id, description="$10 for bad cust")
|
||||
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# clean up stuff we create in test -- right now list current objects
|
||||
|
@ -273,8 +451,12 @@ class Processor(baseprocessor.Processor):
|
|||
sc = StripeClient()
|
||||
|
||||
# create customer and charge id and then charge the customer
|
||||
customer = sc.create_customer(card=token, description=user.username,
|
||||
try:
|
||||
customer = sc.create_customer(card=token, description=user.username,
|
||||
email=user.email)
|
||||
except stripe.StripeError as e:
|
||||
raise StripelibError(e.message, e)
|
||||
|
||||
|
||||
account = Account(host = PAYMENT_HOST_STRIPE,
|
||||
account_id = customer.id,
|
||||
|
@ -322,7 +504,7 @@ class Processor(baseprocessor.Processor):
|
|||
logger.warning("user {0} has more than one active payment account".format(transaction.user))
|
||||
elif transaction.user.account_set.filter(date_deactivated__isnull=True).count() == 0:
|
||||
logger.warning("user {0} has no active payment account".format(transaction.user))
|
||||
raise StripeError("user {0} has no active payment account".format(transaction.user))
|
||||
raise StripelibError("user {0} has no active payment account".format(transaction.user))
|
||||
|
||||
account = transaction.user.account_set.filter(date_deactivated__isnull=True)[0]
|
||||
logger.info("user: {0} customer.id is {1}".format(transaction.user, account.account_id))
|
||||
|
@ -368,11 +550,18 @@ class Processor(baseprocessor.Processor):
|
|||
# is it a customer or a token?
|
||||
|
||||
# BUGBUG: replace description with somethin more useful
|
||||
# TO DO: rewrapping StripeError to StripelibError -- but maybe we should be more specific
|
||||
if transaction.preapproval_key.startswith('cus_'):
|
||||
charge = sc.create_charge(transaction.amount, customer=transaction.preapproval_key, description="${0} for test / retain cc".format(transaction.amount))
|
||||
try:
|
||||
charge = sc.create_charge(transaction.amount, customer=transaction.preapproval_key, description="${0} for test / retain cc".format(transaction.amount))
|
||||
except stripe.StripeError as e:
|
||||
raise StripelibError(e.message, e)
|
||||
elif transaction.preapproval_key.startswith('tok_'):
|
||||
charge = sc.create_charge(transaction.amount, card=transaction.preapproval_key, description="${0} for test / cc not retained".format(transaction.amount))
|
||||
|
||||
try:
|
||||
charge = sc.create_charge(transaction.amount, card=transaction.preapproval_key, description="${0} for test / cc not retained".format(transaction.amount))
|
||||
except stripe.StripeError as e:
|
||||
raise StripelibError(e.message, e)
|
||||
|
||||
transaction.status = TRANSACTION_STATUS_COMPLETE
|
||||
transaction.pay_key = charge.id
|
||||
transaction.date_payment = now()
|
||||
|
@ -415,8 +604,8 @@ class Processor(baseprocessor.Processor):
|
|||
|
||||
def suite():
|
||||
|
||||
testcases = [PledgeScenarioTest]
|
||||
#testcases = []
|
||||
#testcases = [PledgeScenarioTest, StripeErrorTest]
|
||||
testcases = [StripeErrorTest]
|
||||
suites = unittest.TestSuite([unittest.TestLoader().loadTestsFromTestCase(testcase) for testcase in testcases])
|
||||
#suites.addTest(LibraryThingTest('test_cache'))
|
||||
#suites.addTest(SettingsTest('test_dev_me_alignment')) # give option to test this alignment
|
||||
|
|
|
@ -275,3 +275,25 @@ input[name="launch"] {
|
|||
#launchme {
|
||||
margin: 15px auto;
|
||||
}
|
||||
#premium_add span,
|
||||
#premium_add input[type="text"],
|
||||
#premium_add textarea {
|
||||
float: left;
|
||||
}
|
||||
#premium_add input[type="submit"] {
|
||||
float: right;
|
||||
}
|
||||
#premium_add input[type="text"] {
|
||||
width: 33%;
|
||||
}
|
||||
#premium_add textarea {
|
||||
width: 60%;
|
||||
}
|
||||
#premium_add .premium_add_label {
|
||||
width: 30%;
|
||||
margin-right: 2%;
|
||||
}
|
||||
#premium_add .premium_field_label {
|
||||
width: 1%;
|
||||
margin-left: -1%;
|
||||
}
|
||||
|
|
|
@ -1070,3 +1070,9 @@ a.nounderline {
|
|||
width: 480px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
li.checked {
|
||||
list-style-type: none;
|
||||
background: transparent url(/static/images/checkmark_small.png) no-repeat 0 0;
|
||||
margin-left: -20px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
|
|
@ -13,4 +13,32 @@ input[name="launch"] {
|
|||
|
||||
#launchme {
|
||||
margin: 15px auto;
|
||||
}
|
||||
|
||||
#premium_add {
|
||||
span, input[type="text"], textarea {
|
||||
float: left;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
float: right;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.premium_add_label {
|
||||
width: 30%;
|
||||
margin-right: 2%;
|
||||
}
|
||||
|
||||
.premium_field_label {
|
||||
width: 1%;
|
||||
margin-left: -1%;
|
||||
}
|
||||
}
|
|
@ -697,4 +697,11 @@ a.nounderline {
|
|||
.central {
|
||||
width: 480px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
li.checked {
|
||||
list-style-type:none;
|
||||
background:transparent url(/static/images/checkmark_small.png) no-repeat 0 0;
|
||||
margin-left: -20px;
|
||||
padding-left: 20px;
|
||||
}
|
Loading…
Reference in New Issue