From f526d9613354abc77044d5fdac8c2beceb7dc887 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Thu, 21 May 2015 07:34:46 -0400 Subject: [PATCH 1/8] Rename payments.js to routes.js I want to use payments.js to replace tips.js. --- js/gratipay/{payments.js => routes.js} | 74 ++++++++++---------- www/~/%username/routes/bank-account.html.spt | 2 +- www/~/%username/routes/credit-card.html.spt | 2 +- www/~/%username/routes/paypal.html.spt | 2 +- 4 files changed, 40 insertions(+), 40 deletions(-) rename js/gratipay/{payments.js => routes.js} (79%) diff --git a/js/gratipay/payments.js b/js/gratipay/routes.js similarity index 79% rename from js/gratipay/payments.js rename to js/gratipay/routes.js index 8731e40d6e..809a77b493 100644 --- a/js/gratipay/payments.js +++ b/js/gratipay/routes.js @@ -1,29 +1,28 @@ -/* Bank Account and Credit Card forms +/* Payment route forms * - * These two forms share some common wiring under the Gratipay.payments - * namespace, and each has unique code under the Gratipay.payments.{cc,ba} - * namespaces. Each form gets its own page so we only instantiate one of these - * at a time. + * These forms share some common wiring under the Gratipay.routes namespace, + * and each has unique code under the Gratipay.routes.{cc,ba,pp} namespaces. + * Each form gets its own page, so we only instantiate one of these at a time. * */ -Gratipay.payments = {}; +Gratipay.routes = {}; // Common code // =========== -Gratipay.payments.init = function() { - $('#delete').submit(Gratipay.payments.deleteRoute); +Gratipay.routes.init = function() { + $('#delete').submit(Gratipay.routes.deleteRoute); } -Gratipay.payments.lazyLoad = function(script_url) { +Gratipay.routes.lazyLoad = function(script_url) { jQuery.getScript(script_url, function() { $('input[type!="hidden"]').eq(0).focus(); }).fail(Gratipay.error); } -Gratipay.payments.deleteRoute = function(e) { +Gratipay.routes.deleteRoute = function(e) { e.stopPropagation(); e.preventDefault(); @@ -43,18 +42,18 @@ Gratipay.payments.deleteRoute = function(e) { return false; }; -Gratipay.payments.onSuccess = function(data) { +Gratipay.routes.onSuccess = function(data) { $('button#save').prop('disabled', false); window.location.reload(); }; -Gratipay.payments.associate = function (network, address) { +Gratipay.routes.associate = function (network, address) { jQuery.ajax({ url: "associate.json", type: "POST", data: {network: network, address: address}, dataType: "json", - success: Gratipay.payments.onSuccess, + success: Gratipay.routes.onSuccess, error: [ Gratipay.error, function() { $('button#save').prop('disabled', false); }, @@ -66,18 +65,18 @@ Gratipay.payments.associate = function (network, address) { // Bank Accounts // ============= -Gratipay.payments.ba = {}; +Gratipay.routes.ba = {}; -Gratipay.payments.ba.init = function() { - Gratipay.payments.init(); +Gratipay.routes.ba.init = function() { + Gratipay.routes.init(); // Lazily depend on Balanced. - Gratipay.payments.lazyLoad("https://js.balancedpayments.com/1.1/balanced.min.js") + Gratipay.routes.lazyLoad("https://js.balancedroutes.com/1.1/balanced.min.js") - $('form#bank-account').submit(Gratipay.payments.ba.submit); + $('form#bank-account').submit(Gratipay.routes.ba.submit); }; -Gratipay.payments.ba.submit = function (e) { +Gratipay.routes.ba.submit = function (e) { e.preventDefault(); $('button#save').prop('disabled', true); @@ -103,17 +102,17 @@ Gratipay.payments.ba.submit = function (e) { // Okay, send the data to Balanced. balanced.bankAccount.create(bankAccount, function (response) { if (response.status_code !== 201) { - return Gratipay.payments.ba.onError(response); + return Gratipay.routes.ba.onError(response); } /* The request to tokenize the thing succeeded. Now we need to associate it * to the Customer on Balanced and to the participant in our DB. */ - Gratipay.payments.associate('balanced-ba', response.bank_accounts[0].href); + Gratipay.routes.associate('balanced-ba', response.bank_accounts[0].href); }); }; -Gratipay.payments.ba.onError = function(response) { +Gratipay.routes.ba.onError = function(response) { $('button#save').prop('disabled', false); var msg = response.status_code + ": " + $.map(response.errors, function(obj) { return obj.description }).join(', '); @@ -125,16 +124,16 @@ Gratipay.payments.ba.onError = function(response) { // Credit Cards // ============ -Gratipay.payments.cc = {}; +Gratipay.routes.cc = {}; -Gratipay.payments.cc.init = function() { - Gratipay.payments.init(); +Gratipay.routes.cc.init = function() { + Gratipay.routes.init(); // Lazily depend on Braintree. - Gratipay.payments.lazyLoad("https://js.braintreegateway.com/v2/braintree.js") + Gratipay.routes.lazyLoad("https://js.braintreegateway.com/v2/braintree.js") - $('form#credit-card').submit(Gratipay.payments.cc.submit); - Gratipay.payments.cc.formatInputs( + $('form#credit-card').submit(Gratipay.routes.cc.submit); + Gratipay.routes.cc.formatInputs( $('#card_number'), $('#expiration_month'), $('#expiration_year'), @@ -145,7 +144,7 @@ Gratipay.payments.cc.init = function() { /* Most of the following code is taken from https://github.com/wangjohn/creditly */ -Gratipay.payments.cc.formatInputs = function (cardNumberInput, expirationMonthInput, expirationYearInput, cvvInput) { +Gratipay.routes.cc.formatInputs = function (cardNumberInput, expirationMonthInput, expirationYearInput, cvvInput) { function getInputValue(e, element) { var inputValue = element.val().trim(); inputValue = inputValue + String.fromCharCode(e.which); @@ -266,7 +265,7 @@ Gratipay.payments.cc.formatInputs = function (cardNumberInput, expirationMonthIn }); } -Gratipay.payments.cc.submit = function(e) { +Gratipay.routes.cc.submit = function(e) { e.stopPropagation(); e.preventDefault(); @@ -297,28 +296,29 @@ Gratipay.payments.cc.submit = function(e) { if (err) { Gratipay.notification(err, 'error') } else { - Gratipay.payments.associate('braintree-cc', nonce); + Gratipay.routes.associate('braintree-cc', nonce); } }); return false; }; + // PayPal // ====== -Gratipay.payments.pp = {}; +Gratipay.routes.pp = {}; -Gratipay.payments.pp.init = function () { - Gratipay.payments.init(); - $('form#paypal').submit(Gratipay.payments.pp.submit); +Gratipay.routes.pp.init = function () { + Gratipay.routes.init(); + $('form#paypal').submit(Gratipay.routes.pp.submit); } -Gratipay.payments.pp.submit = function (e) { +Gratipay.routes.pp.submit = function (e) { e.stopPropagation(); e.preventDefault(); $('button#save').prop('disabled', true); var paypal_email = $('form#paypal #email').val(); - Gratipay.payments.associate('paypal', paypal_email); + Gratipay.routes.associate('paypal', paypal_email); } diff --git a/www/~/%username/routes/bank-account.html.spt b/www/~/%username/routes/bank-account.html.spt index 42baaa4ec8..3be4475610 100644 --- a/www/~/%username/routes/bank-account.html.spt +++ b/www/~/%username/routes/bank-account.html.spt @@ -45,7 +45,7 @@ title = _("Bank Account") {% if not user.ANON %} {% endif %} diff --git a/www/~/%username/routes/credit-card.html.spt b/www/~/%username/routes/credit-card.html.spt index dbc1be9584..c163256a28 100644 --- a/www/~/%username/routes/credit-card.html.spt +++ b/www/~/%username/routes/credit-card.html.spt @@ -55,7 +55,7 @@ title = _("Credit Card") {% if not user.ANON %} diff --git a/www/~/%username/routes/paypal.html.spt b/www/~/%username/routes/paypal.html.spt index e7770e24b2..2af0a3e87f 100644 --- a/www/~/%username/routes/paypal.html.spt +++ b/www/~/%username/routes/paypal.html.spt @@ -39,7 +39,7 @@ title = _("PayPal Account") {% if not user.ANON %} {% endif %} From 3dbec8776f453b2f04fadb7974abd49ecf4bb6d1 Mon Sep 17 00:00:00 2001 From: Rohit Paul Kuruvilla Date: Sat, 16 May 2015 18:37:14 +0530 Subject: [PATCH 2/8] Fix Participant#update_giving --- gratipay/models/participant.py | 55 ++++++++-------------------------- tests/py/test_participant.py | 47 +++++++++++++---------------- 2 files changed, 33 insertions(+), 69 deletions(-) diff --git a/gratipay/models/participant.py b/gratipay/models/participant.py index 650154b50c..731a64f9f1 100644 --- a/gratipay/models/participant.py +++ b/gratipay/models/participant.py @@ -1022,60 +1022,29 @@ def update_giving_and_tippees(self): Participant.from_username(tip.tippee).update_receiving(cursor) def update_giving(self, cursor=None): + updated = [] # Update is_funded on tips if self.get_credit_card_error() == '': updated = (cursor or self.db).all(""" - UPDATE current_tips + UPDATE current_subscriptions SET is_funded = true - WHERE tipper = %s + WHERE subscriber = %s AND is_funded IS NOT true RETURNING * """, (self.username,)) - else: - tips = (cursor or self.db).all(""" - SELECT t.* - FROM current_tips t - JOIN participants p2 ON p2.username = t.tippee - WHERE t.tipper = %s - AND t.amount > 0 - AND p2.is_suspicious IS NOT true - ORDER BY p2.claimed_time IS NULL, t.ctime ASC - """, (self.username,)) - fake_balance = self.balance + self.receiving - updated = [] - for tip in tips: - if tip.amount > fake_balance: - is_funded = False - else: - fake_balance -= tip.amount - is_funded = True - if tip.is_funded == is_funded: - continue - updated.append((cursor or self.db).one(""" - UPDATE tips - SET is_funded = %s - WHERE id = %s - RETURNING * - """, (is_funded, tip.id))) - - # Update giving on participant + giving = (cursor or self.db).one(""" - WITH our_tips AS ( - SELECT amount, p2.claimed_time - FROM current_tips - JOIN participants p2 ON p2.username = tippee - WHERE tipper = %(username)s - AND p2.is_suspicious IS NOT true - AND amount > 0 - AND is_funded - ) UPDATE participants p SET giving = COALESCE(( - SELECT sum(amount) - FROM our_tips - WHERE claimed_time IS NOT NULL + SELECT sum(amount) + FROM current_subscriptions s + JOIN teams t ON t.slug=s.team + WHERE subscriber=%(username)s + AND amount > 0 + AND is_funded + AND t.is_approved ), 0) - WHERE p.username = %(username)s + WHERE p.username=%(username)s RETURNING giving """, dict(username=self.username)) self.set_attributes(giving=giving) diff --git a/tests/py/test_participant.py b/tests/py/test_participant.py index f7c0c6e5f0..83881cee95 100644 --- a/tests/py/test_participant.py +++ b/tests/py/test_participant.py @@ -602,37 +602,32 @@ def test_only_funded_tips_count(self): alice = self.make_participant('alice', claimed_time='now', last_bill_result='') bob = self.make_participant('bob', claimed_time='now') carl = self.make_participant('carl', claimed_time='now', last_bill_result="Fail!") - dana = self.make_participant('dana', claimed_time='now') - alice.set_tip_to(dana, '3.00') - alice.set_tip_to(bob, '6.00') - bob.set_tip_to(alice, '5.00') - bob.set_tip_to(dana, '2.00') - carl.set_tip_to(dana, '2.08') - - assert alice.giving == Decimal('9.00') - assert alice.receiving == Decimal('5.00') - assert bob.giving == Decimal('5.00') - assert bob.receiving == Decimal('6.00') + team = self.make_team(is_approved=True) + + alice.set_subscription_to(team, '3.00') # The only funded tip + bob.set_subscription_to(team, '5.00') + carl.set_subscription_to(team, '7.00') + + # TODO - Add team payroll and check receiving values + + assert alice.giving == Decimal('3.00') + assert bob.giving == Decimal('0.00') assert carl.giving == Decimal('0.00') - assert carl.receiving == Decimal('0.00') - assert dana.receiving == Decimal('3.00') - assert dana.npatrons == 1 - funded_tips = self.db.all("SELECT amount FROM tips WHERE is_funded ORDER BY id") - assert funded_tips == [3, 6, 5] + funded_tip = self.db.one("SELECT * FROM subscriptions WHERE is_funded ORDER BY id") + assert funded_tip.subscriber == alice.username def test_only_latest_tip_counts(self): alice = self.make_participant('alice', claimed_time='now', last_bill_result='') - bob = self.make_participant('bob', claimed_time='now', last_bill_result='') - carl = self.make_participant('carl', claimed_time='now') - alice.set_tip_to(carl, '12.00') - alice.set_tip_to(carl, '3.00') - bob.set_tip_to(carl, '2.00') - bob.set_tip_to(carl, '0.00') - assert alice.giving == Decimal('3.00') - assert bob.giving == Decimal('0.00') - assert carl.receiving == Decimal('3.00') - assert carl.npatrons == 1 + team = self.make_team(is_approved=True) + + alice.set_subscription_to(team, '12.00') + alice.set_subscription_to(team, '4.00') + + # TODO - Add team payroll and check receiving values + + assert alice.giving == Decimal('4.00') + def test_receiving_includes_tips_from_whitelisted_accounts(self): alice = self.make_participant( 'alice' From d1fd4c136e219848b623f9d53fa52b448b831bd3 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Thu, 21 May 2015 08:34:00 -0400 Subject: [PATCH 3/8] Bring back tip.json as subscription.json --- tests/py/test_subscription_json.py | 68 +++++++++++++++++++++++++++ tests/py/test_tip_json.py | 74 ------------------------------ www/%team/subscription.json.spt | 63 +++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 74 deletions(-) create mode 100644 tests/py/test_subscription_json.py delete mode 100644 tests/py/test_tip_json.py create mode 100644 www/%team/subscription.json.spt diff --git a/tests/py/test_subscription_json.py b/tests/py/test_subscription_json.py new file mode 100644 index 0000000000..39b8a407a5 --- /dev/null +++ b/tests/py/test_subscription_json.py @@ -0,0 +1,68 @@ +from __future__ import print_function, unicode_literals + +import json + +from aspen.utils import utcnow +from gratipay.testing import Harness + + +class TestTipJson(Harness): + + def test_api_returns_amount_and_totals(self): + "Test that we get correct amounts and totals back on POSTs to subscription.json" + + # First, create some test data + # We need accounts + now = utcnow() + self.make_team("A", is_approved=True) + self.make_team("B", is_approved=True) + self.make_participant("alice", claimed_time=now, last_bill_result='') + + # Then, add a $1.50 and $3.00 subscription + response1 = self.client.POST( "/A/subscription.json" + , {'amount': "1.50"} + , auth_as='alice' + ) + + response2 = self.client.POST( "/B/subscription.json" + , {'amount': "3.00"} + , auth_as='alice' + ) + + # Confirm we get back the right amounts. + first_data = json.loads(response1.body) + second_data = json.loads(response2.body) + assert first_data['amount'] == "1.50" + assert first_data['total_giving'] == "1.50" + assert second_data['amount'] == "3.00" + assert second_data['total_giving'] == "4.50" + + + def test_setting_subscription_out_of_range_gets_bad_amount(self): + self.make_team(is_approved=True) + self.make_participant("alice", claimed_time='now', last_bill_result='') + + response = self.client.PxST( "/TheATeam/subscription.json" + , {'amount': "1010.00"} + , auth_as='alice' + ) + assert "bad amount" in response.body + assert response.code == 400 + + response = self.client.PxST( "/TheATeam/subscription.json" + , {'amount': "-1.00"} + , auth_as='alice' + ) + assert "bad amount" in response.body + assert response.code == 400 + + + def test_subscribing_to_rejected_team_fails(self): + self.make_team(is_approved=False) + self.make_participant("alice", claimed_time='now', last_bill_result='') + response = self.client.PxST( "/TheATeam/subscription.json" + , {'amount': "10.00"} + , auth_as='alice' + ) + assert "unapproved team" in response.body + assert response.code == 400 diff --git a/tests/py/test_tip_json.py b/tests/py/test_tip_json.py deleted file mode 100644 index c8179e80d9..0000000000 --- a/tests/py/test_tip_json.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import print_function, unicode_literals - -import json - -import pytest -from aspen.utils import utcnow -from gratipay.testing import Harness - - -class TestTipJson(Harness): - - @pytest.mark.xfail(reason="migrating to Teams; #3399") - def test_get_amount_and_total_back_from_api(self): - "Test that we get correct amounts and totals back on POSTs to tip.json" - - # First, create some test data - # We need accounts - now = utcnow() - self.make_participant("test_tippee1", claimed_time=now) - self.make_participant("test_tippee2", claimed_time=now) - self.make_participant("test_tipper", claimed_time=now, last_bill_result='') - - # Then, add a $1.50 and $3.00 tip - response1 = self.client.POST( "/~test_tippee1/tip.json" - , {'amount': "1.00"} - , auth_as='test_tipper' - ) - - response2 = self.client.POST( "/~test_tippee2/tip.json" - , {'amount': "3.00"} - , auth_as='test_tipper' - ) - - # Confirm we get back the right amounts. - first_data = json.loads(response1.body) - second_data = json.loads(response2.body) - assert first_data['amount'] == "1.00" - assert first_data['total_giving'] == "1.00" - assert second_data['amount'] == "3.00" - assert second_data['total_giving'] == "4.00" - - @pytest.mark.xfail(reason="migrating to Teams; #3399") - def test_set_tip_out_of_range(self): - now = utcnow() - self.make_participant("alice", claimed_time=now) - self.make_participant("bob", claimed_time=now) - - response = self.client.PxST( "/~alice/tip.json" - , {'amount': "110.00"} - , auth_as='bob' - ) - assert "bad amount" in response.body - assert response.code == 400 - - response = self.client.PxST( "/~alice/tip.json" - , {'amount': "-1.00"} - , auth_as='bob' - ) - assert "bad amount" in response.body - assert response.code == 400 - - - @pytest.mark.xfail(reason="migrating to Teams; #3399") - def test_tip_to_unclaimed(self): - now = utcnow() - alice = self.make_elsewhere('twitter', 1, 'alice') - self.make_participant("bob", claimed_time=now) - response = self.client.POST( "/~%s/tip.json" % alice.participant.username - , {'amount': "10.00"} - , auth_as='bob' - ) - data = json.loads(response.body) - assert response.code == 200 - assert data['amount'] == "10.00" diff --git a/www/%team/subscription.json.spt b/www/%team/subscription.json.spt new file mode 100644 index 0000000000..088b34fcb6 --- /dev/null +++ b/www/%team/subscription.json.spt @@ -0,0 +1,63 @@ +"""Get or change the authenticated user's subscription to this team. +""" +from decimal import InvalidOperation + +from aspen import Response +from babel.numbers import NumberFormatError +from gratipay.exceptions import BadAmount +from gratipay.utils import get_team + +[-----------------------------------------------------------------------------] + +if user.ANON: + raise Response(403, _("Please sign in first")) + +else: + out = {} + + # Get team. + # ========= + + team = get_team(state) + if team.is_closed or not team.is_approved: + raise Response(400, "unapproved team") + + + # Get and maybe set amount. + # ========================= + + if request.method == 'POST' and 'amount' in request.body: + try: + out = user.participant.set_subscription_to(team, parse_decimal(request.body['amount'])) + except (InvalidOperation, ValueError, BadAmount, NumberFormatError): + raise Response(400, "bad amount") + else: + out = user.participant.get_subscription_to(team) + + amount = out['amount'] + total_giving = user.participant.giving + total_receiving = user.participant.receiving + + out["amount"] = str(amount) + out["amount_l"] = format_currency(amount, 'USD') + out["msg"] = _("Payment changed to {0} per week. ", out["amount_l"]) + out["msg"] += _("Thank you so much for supporting {0}!", team.name) + out["nsupporters"] = team.nsupporters + out["team_id"] = team.id + out["total_giving"] = str(total_giving) + out["total_giving_l"] = format_currency(total_giving, 'USD') + out["total_receiving"] = str(total_receiving) + out["total_receiving_l"] = format_currency(total_receiving, 'USD') + + total_receiving_team = team.receiving + out["total_receiving_team"] = str(total_receiving_team) + out["total_receiving_team_l"] = format_currency(total_receiving_team, 'USD') + + if 'ctime' in out: + out["ctime"] = str(out['ctime']) + out["mtime"] = str(out['mtime']) + else: + out["ctime"] = out["mtime"] = None + +[---] application/json via json_dump +out From 5ee4e785687f7bbcaa0e8c99ccdeceb2ed598e78 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Thu, 21 May 2015 08:38:09 -0400 Subject: [PATCH 4/8] Bring back Your Tip as Your Payment --- js/gratipay.js | 2 +- js/gratipay/payments.js | 86 ++++++++++++++++++++++++++++++++++++ js/gratipay/tips.js | 86 ------------------------------------ scss/components/cta.scss | 4 +- templates/your-payment.html | 38 ++++++++++++++++ templates/your-tip.html | 39 ---------------- www/%team/index.html.spt | 8 ++-- www/~/%username/tip.json.spt | 72 ------------------------------ 8 files changed, 132 insertions(+), 203 deletions(-) create mode 100644 js/gratipay/payments.js delete mode 100644 js/gratipay/tips.js create mode 100644 templates/your-payment.html delete mode 100644 templates/your-tip.html delete mode 100644 www/~/%username/tip.json.spt diff --git a/js/gratipay.js b/js/gratipay.js index 86940ac04c..129cd3fc81 100644 --- a/js/gratipay.js +++ b/js/gratipay.js @@ -14,7 +14,7 @@ Gratipay.init = function() { Gratipay.forms.initCSRF(); Gratipay.signIn(); Gratipay.signOut(); - Gratipay.tips.initSupportGratipay(); + Gratipay.payments.initSupportGratipay(); }; Gratipay.error = function(jqXHR, textStatus, errorThrown) { diff --git a/js/gratipay/payments.js b/js/gratipay/payments.js new file mode 100644 index 0000000000..b8cf3e5a5c --- /dev/null +++ b/js/gratipay/payments.js @@ -0,0 +1,86 @@ +Gratipay.payments = {}; + +Gratipay.payments.init = function() { + + Gratipay.forms.jsEdit({ + confirmBeforeUnload: true, + hideEditButton: true, + root: $('.your-payment.js-edit'), + success: function(data) { + Gratipay.notification(data.msg, 'success'); + Gratipay.payments.afterTipChange(data); + } + }); + + $('.your-payment button.edit').click(function() { + $('.your-payment input').focus(); + }); + + $('.your-payment button.stop').click(function() { + $('.your-payment input').val('0'); + $('.your-payment button.save').click(); + }); + + $('.your-payment button.cancel').click(function() { + $('.your-payment form').trigger('reset'); + }); + + // Cancel if the user presses the Escape key + $('.your-payment input').keyup(function(e) { + if (e.keyCode === 27) + $('.your-payment button.cancel').click(); + }); +}; + + +Gratipay.payments.initSupportGratipay = function() { + $('.support-gratipay button').click(function() { + var amount = parseFloat($(this).attr('data-amount'), 10); + Gratipay.payments.set('Gratipay', amount, function(data) { + Gratipay.notification(data.msg, 'success'); + $('.support-gratipay').slideUp(); + + // If you're on your own giving page ... + var payment_on_giving = $('.your-payment[data-team="Gratipay"]'); + if (payment_on_giving.length > 0) { + payment_on_giving[0].defaultValue = amount; + payment_on_giving.attr('value', amount.toFixed(2)); + } + }); + }); + + $('.support-gratipay .no-thanks').click(function(event) { + event.preventDefault(); + jQuery.post('/ride-free.json') + .success(function() { $('.support-gratipay').slideUp(); }) + .fail(Gratipay.error) + }); +}; + + +Gratipay.payments.afterTipChange = function(data) { + $('.my-total-giving').text(data.total_giving_l); + $('.total-receiving[data-team="'+data.team_id+'"]').text(data.total_receiving_team_l); + $('#payment-prompt').toggleClass('needed', data.amount > 0); + $('.nsupporters[data-team="'+data.team_id+'"]').text(data.nsupporters); + + var $your_payment = $('.your-payment[data-team="'+data.team_id+'"]'); + if ($your_payment) { + var $input = $your_payment.find('input'); + $input[0].defaultValue = $input.val(); + $your_payment.find('span.amount').text(data.amount_l); + $your_payment.find('.edit').toggleClass('not-zero', data.amount > 0); + $your_payment.find('.stop').toggleClass('zero', data.amount === 0); + } +}; + + +Gratipay.payments.set = function(team, amount, callback) { + + // send request to set up a recurring payment + $.post('/' + team + '/subscription.json', { amount: amount }, function(data) { + if (callback) callback(data); + Gratipay.payments.afterTipChange(data); + }) + .fail(Gratipay.error); +}; diff --git a/js/gratipay/tips.js b/js/gratipay/tips.js deleted file mode 100644 index 2d4e2114e2..0000000000 --- a/js/gratipay/tips.js +++ /dev/null @@ -1,86 +0,0 @@ -Gratipay.tips = {}; - -Gratipay.tips.init = function() { - - Gratipay.forms.jsEdit({ - confirmBeforeUnload: true, - hideEditButton: true, - root: $('.your-tip.js-edit'), - success: function(data) { - Gratipay.notification(data.msg, 'success'); - Gratipay.tips.afterTipChange(data); - } - }); - - $('.your-tip button.edit').click(function() { - $('.your-tip input').focus(); - }); - - $('.your-tip button.stop').click(function() { - $('.your-tip input').val('0'); - $('.your-tip button.save').click(); - }); - - $('.your-tip button.cancel').click(function() { - $('.your-tip form').trigger('reset'); - }); - - // Cancel if the user presses the Escape key - $('.your-tip input').keyup(function(e) { - if (e.keyCode === 27) - $('.your-tip button.cancel').click(); - }); -}; - - -Gratipay.tips.initSupportGratipay = function() { - $('.support-gratipay button').click(function() { - var amount = parseFloat($(this).attr('data-amount'), 10); - Gratipay.tips.set('Gratipay', amount, function(data) { - Gratipay.notification(data.msg, 'success'); - $('.support-gratipay').slideUp(); - - // If you're on your own giving page ... - var tip_on_giving = $('.your-tip[data-tippee="Gratipay"]'); - if (tip_on_giving.length > 0) { - tip_on_giving[0].defaultValue = amount; - tip_on_giving.attr('value', amount.toFixed(2)); - } - }); - }); - - $('.support-gratipay .no-thanks').click(function(event) { - event.preventDefault(); - jQuery.post('/ride-free.json') - .success(function() { $('.support-gratipay').slideUp(); }) - .fail(Gratipay.error) - }); -}; - - -Gratipay.tips.afterTipChange = function(data) { - $('.my-total-giving').text(data.total_giving_l); - $('.total-receiving[data-tippee="'+data.tippee_id+'"]').text(data.total_receiving_tippee_l); - $('#payment-prompt').toggleClass('needed', data.amount > 0); - $('.npatrons[data-tippee="'+data.tippee_id+'"]').text(data.npatrons); - - var $your_tip = $('.your-tip[data-tippee="'+data.tippee_id+'"]'); - if ($your_tip) { - var $input = $your_tip.find('input'); - $input[0].defaultValue = $input.val(); - $your_tip.find('span.amount').text(data.amount_l); - $your_tip.find('.edit').toggleClass('not-zero', data.amount > 0); - $your_tip.find('.stop').toggleClass('zero', data.amount === 0); - } -}; - - -Gratipay.tips.set = function(tippee, amount, callback) { - - // send request to change tip - $.post('/' + tippee + '/tip.json', { amount: amount }, function(data) { - if (callback) callback(data); - Gratipay.tips.afterTipChange(data); - }) - .fail(Gratipay.error); -}; diff --git a/scss/components/cta.scss b/scss/components/cta.scss index 1ba64443d4..bc351b5042 100644 --- a/scss/components/cta.scss +++ b/scss/components/cta.scss @@ -8,7 +8,7 @@ color: $white; } - .your-tip .amount { + .your-payment .amount { /* Match content h1 */ font: bold 28px/37px $Ideal; } @@ -17,7 +17,7 @@ font: normal 14px/14px $Ideal; } - .your-tip { + .your-payment { input { width: 120px; height: 33px; diff --git a/templates/your-payment.html b/templates/your-payment.html new file mode 100644 index 0000000000..74eb6b2955 --- /dev/null +++ b/templates/your-payment.html @@ -0,0 +1,38 @@ +{% if user.ANON %} +
+ {% include "templates/sign-in-using-to-give.html" %} +
+{% else %} +
+ {% set subscription = user.participant.get_subscription_to(team.slug) %} +

{{ _('Your Payment') }}

+
+
+ {{ format_currency(subscription.amount, 'USD') }} +
{{ _("per week") }}
+ +
+
+ $ + +
{{ _("per week") }}
+ + + +
+
+ + {% if not subscription.is_funded %} +
+ {{ _("Back your payment with a {0}credit card{1} to make sure it goes through!", + ""|safe % user.participant.username, + ""|safe) }} +
+ {% endif %} +
+{% endif %} diff --git a/templates/your-tip.html b/templates/your-tip.html deleted file mode 100644 index df29622b00..0000000000 --- a/templates/your-tip.html +++ /dev/null @@ -1,39 +0,0 @@ -{% if user.ANON %} -
- {% include "templates/sign-in-using-to-give.html" %} -
-{% else %} -
- {% set tippee = participant.username %} - {% set tip = user.participant.get_tip_to(tippee) %} -

{{ _('Your Tip') }}

-
-
- {{ format_currency(tip.amount, 'USD') }} -
{{ _("per week") }}
- -
-
- $ - -
{{ _("per week") }}
- - - -
-
- - {% if not tip.is_funded %} -
- {{ _("Back your payment with a {0}credit card{1} to make sure it goes through!", - ""|safe % user.participant.username, - ""|safe) }} -
- {% endif %} -
-{% endif %} diff --git a/www/%team/index.html.spt b/www/%team/index.html.spt index 6ca978b64f..2ef53576e4 100644 --- a/www/%team/index.html.spt +++ b/www/%team/index.html.spt @@ -29,6 +29,10 @@ title = name = team.name {% endblock %} +{% block sidebar %} +{% include "templates/your-payment.html" %} +{% endblock %} + {% block content %}
{% if team.is_approved in (None, False) %} @@ -58,8 +62,6 @@ title = name = team.name {% endblock %} {% block scripts %} -{% if user.participant == team.owner %} - -{% endif %} + {{ super() }} {% endblock %} diff --git a/www/~/%username/tip.json.spt b/www/~/%username/tip.json.spt deleted file mode 100644 index 1fc5005330..0000000000 --- a/www/~/%username/tip.json.spt +++ /dev/null @@ -1,72 +0,0 @@ -"""Get or change the authenticated user's tip to this person. -""" -from decimal import InvalidOperation - -from aspen import Response -from babel.numbers import NumberFormatError -from gratipay.exceptions import BadAmount -from gratipay.models.participant import Participant -from gratipay.utils import get_participant - -[-----------------------------------------------------------------------------] - -raise Response(404) # turn off while migrating to Teams (see #3399) - -if user.ANON: - raise Response(403, _("Please sign in first")) - -else: - out = {} - - # Get tipper and tippee. - # ====================== - - tipper = user.participant - tippee = get_participant(state, restrict=False, resolve_unclaimed=False) - - - # Get and maybe set amount. - # ========================= - - if request.method == 'POST' and 'amount' in request.body and tippee != tipper: - try: - out = tipper.set_tip_to(tippee, parse_decimal(request.body['amount'])) - except (InvalidOperation, ValueError, BadAmount, NumberFormatError): - raise Response(400, "bad amount") - else: - out = tipper.get_tip_to(tippee.username) - - amount = out['amount'] - total_giving = tipper.giving - total_receiving = tipper.receiving - - out["amount"] = str(amount) - out["amount_l"] = format_currency(amount, 'USD') - if tippee.username == 'Gratipay': - out["msg"] = _("Thank you so much for supporting Gratipay! :D") - else: - out["msg"] = _("Tip changed to {0} per week!", out["amount_l"]) - out["npatrons"] = tippee.npatrons - out["tippee_id"] = tippee.id - out["total_giving"] = str(total_giving) - out["total_giving_l"] = format_currency(total_giving, 'USD') - out["total_receiving"] = str(total_receiving) - out["total_receiving_l"] = format_currency(total_receiving, 'USD') - - if not tippee.anonymous_receiving: - total_receiving_tippee = tippee.receiving - out["total_receiving_tippee"] = str(total_receiving_tippee) - out["total_receiving_tippee_l"] = format_currency(total_receiving_tippee, 'USD') - else: - out["total_receiving_tippee"] = None - out["total_receiving_tippee_l"] = '[' + _("hidden") + ']' - - if 'ctime' in out: - out["ctime"] = str(out['ctime']) - out["mtime"] = str(out['mtime']) - else: - out["ctime"] = out["mtime"] = None - - -[---] application/json via json_dump -out From 426d63c060d9dfda1ff583e23e9c19c7256cbadb Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Thu, 21 May 2015 09:41:37 -0400 Subject: [PATCH 5/8] Turn off Members, Receiving, and Giving pages Too much to convert Giving to Subscriptions as part of #3467, because it depends on caching, which is a lot of work to change. --- templates/profile-subnav.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/templates/profile-subnav.html b/templates/profile-subnav.html index ee069096fa..2044e8639f 100644 --- a/templates/profile-subnav.html +++ b/templates/profile-subnav.html @@ -6,9 +6,6 @@ {% set u = participant.username %} {% set pages = [ ('/', _('Dashboard'), True, False) , ('/~'+u+'/', _('Profile'), True, show_profile) - , ('/~'+u+'/members/', _('Members'), show_members, show_members) - , ('/~'+u+'/receiving/', _('Receiving'), True, show_receiving) - , ('/~'+u+'/giving/', _('Giving'), True, False) , ('/~'+u+'/history/', _('History'), True, False) , ('/~'+u+'/widgets/', _('Widgets'), True, False) , ('/~'+u+'/identity', _('Identity'), True, False) From 5d16d1d3b8a145ed85bac92fa99a595ed7bbbec4 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Thu, 21 May 2015 10:03:52 -0400 Subject: [PATCH 6/8] Update payment widget for anon --- templates/sign-in-using-to-give.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/sign-in-using-to-give.html b/templates/sign-in-using-to-give.html index d06363af1d..2b802a515f 100644 --- a/templates/sign-in-using-to-give.html +++ b/templates/sign-in-using-to-give.html @@ -1,4 +1,4 @@ From d062f211f8751f2d9fccbbb7de0a0bcfbe4ce48d Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Thu, 21 May 2015 10:04:02 -0400 Subject: [PATCH 7/8] Mark caching tests xfail --- tests/py/test_billing_payday.py | 2 ++ tests/py/test_communities.py | 3 +++ tests/py/test_participant.py | 4 ++++ tests/py/test_public_json.py | 8 ++++++++ tests/py/test_stats.py | 6 ++++++ tests/py/test_take.py | 10 ++++++++++ tests/py/test_tip_distribution_json.py | 2 ++ 7 files changed, 35 insertions(+) diff --git a/tests/py/test_billing_payday.py b/tests/py/test_billing_payday.py index 446841c418..efc9723a5f 100644 --- a/tests/py/test_billing_payday.py +++ b/tests/py/test_billing_payday.py @@ -107,6 +107,7 @@ def test_ncc_failing(self, cch, fch): after = self.fetch_payday() assert after['ncc_failing'] == 1 + @pytest.mark.xfail(reason="#3399") def test_update_cached_amounts(self): team = self.make_participant('team', claimed_time='now', number='plural') alice = self.make_participant('alice', claimed_time='now', last_bill_result='') @@ -164,6 +165,7 @@ def check(): Payday.start().update_cached_amounts() check() + @pytest.mark.xfail(reason="#3399") def test_update_cached_amounts_depth(self): alice = self.make_participant('alice', claimed_time='now', last_bill_result='') usernames = ('bob', 'carl', 'dana', 'emma', 'fred', 'greg') diff --git a/tests/py/test_communities.py b/tests/py/test_communities.py index a123565e0a..7c5b4d34e2 100644 --- a/tests/py/test_communities.py +++ b/tests/py/test_communities.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, division, print_function, unicode_literals +import pytest from gratipay.testing import Harness @@ -19,6 +20,7 @@ def test_community_member_shows_up_on_community_listing(self): html = self.client.GET('/for/something/', want='response.body') assert html.count('alice') == 2 # entry in New Participants + @pytest.mark.xfail(reason="#3399") def test_givers_show_up_on_community_page(self): # Alice tips bob. @@ -40,6 +42,7 @@ def test_givers_dont_show_up_if_they_give_zero(self): assert html.count('alice') == 2 # entry in New Participants only assert 'bob' not in html + @pytest.mark.xfail(reason="#3399") def test_receivers_show_up_on_community_page(self): # Bob tips alice. diff --git a/tests/py/test_participant.py b/tests/py/test_participant.py index 83881cee95..628e424bf1 100644 --- a/tests/py/test_participant.py +++ b/tests/py/test_participant.py @@ -84,6 +84,7 @@ def test_participant_can_be_instantiated(self): actual = Participant.from_username('alice').__class__ assert actual is expected + @pytest.mark.xfail(reason="#3399") def test_bob_has_two_dollars_in_tips(self): expected = Decimal('2.00') actual = self.bob.receiving @@ -629,6 +630,7 @@ def test_only_latest_tip_counts(self): assert alice.giving == Decimal('4.00') + @pytest.mark.xfail(reason="#3399") def test_receiving_includes_tips_from_whitelisted_accounts(self): alice = self.make_participant( 'alice' , claimed_time='now' @@ -641,6 +643,7 @@ def test_receiving_includes_tips_from_whitelisted_accounts(self): assert bob.receiving == Decimal('3.00') assert bob.npatrons == 1 + @pytest.mark.xfail(reason="#3399") def test_receiving_includes_tips_from_unreviewed_accounts(self): alice = self.make_participant( 'alice' , claimed_time='now' @@ -665,6 +668,7 @@ def test_receiving_ignores_tips_from_blacklisted_accounts(self): assert bob.receiving == Decimal('0.00') assert bob.npatrons == 0 + @pytest.mark.xfail(reason="#3399") def test_receiving_includes_taking_when_updated_from_set_tip_to(self): alice = self.make_participant('alice', claimed_time='now', last_bill_result='') bob = self.make_participant('bob', claimed_time='now', taking=Decimal('42.00')) diff --git a/tests/py/test_public_json.py b/tests/py/test_public_json.py index 9e54a73ebe..bf428e7005 100644 --- a/tests/py/test_public_json.py +++ b/tests/py/test_public_json.py @@ -2,6 +2,7 @@ import json +import pytest from aspen.utils import utcnow from gratipay.testing import Harness @@ -18,6 +19,7 @@ def test_on_key_gives_gratipay(self): assert data['on'] == 'gratipay' + @pytest.mark.xfail(reason="#3399") def test_anonymous_gets_receiving(self): alice = self.make_participant('alice', last_bill_result='') bob = self.make_participant('bob') @@ -38,6 +40,7 @@ def test_anonymous_does_not_get_my_tip(self): assert data.has_key('my_tip') == False + @pytest.mark.xfail(reason="#3399") def test_anonymous_gets_giving(self): alice = self.make_participant('alice', last_bill_result='') bob = self.make_participant('bob') @@ -70,6 +73,7 @@ def test_anonymous_gets_null_receiving_if_user_anonymous(self): assert data['receiving'] == None + @pytest.mark.xfail(reason="#3399") def test_authenticated_user_gets_their_tip(self): alice = self.make_participant('alice', last_bill_result='') bob = self.make_participant('bob') @@ -83,6 +87,7 @@ def test_authenticated_user_gets_their_tip(self): assert data['receiving'] == '1.00' assert data['my_tip'] == '1.00' + @pytest.mark.xfail(reason="#3399") def test_authenticated_user_doesnt_get_other_peoples_tips(self): alice = self.make_participant('alice', last_bill_result='') bob = self.make_participant('bob', last_bill_result='') @@ -100,6 +105,7 @@ def test_authenticated_user_doesnt_get_other_peoples_tips(self): assert data['receiving'] == '16.00' assert data['my_tip'] == '1.00' + @pytest.mark.xfail(reason="#3399") def test_authenticated_user_gets_zero_if_they_dont_tip(self): self.make_participant('alice', last_bill_result='') bob = self.make_participant('bob', last_bill_result='') @@ -114,6 +120,7 @@ def test_authenticated_user_gets_zero_if_they_dont_tip(self): assert data['receiving'] == '3.00' assert data['my_tip'] == '0.00' + @pytest.mark.xfail(reason="#3399") def test_authenticated_user_gets_self_for_self(self): alice = self.make_participant('alice', last_bill_result='') bob = self.make_participant('bob') @@ -133,6 +140,7 @@ def test_access_control_allow_origin_header_is_asterisk(self): assert response.headers['Access-Control-Allow-Origin'] == '*' + @pytest.mark.xfail(reason="#3399") def test_jsonp_works(self): alice = self.make_participant('alice', last_bill_result='') bob = self.make_participant('bob') diff --git a/tests/py/test_stats.py b/tests/py/test_stats.py index 9a152e6b08..b60299fef0 100644 --- a/tests/py/test_stats.py +++ b/tests/py/test_stats.py @@ -4,6 +4,7 @@ from decimal import Decimal import json +import pytest from mock import patch from gratipay import wireup @@ -38,6 +39,7 @@ def setUp(self): p = self.make_participant(participant, claimed_time='now', last_bill_result='') setattr(self, participant, p) + @pytest.mark.xfail(reason="#3399") def test_get_tip_distribution_handles_a_tip(self): self.alice.set_tip_to(self.bob, '3.00') expected = ([[Decimal('3.00'), 1, Decimal('3.00'), 1.0, Decimal('1')]], @@ -50,6 +52,7 @@ def test_get_tip_distribution_handles_no_tips(self): actual = self.alice.get_tip_distribution() assert actual == expected + @pytest.mark.xfail(reason="#3399") def test_get_tip_distribution_handles_multiple_tips(self): carl = self.make_participant('carl', claimed_time='now', last_bill_result='') self.alice.set_tip_to(self.bob, '1.00') @@ -61,6 +64,7 @@ def test_get_tip_distribution_handles_multiple_tips(self): actual = self.bob.get_tip_distribution() assert actual == expected + @pytest.mark.xfail(reason="#3399") def test_get_tip_distribution_handles_big_tips(self): self.bob.update_number('plural') carl = self.make_participant('carl', claimed_time='now', last_bill_result='') @@ -73,6 +77,7 @@ def test_get_tip_distribution_handles_big_tips(self): actual = self.bob.get_tip_distribution() assert actual == expected + @pytest.mark.xfail(reason="#3399") def test_get_tip_distribution_ignores_bad_cc(self): bad_cc = self.make_participant('bad_cc', claimed_time='now', last_bill_result='Failure!') self.alice.set_tip_to(self.bob, '1.00') @@ -82,6 +87,7 @@ def test_get_tip_distribution_ignores_bad_cc(self): actual = self.bob.get_tip_distribution() assert actual == expected + @pytest.mark.xfail(reason="#3399") def test_get_tip_distribution_ignores_missing_cc(self): missing_cc = self.make_participant('missing_cc', claimed_time='now') self.alice.set_tip_to(self.bob, '1.00') diff --git a/tests/py/test_take.py b/tests/py/test_take.py index a079299ff7..940701795d 100644 --- a/tests/py/test_take.py +++ b/tests/py/test_take.py @@ -2,6 +2,7 @@ from decimal import Decimal as D +import pytest from gratipay.testing import Harness from gratipay.models.participant import Participant @@ -81,6 +82,7 @@ def test_if_last_week_is_less_than_a_dollar_can_increase_to_a_dollar(self): actual = team.set_take_for(alice, D('42.00'), team) assert actual == 1 + @pytest.mark.xfail(reason="#3399") def test_get_members(self): team = self.make_team() alice = self.make_participant('alice', claimed_time='now') @@ -92,6 +94,7 @@ def test_get_members(self): assert members[0]['take'] == 42 assert members[0]['balance'] == 58 + @pytest.mark.xfail(reason="#3399") def test_compute_actual_takes_counts_the_team_balance(self): team = self.make_team(balance=D('59.46'), giving=D('7.15')) alice = self.make_participant('alice', claimed_time='now') @@ -106,6 +109,7 @@ def test_compute_actual_takes_counts_the_team_balance(self): assert takes[1]['actual_amount'] == 0 assert takes[1]['balance'] == D('10.31') + @pytest.mark.xfail(reason="#3399") def test_compute_actual_takes_gives_correct_final_balance(self): team = self.make_team(balance=D('53.72')) alice = self.make_participant('alice', claimed_time='now') @@ -120,6 +124,7 @@ def test_compute_actual_takes_gives_correct_final_balance(self): assert takes[1]['actual_amount'] == 14 assert takes[1]['balance'] == D('67.72') + @pytest.mark.xfail(reason="#3399") def test_taking_and_receiving_are_updated_correctly(self): team = self.make_team() alice = self.make_participant('alice', claimed_time='now') @@ -134,6 +139,7 @@ def test_taking_and_receiving_are_updated_correctly(self): assert alice.taking == 50 assert alice.receiving == 60 + @pytest.mark.xfail(reason="#3399") def test_taking_is_zero_for_team(self): team = self.make_team() alice = self.make_participant('alice', claimed_time='now') @@ -142,6 +148,7 @@ def test_taking_is_zero_for_team(self): assert team.taking == 0 assert team.receiving == 100 + @pytest.mark.xfail(reason="#3399") def test_but_team_can_take_from_other_team(self): a_team = self.make_team('A Team', claimed_time='now') b_team = self.make_team('B Team', claimed_time='now') @@ -152,6 +159,7 @@ def test_but_team_can_take_from_other_team(self): assert b_team.taking == 1 assert b_team.receiving == 101 + @pytest.mark.xfail(reason="#3399") def test_changes_to_team_receiving_affect_members_take(self): team = self.make_team() alice = self.make_participant('alice', claimed_time='now') @@ -162,6 +170,7 @@ def test_changes_to_team_receiving_affect_members_take(self): alice = Participant.from_username('alice') assert alice.receiving == alice.taking == 10 + @pytest.mark.xfail(reason="#3399") def test_changes_to_others_take_affects_members_take(self): team = self.make_team() @@ -179,6 +188,7 @@ def test_changes_to_others_take_affects_members_take(self): # But get_members still uses nominal amount assert [m['take'] for m in team.get_members(alice)] == [60, 42, 0] + @pytest.mark.xfail(reason="#3399") def test_changes_to_others_take_can_increase_members_take(self): team = self.make_team() diff --git a/tests/py/test_tip_distribution_json.py b/tests/py/test_tip_distribution_json.py index 485f2bc251..628bd25058 100644 --- a/tests/py/test_tip_distribution_json.py +++ b/tests/py/test_tip_distribution_json.py @@ -2,11 +2,13 @@ import json +import pytest from gratipay.testing import Harness class Tests(Harness): + @pytest.mark.xfail(reason="#3399") def test_tip_distribution_json_gives_tip_distribution(self): alice = self.make_participant('alice', claimed_time='now', last_bill_result='') bob = self.make_participant('bob', claimed_time='now', number='plural') From 65148989ea7b96405fd785e5e571957c0e2647b9 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Thu, 21 May 2015 09:18:55 -0400 Subject: [PATCH 8/8] Replace Profile > Giving with > Subscriptions Removes Profile > {Members,Receiving} entirely. Conflicts: templates/profile-subnav.html --- gratipay/models/participant.py | 47 +++++++++---------- js/gratipay/giving.js | 29 ------------ js/gratipay/subscriptions.js | 29 ++++++++++++ scss/gratipay.scss | 2 +- .../pages/{giving.scss => subscriptions.scss} | 2 +- templates/profile-subnav.html | 1 + ...ng-table.html => subscriptions-table.html} | 21 ++++----- tests/py/test_close.py | 6 +++ tests/py/test_pages.py | 20 ++++---- .../{giving => subscriptions}/index.html.spt | 34 +++++++------- 10 files changed, 92 insertions(+), 99 deletions(-) delete mode 100644 js/gratipay/giving.js create mode 100644 js/gratipay/subscriptions.js rename scss/pages/{giving.scss => subscriptions.scss} (77%) rename templates/{giving-table.html => subscriptions-table.html} (54%) rename www/~/%username/{giving => subscriptions}/index.html.spt (54%) diff --git a/gratipay/models/participant.py b/gratipay/models/participant.py index 731a64f9f1..ee0dc911a3 100644 --- a/gratipay/models/participant.py +++ b/gratipay/models/participant.py @@ -372,6 +372,7 @@ class NoOneToGiveFinalGiftTo(Exception): pass def distribute_balance_as_final_gift(self, cursor): """Distribute a balance as a final gift. """ + raise NotImplementedError # XXX Bring me back! if self.balance == 0: return @@ -1310,49 +1311,43 @@ def get_tip_distribution(self): return tip_amounts, npatrons, contributed - def get_giving_for_profile(self): - """Given a participant id and a date, return a list and a Decimal. - - This function is used to populate a participant's page for their own - viewing pleasure. - + def get_subscriptions_for_profile(self): + """Return a list and a Decimal. """ - TIPS = """\ + SUBSCRIPTIONS = """\ SELECT * FROM ( - SELECT DISTINCT ON (tippee) - amount - , tippee - , t.ctime - , t.mtime - , p.claimed_time - , p.username_lower - , p.number - FROM tips t - JOIN participants p ON p.username = t.tippee - WHERE tipper = %s - AND p.is_suspicious IS NOT true - AND p.claimed_time IS NOT NULL - ORDER BY tippee - , t.mtime DESC + SELECT DISTINCT ON (s.team) + s.team as team_slug + , s.amount + , s.ctime + , s.mtime + , t.name as team_name + FROM subscriptions s + JOIN teams t ON s.team = t.slug + WHERE subscriber = %s + AND t.is_approved is true + AND t.is_closed is not true + ORDER BY s.team + , s.mtime DESC ) AS foo ORDER BY amount DESC - , username_lower + , team_slug """ - tips = self.db.all(TIPS, (self.username,)) + subscriptions = self.db.all(SUBSCRIPTIONS, (self.username,)) # Compute the total. # ================== - total = sum([t.amount for t in tips]) + total = sum([s.amount for s in subscriptions]) if not total: # If tips is an empty list, total is int 0. We want a Decimal. total = Decimal('0.00') - return tips, total + return subscriptions, total def get_current_tips(self): """Get the tips this participant is currently sending to others. diff --git a/js/gratipay/giving.js b/js/gratipay/giving.js deleted file mode 100644 index 93db2ccd2e..0000000000 --- a/js/gratipay/giving.js +++ /dev/null @@ -1,29 +0,0 @@ -Gratipay.giving = {} - -Gratipay.giving.init = function() { - Gratipay.giving.activateTab('active'); - $('.giving #tab-nav a').on('click', Gratipay.giving.handleClick); -} - -Gratipay.giving.handleClick = function(e) { - e.preventDefault(); - var $target = $(e.target); - Gratipay.giving.activateTab($target.data('tab')); -} - -Gratipay.giving.activateTab = function(tab) { - $.each($('.giving #tab-nav a'), function(i, obj) { - var $obj = $(obj); - if ($obj.data('tab') == tab) { - $obj.addClass('selected'); - } else { - $obj.removeClass('selected'); - } - }) - - $.each($('.giving .tab'), function(i, obj) { - var $obj = $(obj); - if ($obj.data('tab') == tab) { $obj.show(); } else { $obj.hide(); } - }) -} - diff --git a/js/gratipay/subscriptions.js b/js/gratipay/subscriptions.js new file mode 100644 index 0000000000..2b34562631 --- /dev/null +++ b/js/gratipay/subscriptions.js @@ -0,0 +1,29 @@ +Gratipay.subscriptions = {} + +Gratipay.subscriptions.init = function() { + Gratipay.subscriptions.activateTab('active'); + $('.subscriptions #tab-nav a').on('click', Gratipay.subscriptions.handleClick); +} + +Gratipay.subscriptions.handleClick = function(e) { + e.preventDefault(); + var $target = $(e.target); + Gratipay.subscriptions.activateTab($target.data('tab')); +} + +Gratipay.subscriptions.activateTab = function(tab) { + $.each($('.subscriptions #tab-nav a'), function(i, obj) { + var $obj = $(obj); + if ($obj.data('tab') == tab) { + $obj.addClass('selected'); + } else { + $obj.removeClass('selected'); + } + }) + + $.each($('.subscriptions .tab'), function(i, obj) { + var $obj = $(obj); + if ($obj.data('tab') == tab) { $obj.show(); } else { $obj.hide(); } + }) +} + diff --git a/scss/gratipay.scss b/scss/gratipay.scss index 8caec97ae2..c2e4c5fe69 100644 --- a/scss/gratipay.scss +++ b/scss/gratipay.scss @@ -57,7 +57,7 @@ @import "pages/history"; @import "pages/team"; @import "pages/profile-edit"; -@import "pages/giving"; +@import "pages/subscriptions"; @import "pages/settings"; @import "pages/cc-ba"; @import "pages/on-confirm"; diff --git a/scss/pages/giving.scss b/scss/pages/subscriptions.scss similarity index 77% rename from scss/pages/giving.scss rename to scss/pages/subscriptions.scss index 2c66bb7328..3508be0c6a 100644 --- a/scss/pages/giving.scss +++ b/scss/pages/subscriptions.scss @@ -1,4 +1,4 @@ -.giving { +.subscriptions { .note { font: italic 12px/14px $Ideal; } diff --git a/templates/profile-subnav.html b/templates/profile-subnav.html index 2044e8639f..da0f602d2d 100644 --- a/templates/profile-subnav.html +++ b/templates/profile-subnav.html @@ -6,6 +6,7 @@ {% set u = participant.username %} {% set pages = [ ('/', _('Dashboard'), True, False) , ('/~'+u+'/', _('Profile'), True, show_profile) + , ('/~'+u+'/subscriptions/', _('Subscriptions'), True, False) , ('/~'+u+'/history/', _('History'), True, False) , ('/~'+u+'/widgets/', _('Widgets'), True, False) , ('/~'+u+'/identity', _('Identity'), True, False) diff --git a/templates/giving-table.html b/templates/subscriptions-table.html similarity index 54% rename from templates/giving-table.html rename to templates/subscriptions-table.html index 99b9c82de1..7f966f88fb 100644 --- a/templates/giving-table.html +++ b/templates/subscriptions-table.html @@ -1,8 +1,8 @@ -{% macro giving_table(state, tips, total) %} +{% macro subscriptions_table(state, subscriptions, total) %} - + {% if state != 'cancelled' %} {% endif %} @@ -21,21 +21,16 @@ - {% for tip in tips %} + {% for subscription in subscriptions %} - {% if state != 'cancelled' %} - + {% endif %} - - + + {% endfor %} diff --git a/tests/py/test_close.py b/tests/py/test_close.py index 49080cd35b..ee94288284 100644 --- a/tests/py/test_close.py +++ b/tests/py/test_close.py @@ -109,6 +109,7 @@ def test_wbtba_raises_NotWhitelisted_if_blacklisted(self): # dbafg - distribute_balance_as_final_gift + @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3467') def test_dbafg_distributes_balance_as_final_gift(self): alice = self.make_participant('alice', claimed_time='now', balance=D('10.00')) bob = self.make_participant('bob', claimed_time='now') @@ -121,6 +122,7 @@ def test_dbafg_distributes_balance_as_final_gift(self): assert Participant.from_username('carl').balance == D('4.00') assert Participant.from_username('alice').balance == D('0.00') + @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3467') def test_dbafg_needs_claimed_tips(self): alice = self.make_participant('alice', claimed_time='now', balance=D('10.00')) bob = self.make_participant('bob') @@ -134,6 +136,7 @@ def test_dbafg_needs_claimed_tips(self): assert Participant.from_username('carl').balance == D('0.00') assert Participant.from_username('alice').balance == D('10.00') + @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3467') def test_dbafg_gives_all_to_claimed(self): alice = self.make_participant('alice', claimed_time='now', balance=D('10.00')) bob = self.make_participant('bob', claimed_time='now') @@ -146,6 +149,7 @@ def test_dbafg_gives_all_to_claimed(self): assert Participant.from_username('carl').balance == D('0.00') assert Participant.from_username('alice').balance == D('0.00') + @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3467') def test_dbafg_skips_zero_tips(self): alice = self.make_participant('alice', claimed_time='now', balance=D('10.00')) bob = self.make_participant('bob', claimed_time='now') @@ -159,6 +163,7 @@ def test_dbafg_skips_zero_tips(self): assert Participant.from_username('carl').balance == D('10.00') assert Participant.from_username('alice').balance == D('0.00') + @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3467') def test_dbafg_favors_highest_tippee_in_rounding_errors(self): alice = self.make_participant('alice', claimed_time='now', balance=D('10.00')) bob = self.make_participant('bob', claimed_time='now') @@ -171,6 +176,7 @@ def test_dbafg_favors_highest_tippee_in_rounding_errors(self): assert Participant.from_username('carl').balance == D('6.67') assert Participant.from_username('alice').balance == D('0.00') + @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3467') def test_dbafg_with_zero_balance_is_a_noop(self): alice = self.make_participant('alice', claimed_time='now', balance=D('0.00')) bob = self.make_participant('bob', claimed_time='now') diff --git a/tests/py/test_pages.py b/tests/py/test_pages.py index 09bb3d00be..07e13996c1 100644 --- a/tests/py/test_pages.py +++ b/tests/py/test_pages.py @@ -162,22 +162,18 @@ def test_settings_page_available_balance(self): expected = "123" assert expected in actual - def test_giving_page(self): + def test_subscriptions_page(self): + self.make_team(is_approved=True) alice = self.make_participant('alice', claimed_time='now') - bob = self.make_participant('bob', claimed_time='now') - alice.set_tip_to(bob, "1.00") - actual = self.client.GET("/~alice/giving/", auth_as="alice").body - expected = "bob" - assert expected in actual + alice.set_subscription_to('TheATeam', "1.00") + assert "The A Team" in self.client.GET("/~alice/subscriptions/", auth_as="alice").body def test_giving_page_shows_cancelled(self): + self.make_team(is_approved=True) alice = self.make_participant('alice', claimed_time='now') - bob = self.make_participant('bob', claimed_time='now') - alice.set_tip_to(bob, "1.00") - alice.set_tip_to(bob, "0.00") - actual = self.client.GET("/~alice/giving/", auth_as="alice").body - assert "bob" in actual - assert "Cancelled" in actual + alice.set_subscription_to('TheATeam', "1.00") + alice.set_subscription_to('TheATeam', "0.00") + assert "Cancelled" in self.client.GET("/~alice/subscriptions/", auth_as="alice").body def test_new_participant_can_edit_profile(self): self.make_participant('alice', claimed_time='now') diff --git a/www/~/%username/giving/index.html.spt b/www/~/%username/subscriptions/index.html.spt similarity index 54% rename from www/~/%username/giving/index.html.spt rename to www/~/%username/subscriptions/index.html.spt index 8da63e02a2..51867af7f2 100644 --- a/www/~/%username/giving/index.html.spt +++ b/www/~/%username/subscriptions/index.html.spt @@ -5,50 +5,50 @@ from datetime import timedelta [-----------------------------------------------------------------------------] participant = get_participant(state, restrict=True) -tips, total = participant.get_giving_for_profile() +subscriptions, total = participant.get_subscriptions_for_profile() title = participant.username -subhead = _("Giving") +subhead = _("Subscriptions") recently = utcnow() - timedelta(days=30) -cancelled_tips = [x for x in tips if x.amount == 0 and x.mtime >= recently] +cancelled_subscriptions = [x for x in subscriptions if x.amount == 0 and x.mtime >= recently] # don't filter until after cancelled are looked at -tips = [t for t in tips if t.amount > 0] +subscriptions = [s for s in subscriptions if s.amount > 0] tabs = { 'active': { - 'tips': tips, - 'ntips': len(tips), + 'subscriptions': subscriptions, + 'nsubscriptions': len(subscriptions), 'name': _("Active"), 'note': None, 'total': total }, 'cancelled': { - 'tips': cancelled_tips, - 'ntips': len(cancelled_tips), + 'subscriptions': cancelled_subscriptions, + 'nsubscriptions': len(cancelled_subscriptions), 'name': _("Cancelled"), - 'note': _("These are tips that you recently cancelled."), + 'note': _("These are subscriptions that you recently cancelled."), 'total': 0 } } [-----------------------------------------------------------------------------] -{% from 'templates/giving-table.html' import giving_table with context %} +{% from 'templates/subscriptions-table.html' import subscriptions_table with context %} {% extends "templates/profile.html" %} {% block scripts %} - + {{ super() }} {% endblock %} {% block content %} -
+
-

{{ _("You give {0} every week.", format_currency(participant.giving, "USD")) }}

+

{{ _("You pay {0} every week.", format_currency(participant.giving, "USD")) }}

-

{{ _("Tips") }}

+

{{ _("Subscriptions") }}

{{ _("Recipient") }}{{ _("Team") }}{{ _("Amount ($)") }}
- {% if state == 'unclaimed' %} - - {{ tip.user_name }} - {% else %} - {{ tip.tippee }} - {% endif %} + + {{ subscription.team_name }} {{ tip.amount }}{{ subscription.amount }}{{ to_age(tip.mtime) }}{{ to_age(tip.ctime) }}{{ to_age(subscription.mtime) }}{{ to_age(subscription.ctime) }}