diff --git a/core/bookloader.py b/core/bookloader.py index fb0b16a3..4104af30 100755 --- a/core/bookloader.py +++ b/core/bookloader.py @@ -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) diff --git a/frontend/forms.py b/frontend/forms.py index 9a4161b5..f2d999a2 100644 --- a/frontend/forms.py +++ b/frontend/forms.py @@ -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'}), diff --git a/frontend/templates/manage_campaign.html b/frontend/templates/manage_campaign.html index c515c93c..4b9c2caa 100644 --- a/frontend/templates/manage_campaign.html +++ b/frontend/templates/manage_campaign.html @@ -161,6 +161,19 @@ Please fix the following before launching your campaign:

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.

Looking for inspiration? Check out the all-time most-funded projects on crowdfunding sites Kickstarter or IndieGogo, or have a look at Kickstarter's Publishing category or IndieGogo's Writing category.

+ +

How to add video

+

We strongly encourage you to include video that communicates directly with your supporters. To add a video:

+ +

You'll see an IFRAME code in the editor where your video will go. The video will display normally on your campaign page.

{{ form.description.errors }}{{ form.description }}

Edition and Rights Details

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

License being offered

-

This is the license you are offering to use once the campaign succeeds. For more information on the licenses you can use, see Creative Commons: About the Licenses. Once your campaign is active, you'll be able to switch to a less restrictive license, but not a more restrictive one.

+

This is the license you are offering to use once the campaign succeeds. For more information on the licenses you can use, see Creative Commons: About the Licenses. 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.

{{ form.license.errors }}{{ form.license }}

Ending date

This is the ending date of your campaign. Once you launch the campaign, you won't be able to change it.

@@ -252,18 +265,29 @@ Please fix the following before launching your campaign:

Add a premium for this campaign

-
+ + {% csrf_token %} - Pledge Amount: {{ premium_form.amount.errors }}${{ premium_form.amount }}
- Premium Description: {{ premium_form.description.errors }}{{ premium_form.description }}
- Number Available (0 if no limit): {{ premium_form.limit.errors }}{{ premium_form.limit }}
+ Pledge Amount ($2000 maximum): {{ premium_form.amount.errors }}${{ premium_form.amount }}

+ Premium Description: {{ premium_form.description.errors }}{{ premium_form.description }}

+ Number Available (0 if no limit): {{ premium_form.limit.errors }}{{ premium_form.limit }}

{{ premium_form.campaign }} {{ premium_form.type.errors }}{{ premium_form.type }}
-
+

+

Editing premiums

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.

+ +

If you deactivate a premium that people have pledged toward, you are still responsible for delivering it to those pledgers.

+ +

A few things to keep in mind:

+ {% ifequal campaign_status 'INITIALIZED' %} @@ -299,14 +323,14 @@ Please fix the following before launching your campaign:

What to do next

{% endif %} -

Campaign Summary

-

You can see what tiers people have pledged for (or earned) by looking at the sample acknowledgement page. -After your campaign succeeds, you can used this page to generate epub code! +

Acknowledgements

+

You can see who your biggest supporters are by looking at the sample acknowledgement page. +After your campaign succeeds, you can used this page to generate epub code for the acknowledgements section of your unglued ebook.

{% endif %} diff --git a/frontend/templates/press.html b/frontend/templates/press.html index 6744c1cc..bd656cb6 100644 --- a/frontend/templates/press.html +++ b/frontend/templates/press.html @@ -28,17 +28,17 @@

Latest Press

+ Unstuck: Chatting with Eric Hellman of book rights crowd-sourcing site Unglue.it about ebooks, the creative commons, passionate authors and life after Amazon
+ Book Business - October 3, 2012
+
+
+ First Book Comes Unglued
+ The Digital Shift (Library Journal/School Library Journal) - September 12, 2012
+
Unglue.it: a crafty new way to resurrect lost classics
The Guardian - August 12, 2012
-
- Crowdfunding Gets Tricky for Amazon
- Wired - August 10, 2012
-
- Ebook site Unglue.it hits bump after Amazon ends crowdfunding payment support
- paidContent - August 9, 2012
-

Overview

@@ -73,6 +73,11 @@ Creative Commons offers a variety of other licenses, many of them with even less

Press Coverage

+
+ Readmill's About to Make Reading More Social
+ Wall Street Journal - September 1, 2011
+ Unglue.it announced a partnership with Readmill on October 5, 2012.
+
Unglue.it: a crafty new way to resurrect lost classics
The Guardian - August 12, 2012
@@ -181,6 +186,10 @@ Creative Commons offers a variety of other licenses, many of them with even less

Blog Coverage (Highlights)

+
+ Unstuck: Chatting with Eric Hellman of book rights crowd-sourcing site Unglue.it about ebooks, the creative commons, passionate authors and life after Amazon
+ Book Business - October 3, 2012
+
Words on the Water
Boing Boing - September 20, 2012
@@ -193,6 +202,10 @@ Creative Commons offers a variety of other licenses, many of them with even less First Book Comes Unglued
The Digital Shift (Library Journal/School Library Journal) - September 12, 2012
+
+ Giving ebooks to the world – Unglue.it
+ Crowdfund it! - August 29, 2012
+
Transparency, Secrecy, and Copyright for the Modern Age
Contrary Brin - July 1, 2012
diff --git a/frontend/templates/rh_tools.html b/frontend/templates/rh_tools.html index 37daada2..f741164d 100644 --- a/frontend/templates/rh_tools.html +++ b/frontend/templates/rh_tools.html @@ -126,45 +126,26 @@ Any questions not covered here? Please email us at -
  • - 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. -
  • -
  • - 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. -
  • -
  • - 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. -
  • -
  • - 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! -
  • + {% if not request.user.is_authenticated %}
  • {% else %}
  • {% endif %}Set up an Unglue.it account (start at the Sign Up button at the top of the page).
  • + {% if not request.user.rights_holder.count %}
  • {% else %}
  • {% endif %}Email rights@gluejar.com about becoming an authorized rights holder.
  • + {% if not request.user.rights_holder.count %}
  • {% else %}
  • {% endif %}After we review your credentials, sign a Platform Services Agreement (available from us).
  • +
  • Claim your work(s):
  • +
    +
  • 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.
  • +
  • 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.
  • Rewards

    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.

    -

    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.

    +

    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.

    Acknowledgements

    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.

    @@ -176,5 +157,5 @@ Any questions not covered here? Please email us at Send us feedback. +

    Check the FAQ to the left, or send us feedback. {% endblock %} \ No newline at end of file diff --git a/frontend/views.py b/frontend/views.py index 0153a444..ae93b5c8 100755 --- a/frontend/views.py +++ b/frontend/views.py @@ -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() : diff --git a/payment/baseprocessor.py b/payment/baseprocessor.py index b166a8c5..8f9ac9d5 100644 --- a/payment/baseprocessor.py +++ b/payment/baseprocessor.py @@ -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: ''' diff --git a/payment/stripelib.py b/payment/stripelib.py index 87050e7a..8a701a88 100644 --- a/payment/stripelib.py +++ b/payment/stripelib.py @@ -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 diff --git a/static/css/manage_campaign.css b/static/css/manage_campaign.css index 79ba1555..e20cca42 100644 --- a/static/css/manage_campaign.css +++ b/static/css/manage_campaign.css @@ -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%; +} diff --git a/static/css/sitewide.css b/static/css/sitewide.css index 1d8c4a83..f210efcb 100644 --- a/static/css/sitewide.css +++ b/static/css/sitewide.css @@ -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; +} diff --git a/static/less/manage_campaign.less b/static/less/manage_campaign.less index 35b3a69a..075d2461 100644 --- a/static/less/manage_campaign.less +++ b/static/less/manage_campaign.less @@ -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%; + } } \ No newline at end of file diff --git a/static/less/sitewide.less b/static/less/sitewide.less index 929a27c1..ad360d72 100644 --- a/static/less/sitewide.less +++ b/static/less/sitewide.less @@ -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; } \ No newline at end of file