diff --git a/410.spt b/410.spt new file mode 100644 index 0000000000..5707944f41 --- /dev/null +++ b/410.spt @@ -0,0 +1,21 @@ +[---] +[---] text/html via jinja2 +{% extends "templates/base.html" %} +{% block heading %}

Closed

{% endblock %} +{% block box %} + +
+ +

The account owner has closed this account.

+ + {% if user.ANON %} +

Are you the account owner?

+ +

{% include "templates/sign-in-using.html" %} to reopen your account.

+ {% endif %} + +
+ + + +{% endblock %} diff --git a/bin/deactivate.py b/bin/deactivate.py deleted file mode 100644 index fb1e5aea12..0000000000 --- a/bin/deactivate.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python -"""The final rename and clear step for canceling an account. - -If the account has a balance or is involved in active tips, -this script will report the problem and abort without making any update. - -If the first eight digits of the account's API key are not given or do not match, -this script will report the problem and abort without making any update. - -Usage: - - [gittip] $ heroku config -s -a gittip | foreman run -e /dev/stdin ./env/bin/python ./bin/deactivate.py "username" [first-eight-of-api-key] - -""" -from __future__ import print_function - -import sys - -from gittip import wireup -from gittip.models.participant import Participant - - -username = sys.argv[1] # will fail with KeyError if missing -if len(sys.argv) < 3: - first_eight = "unknown!" -else: - first_eight = sys.argv[2] - -db = wireup.db(wireup.env()) - - -# Ensure that balance and tips have been dealt with. -# ================================================== - -target = Participant.from_username(username) - -INCOMING = """ - SELECT count(*) - FROM current_tips - WHERE tippee = %s - AND amount > 0 -""" - -FIELDS = """ - SELECT username, username_lower, api_key, claimed_time - FROM participants - WHERE username = %s -""" - -incoming = db.one(INCOMING, (username,)) -fields = db.one(FIELDS, (username,)) - -print("Current balance ", target.balance) -print("Incoming tip count ", incoming) -print(fields) - -assert target.balance == 0 -assert incoming == 0 -if fields.api_key == None: - assert first_eight == "None" -else: - assert fields.api_key[0:8] == first_eight - - -# Archive the participant record. -# =============================== - -deactivated_name = "deactivated-" + username -print("Renaming " + username + " to " + deactivated_name) - -RENAME = """ - UPDATE participants - SET claimed_time = null - , session_token = null - , username = %s - , username_lower = %s - WHERE username = %s -""" - -print(RENAME % (deactivated_name, deactivated_name.lower(), username)) - -db.run(RENAME, (deactivated_name, deactivated_name.lower(), username)) - -print("All done.") diff --git a/bin/final-gift.py b/bin/final-gift.py deleted file mode 100644 index c73af66397..0000000000 --- a/bin/final-gift.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python -"""Distribute a balance as a final gift. This addresses part of #54. - -Usage: - - [gittip] $ heroku config -s -a gittip | foreman run -e /dev/stdin ./env/bin/python ./bin/final-gift.py "username" [first-eight-of-api-key] - -""" -from __future__ import print_function - -import sys - -from gittip import wireup -from gittip.models.participant import Participant - -db = wireup.db(wireup.env()) - -username = sys.argv[1] # will fail with KeyError if missing -tipper = Participant.from_username(username) -if len(sys.argv) < 3: - first_eight = "unknown!" -else: - first_eight = sys.argv[2] - -# Ensure user is legit -FIELDS = """ - SELECT username, username_lower, api_key, claimed_time - FROM participants - WHERE username = %s -""" - -fields = db.one(FIELDS, (username,)) -print(fields) - -if fields.api_key == None: - assert first_eight == "None" -else: - assert fields.api_key[0:8] == first_eight - -print("Distributing {} from {}.".format(tipper.balance, tipper.username)) -tipper.distribute_balance_as_final_gift() diff --git a/bin/untip.py b/bin/untip.py deleted file mode 100644 index 17bf953043..0000000000 --- a/bin/untip.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python -"""Zero out tips to a given user. This is a workaround for #1469. - -Usage: - - [gittip] $ heroku config -s -a gittip | foreman run -e /dev/stdin ./env/bin/python ./scripts/untip.py "username" - -""" -from __future__ import print_function - -import sys - -from gittip import wireup -from gittip.models.participant import Participant - - -tippee = sys.argv[1] # will fail with KeyError if missing -db = wireup.db(wireup.env()) -Participant.from_username(tippee).clear_tips_receiving() diff --git a/gittip/models/participant.py b/gittip/models/participant.py index c301b4730a..8e4d76015f 100644 --- a/gittip/models/participant.py +++ b/gittip/models/participant.py @@ -149,6 +149,14 @@ def set_session_expires(self, expires): self.set_attributes(session_expires=expires) + # Suspiciousness + # ============== + + @property + def is_whitelisted(self): + return self.is_suspicious is False + + # Claimed-ness # ============ @@ -236,22 +244,19 @@ def set_as_claimed(self): self.set_attributes(claimed_time=claimed_time) - # Canceling - # ========= + # Closing + # ======= class UnknownDisbursementStrategy(Exception): pass - def cancel(self, disbursement_strategy): - """Cancel the participant's account. + def close(self, disbursement_strategy): + """Close the participant's account. """ with self.db.get_cursor() as cursor: - if disbursement_strategy == None: pass # No balance, supposedly. final_check will make sure. elif disbursement_strategy == 'bank': self.withdraw_balance_to_bank_account(cursor) - elif disbursement_strategy == 'upstream': - self.refund_to_patrons(cursor) elif disbursement_strategy == 'downstream': # This in particular needs to come before clear_tips_giving. self.distribute_balance_as_final_gift(cursor) @@ -290,10 +295,6 @@ def withdraw_balance_to_bank_account(self, cursor): ) # XXX Records the exchange using a different cursor. :-/ - def refund_balance_to_patrons(self, cursor): - raise NotImplementedError - - class NoOneToGiveFinalGiftTo(Exception): pass def distribute_balance_as_final_gift(self, cursor): diff --git a/gittip/utils/__init__.py b/gittip/utils/__init__.py index a8711d9d86..feeb647d3a 100644 --- a/gittip/utils/__init__.py +++ b/gittip/utils/__init__.py @@ -352,6 +352,9 @@ def get_participant(request, restrict=True): canonicalize(request.line.uri.path.raw, '/', participant.username, slug, qs) + if participant.is_closed: + raise Response(410) + if participant.claimed_time is None: # This is a stub participant record for someone on another platform who diff --git a/js/gittip/account.js b/js/gittip/account.js index 52da2336a8..4ae3c6958f 100644 --- a/js/gittip/account.js +++ b/js/gittip/account.js @@ -163,4 +163,11 @@ Gittip.account.init = function() { return false; }); + + // Wire up close knob. + // =================== + + $('button.close-account').click(function() { + window.location.href = './close'; + }); }; diff --git a/scss/buttons-knobs.scss b/scss/buttons-knobs.scss index d7a9a8283c..18e4e78c4c 100644 --- a/scss/buttons-knobs.scss +++ b/scss/buttons-knobs.scss @@ -48,11 +48,12 @@ button.selected:hover, button.selected.drag, } button.join-leave[data-is-member="true"], -.button.join-leave[data-is-member="true"] { +.button.join-leave[data-is-member="true"], +button.close-account { font-weight: normal; background: none; color: $red; - text-decoration: underline; + border: 1px solid $red; &:hover { background: $red; color: white; diff --git a/scss/layout.scss b/scss/layout.scss index c76e9571c5..d6f3450df4 100644 --- a/scss/layout.scss +++ b/scss/layout.scss @@ -420,7 +420,14 @@ padding: 0; text-transform: uppercase; } - input { + input[type="radio"] + label { + display: inline; + margin: 0; + } + input[type="radio"]:disabled + label { + color: #888; + } + input:not([type]), input[type="text"] { font: normal 11pt/14pt $Helvetica; width: 292px; margin: 0; @@ -431,6 +438,9 @@ input.disabled { color: $light-brown; } + input[type="radio"] { + vertical-align: middle; + } .half input { width: 137px; diff --git a/tests/py/fixtures/TestCanceling.yml b/tests/py/fixtures/TestClosing.yml similarity index 63% rename from tests/py/fixtures/TestCanceling.yml rename to tests/py/fixtures/TestClosing.yml index 2ebb302598..4ef3423739 100644 --- a/tests/py/fixtures/TestCanceling.yml +++ b/tests/py/fixtures/TestClosing.yml @@ -15,10 +15,10 @@ response: body: {string: "{\n \"customers\": [\n {\n \"name\": null,\n \"\ links\": {\n \"source\": null,\n \"destination\": null\n \ - \ },\n \"updated_at\": \"2014-06-06T02:27:17.902166Z\",\n \"created_at\"\ - : \"2014-06-06T02:27:17.770697Z\",\n \"dob_month\": null,\n \"id\"\ - : \"CUCrb24TQtaUrFR69oRDmLP\",\n \"phone\": null,\n \"href\": \"\ - /customers/CUCrb24TQtaUrFR69oRDmLP\",\n \"merchant_status\": \"no-match\"\ + \ },\n \"updated_at\": \"2014-06-06T16:33:38.386693Z\",\n \"created_at\"\ + : \"2014-06-06T16:33:38.274538Z\",\n \"dob_month\": null,\n \"id\"\ + : \"CU2qN2ov0SNIumvBA8iWOm81\",\n \"phone\": null,\n \"href\": \"\ + /customers/CU2qN2ov0SNIumvBA8iWOm81\",\n \"merchant_status\": \"no-match\"\ ,\n \"meta\": {},\n \"dob_year\": null,\n \"address\": {\n\ \ \"city\": null,\n \"line2\": null,\n \"line1\": null,\n\ \ \"state\": null,\n \"postal_code\": null,\n \"country_code\"\ @@ -36,19 +36,19 @@ \ \"customers.reversals\": \"/customers/{customers.id}/reversals\",\n \"\ customers.orders\": \"/customers/{customers.id}/orders\",\n \"customers.credits\"\ : \"/customers/{customers.id}/credits\"\n }\n}"} - headers: ["Content-Type: application/json\r\n", "Date: Fri, 06 Jun 2014 02:27:17\ - \ GMT\r\n", "Server: ngx_openresty/1.2.6.3\r\n", "X-Balanced-Guru: OHM1459e22eed2211e3ab2502a1fe52a36c\r\ - \n", "x-balanced-host: bapi-integration-prod-8u30f7-10-3-4-8\r\n", "x-balanced-marketplace:\ + headers: ["Content-Type: application/json\r\n", "Date: Fri, 06 Jun 2014 16:33:38\ + \ GMT\r\n", "Server: ngx_openresty/1.2.6.3\r\n", "X-Balanced-Guru: OHM4fe477e8ed9811e39cde02a1fe53e539\r\ + \n", "x-balanced-host: bapi-integration-prod-8u30f7-10-3-5-201\r\n", "x-balanced-marketplace:\ \ TEST-MP1wTyxDAJKCaWPjBS4Oli4d\r\n", "x-balanced-merchant: TEST-MR1wiJbFKXFmlmVIyaPiduUV\r\ \n", "x-balanced-revision: 1.1\r\n", "x-balanced-software-build: 1.2.50\r\n", - "X-Midlr-Version: 2\r\n", "x-newrelic-app-data: PxQFWFNXCQYTVVhWAwQDVUYdFhE1AwE2QgNWEVlbQFtcCxYxSBVbDQoZVA4IF0pcXAgEEBhSQhUQXltWEBVNAUIHWRZdGhgSA0kZUR9TAFdQBA9RX1cABQNcWFMBHRdUSBEUUFsCBltTUlxVDg0MBVFVC0MdQVUDCEVSPA==\r\ - \n", "Content-Length: 1591\r\n", "Connection: keep-alive\r\n"] + "X-Midlr-Version: 2\r\n", "x-newrelic-app-data: PxQFWFNXCQYTVVhWAwQDVUYdFhE1AwE2QgNWEVlbQFtcCxYxSBVbDQoZVA4IF0pcXAgEEBhSQhUQXltWEBVNAUIHWRZdGhgSA0kZUR9QC1ZcDgJRVFYIAQRQUFYHBxtGVh0WEQFSAFZWW10DDw0CUFVSW1ETTRMEBVpEBDs=\r\ + \n", "Content-Length: 1593\r\n", "Connection: keep-alive\r\n"] status: {code: 201, message: CREATED} - request: !!python/object:vcr.request.Request body: '{"routing_number": "321174851", "account_type": "checking", "account_number": - "9900000001", "name": "alice"}' + "9900000001", "name": "Alice G. Krebs"}' headers: !!python/object/apply:__builtin__.frozenset - - - !!python/tuple [Content-Length, !!python/unicode '108'] + - - !!python/tuple [Content-Length, !!python/unicode '117'] - !!python/tuple [User-Agent, !!python/unicode 'balanced-python/1.0.1beta2'] - !!python/tuple [Authorization, !!python/unicode 'Basic YWstdGVzdC1Xbnh5QTA2N1pYd0JsYW5xbWdOZm90bUFPeUpsRm5rZTpOb25l'] - !!python/tuple [Content-Type, !!python/unicode 'application/json'] @@ -62,16 +62,16 @@ response: body: {string: "{\n \"bank_accounts\": [\n {\n \"routing_number\": \"\ 321174851\",\n \"bank_name\": \"SAN MATEO CREDIT UNION\",\n \"account_type\"\ - : \"checking\",\n \"name\": \"alice\",\n \"links\": {\n \"\ - customer\": null,\n \"bank_account_verification\": null\n },\n\ - \ \"can_credit\": true,\n \"created_at\": \"2014-06-06T02:27:18.955306Z\"\ + : \"checking\",\n \"name\": \"Alice G. Krebs\",\n \"links\": {\n\ + \ \"customer\": null,\n \"bank_account_verification\": null\n\ + \ },\n \"can_credit\": true,\n \"created_at\": \"2014-06-06T16:33:39.496116Z\"\ ,\n \"fingerprint\": \"5f0ba9fa3f1122ef13b944a40abfe44e7eba9e16934e64200913cb4c402ace14\"\ - ,\n \"updated_at\": \"2014-06-06T02:27:18.955308Z\",\n \"href\"\ - : \"/bank_accounts/BADLD5GZ8Uv8SXloInEFuKB\",\n \"meta\": {},\n \ - \ \"account_number\": \"xxxxxx0001\",\n \"address\": {\n \"city\"\ + ,\n \"updated_at\": \"2014-06-06T16:33:39.496118Z\",\n \"href\"\ + : \"/bank_accounts/BA2s9HPLLimkYjd9Xufqeb3f\",\n \"meta\": {},\n \ + \ \"account_number\": \"xxxxxx0001\",\n \"address\": {\n \"city\"\ : null,\n \"line2\": null,\n \"line1\": null,\n \"state\"\ : null,\n \"postal_code\": null,\n \"country_code\": null\n\ - \ },\n \"can_debit\": false,\n \"id\": \"BADLD5GZ8Uv8SXloInEFuKB\"\ + \ },\n \"can_debit\": false,\n \"id\": \"BA2s9HPLLimkYjd9Xufqeb3f\"\ \n }\n ],\n \"links\": {\n \"bank_accounts.credits\": \"/bank_accounts/{bank_accounts.id}/credits\"\ ,\n \"bank_accounts.bank_account_verifications\": \"/bank_accounts/{bank_accounts.id}/verifications\"\ ,\n \"bank_accounts.customer\": \"/customers/{bank_accounts.customer}\"\ @@ -80,53 +80,53 @@ \n }\n}"} headers: ["access-control-allow-headers: Content-Type\r\n", "access-control-allow-methods:\ \ POST, OPTIONS\r\n", "access-control-allow-origin: *\r\n", "Content-Type:\ - \ application/json\r\n", "Date: Fri, 06 Jun 2014 02:27:19 GMT\r\n", "Server:\ - \ ngx_openresty/1.2.6.3\r\n", "X-Balanced-Guru: OHM14d5b37ced2211e3a52c02a1fe53e539\r\ - \n", "x-balanced-host: bapi-integration-prod-8u30f7-10-3-5-201\r\n", "x-balanced-marketplace:\ + \ application/json\r\n", "Date: Fri, 06 Jun 2014 16:33:39 GMT\r\n", "Server:\ + \ ngx_openresty/1.2.6.3\r\n", "X-Balanced-Guru: OHM506b90b6ed9811e3a82802a1fe52a36c\r\ + \n", "x-balanced-host: bapi-integration-prod-8u30f7-10-3-4-8\r\n", "x-balanced-marketplace:\ \ TEST-MP1wTyxDAJKCaWPjBS4Oli4d\r\n", "x-balanced-merchant: TEST-MR1wiJbFKXFmlmVIyaPiduUV\r\ \n", "x-balanced-revision: 1.1\r\n", "x-balanced-software-build: 1.2.50\r\n", - "X-Midlr-Version: 2\r\n", "x-newrelic-app-data: PxQFWFNXCQYTVVhWAwQDVUYdFhE1AwE2QgNWEVlbQFtcCxYxSBVbDQoZVA4IF0pcXAgEEBhTVggPbldQAQkWDEQRFgFKXVVGVkcVQQFNE1JKBQJWUlcAAgBVUVYEBgdXVQYaE1BXVk4QQFtWDwAHAQQECQQJAFEBUwAVTUYFWV9DATw=\r\ - \n", "Content-Length: 1281\r\n", "Connection: keep-alive\r\n"] + "X-Midlr-Version: 2\r\n", "x-newrelic-app-data: PxQFWFNXCQYTVVhWAwQDVUYdFhE1AwE2QgNWEVlbQFtcCxYxSBVbDQoZVA4IF0pcXAgEEBhTVggPbldQAQkWDEQRFgFKXVVGVkcVQQFNE1JKAgVZVFQPCgZdUlQHAgJXUQcaE1BUV04QQF0BXQgGBlUACFgIWQNUUQMVTUYFWV9DATw=\r\ + \n", "Content-Length: 1292\r\n", "Connection: keep-alive\r\n"] status: {code: 201, message: CREATED} - request: !!python/object:vcr.request.Request body: '{"routing_number": "321174851", "bank_name": "SAN MATEO CREDIT UNION", - "account_type": "checking", "name": "alice", "links": {"customer": "/customers/CUCrb24TQtaUrFR69oRDmLP"}, - "can_credit": true, "created_at": "2014-06-06T02:27:18.955306Z", "fingerprint": - "5f0ba9fa3f1122ef13b944a40abfe44e7eba9e16934e64200913cb4c402ace14", "updated_at": - "2014-06-06T02:27:18.955308Z", "meta": {}, "account_number": "xxxxxx0001", "address": - {"city": null, "line2": null, "line1": null, "state": null, "postal_code": null, - "country_code": null}, "can_debit": false, "id": "BADLD5GZ8Uv8SXloInEFuKB"}' + "account_type": "checking", "name": "Alice G. Krebs", "links": {"customer": + "/customers/CU2qN2ov0SNIumvBA8iWOm81"}, "can_credit": true, "created_at": "2014-06-06T16:33:39.496116Z", + "fingerprint": "5f0ba9fa3f1122ef13b944a40abfe44e7eba9e16934e64200913cb4c402ace14", + "updated_at": "2014-06-06T16:33:39.496118Z", "meta": {}, "account_number": "xxxxxx0001", + "address": {"city": null, "line2": null, "line1": null, "state": null, "postal_code": + null, "country_code": null}, "can_debit": false, "id": "BA2s9HPLLimkYjd9Xufqeb3f"}' headers: !!python/object/apply:__builtin__.frozenset - - - !!python/tuple [Content-Length, !!python/unicode '581'] - - !!python/tuple [User-Agent, !!python/unicode 'balanced-python/1.0.1beta2'] - - !!python/tuple [Authorization, !!python/unicode 'Basic YWstdGVzdC1Xbnh5QTA2N1pYd0JsYW5xbWdOZm90bUFPeUpsRm5rZTpOb25l'] + - - !!python/tuple [User-Agent, !!python/unicode 'balanced-python/1.0.1beta2'] + - !!python/tuple [Content-Length, !!python/unicode '592'] - !!python/tuple [Content-Type, !!python/unicode 'application/json'] - !!python/tuple [Accept-Encoding, 'gzip, deflate, compress'] + - !!python/tuple [Authorization, !!python/unicode 'Basic YWstdGVzdC1Xbnh5QTA2N1pYd0JsYW5xbWdOZm90bUFPeUpsRm5rZTpOb25l'] - !!python/tuple [accept, !!python/unicode 'application/vnd.api+json;revision=1.1'] host: api.balancedpayments.com method: PUT - path: /bank_accounts/BADLD5GZ8Uv8SXloInEFuKB + path: /bank_accounts/BA2s9HPLLimkYjd9Xufqeb3f port: 443 protocol: https response: body: string: !!binary | - H4sIAAAAAAAAA5VTXW+bMBR9769APG+JbRwCeUuTdIrWpV2aSFOrCRlzWa0QExlTLYr47zMkFELT - qkOIh3su1+fj+nBlWXbI5CZgnKe51Jk9sp5M0bIO1dfAKs21kH8CmW9DUAa3HYLxkHoDbH+pm6oZ - km2hxB/GC+vHeDW7sybL2XS+staL+d2iaT6dFej9rurnz8A35oimo57EEsGhKSdCbkqGNTfDjueZ - TrdHXpP1RIWErn5qtlY3S9dPl9Pt7f3r/x2twQsoEQvOtEilmSrzJDnpKV6FcSYDriAS2nRolUOD - KGAaooCViE0Qpl+Ra94VIiMyHGGv5w8GDnIfG/6xEQlqp4Ss/hnEKGR+zJwYY0Igxk7oU8ooYmEM - lMIQDAzY9R0KLiUI+djhIeUUEcYB02Zwvos+4kJQz0XUGw5bXJ4VxCWJ/ln8/evx9HY6+PborV+8 - h19JOpezm/z7dXPUFjQrI2gsquNsFuRv9SCEWhvCokhB1k1P6P3J+dpXE5KJGcjlMn5bzrSR/ra8 - Sw2QBDyNLoDVrqt9G72YfARhFXzMkqxJXkSlce85VQ0qzPd3KakU09rZ88vWO25WaUo3h8NZLD0R - Ff26+ehUZ1K7/WyxPzm8fReyU9pdsq2r1q/vXdbvMK2B4vKQytFPcjr1/p/eysszNV2G71pV2Caw - 4qr4B7tFnpIWBQAA + H4sIAAAAAAAAA5VTW2+bMBR+769APG+JbVwS8pZ21RatS6Y11W6akDHHqxcuqTHRooj/PkPCDDSd + OoR48Hc4/i7nHC4cx41YtgkZ53mZ6cKdOd/NoeMcmq+BVV5qmf0MszKNQBnc9QjGEzq9xO6rtqjp + kbEUavxuvnQ+zNc3K+f6082bxdq5Xy5WS1t8uivU+21Tzx+Ab8wVtqLtNE8kB+ftyHmvICosnshs + U1NtSRqavCx0nh4JXt+TxyXJd+huuSjT3dV8Kj+v0qnlO1Ad7kBJITnTMs9M26xMkpOy6q9EzrKQ + K4ilNhValWARBUxDHLIacQnC9DXyzbvG/szzZl4wooGPsf/NChBGLqitklnzz6VAEQsE8wTGhIDA + XhRQyihikQBKYQIGBuwHHgWfEoQC7PGIcooI44CpbVxu439xoWhE6NRDkw6XBwWiJjHuDcL4ak6K + 4N3H21uZbr7+ioMvpXiEyBP2rhQ0q0OwHrXJ2ln53TwIoc6wsDhWUAzzk3p/sr411qRkggZy/hg/ + PS600f70eJsbIAl5Hp8Bm7FX+y56NvoYoiZ5wZLCRi/j2rlnrWo6Veb7o9ZUq+mMbX/xRsfZql0Z + JnHoBTOScTVui49WDTp1y3uj/cLm3W1ol25ItrNt43b1ivGAaQtUp5kZNGksfSGnU+3/6W287KkZ + MnzWqso1gVUX1R/fcKEyIgUAAA== headers: ["Content-Encoding: gzip\r\n", "Content-Type: application/json\r\n", - "Date: Fri, 06 Jun 2014 02:27:20 GMT\r\n", "Server: ngx_openresty/1.2.6.3\r\n", - "X-Balanced-Guru: OHM160903e8ed2211e3aecc02b12035401b\r\n", "X-Balanced-Host:\ + "Date: Fri, 06 Jun 2014 16:33:40 GMT\r\n", "Server: ngx_openresty/1.2.6.3\r\n", + "X-Balanced-Guru: OHM5108d222ed9811e3b7da06429171ffad\r\n", "X-Balanced-Host:\ \ bapi-integration-prod-8u30f7-10-3-4-8\r\n", "X-Balanced-Marketplace: TEST-MP1wTyxDAJKCaWPjBS4Oli4d\r\ \n", "X-Balanced-Merchant: TEST-MR1wiJbFKXFmlmVIyaPiduUV\r\n", "X-Balanced-Revision:\ - \ 1.1\r\n", "X-Balanced-Software-Build: 1.2.50\r\n", "Content-Length: 525\r\ + \ 1.1\r\n", "X-Balanced-Software-Build: 1.2.50\r\n", "Content-Length: 532\r\ \n", "Connection: keep-alive\r\n"] status: {code: 200, message: OK} - request: !!python/object:vcr.request.Request @@ -139,27 +139,27 @@ - !!python/tuple [User-Agent, !!python/unicode 'balanced-python/1.0.1beta2'] host: api.balancedpayments.com method: GET - path: /customers/CUCrb24TQtaUrFR69oRDmLP + path: /customers/CU2qN2ov0SNIumvBA8iWOm81 port: 443 protocol: https response: body: string: !!binary | - H4sIAAAAAAAAA41Uy27bMBC85ysEnZtIFgwJ9jVFTz20QXJpEAgrkoWISKTBRxDD0L93aUsyLapl - AcOH5cxwd3bE012SpMRqI3umdLpPXrGQJKfzPx4J6BlWhe26L1Ot4+LdQScQwrS0iiyBWKdMGy7A - cClGlVFkmNXsgYJhtAaDiLTIN9v7vMTfc17si2q/qR52ebEpy1/pTCGKRShVlZe7yqNQ2dS9FKZd - DsOpu/bx5VE1xfb5p4EX9e2p3Mmnr/33H9crD60Uy/nSVrHfjp3NBmZRIbSZtCBMrQ0Y62xMhbzv - wZD2elvPDDiDry65/o8M1LJ9oFQxvdgG4ea4AOIucG2sWC9vwrJrbzkwihwkHnQ1kXTlkEgrjDr6 - p8G6G6uxD63rtWRpLeoOtNkux2Q98C4o8ptQDXjXm8uIm9QL6DXeD3NM0wxdO2dWZ6d5e+P5MC7C - IxJQtG5lR88L8/btkTkdMg93Saun0YB4r4GcTYrI3EIDJcoaHpMYMStc/4v8iw/eZ7tiBvs0TAlM - wf8NE8KDppxtEUsukIBJuT5YwyLkGRXwjQKhgbgHKqJxgwx08CWwIjbDBFphf+DrC12kBcUmWKAg - Fb283/8I54gJuPic0mieJhB+X8Pd8Ae3UkW6NwYAAA== + H4sIAAAAAAAAA41Uy27bMBC85ysEnZPIllxX9S3JqZf00BYFWhTCmmQgIhLp8mHUMPTvXdqSTItK + GUDQYTkz5M4OebxJkpRYbWTLlE43yS8sJMnx9MclAS3DqrBNczvUGi5eHXQAIUxLq8gUiHXKtOEC + DJeiV+lFulHN7igYRiswiEjzxXJ1t1jj92253hTFpijvi3K9/lT8TEcKUSxCyT+uPhSlR6FyW7VS + mHraDKdu26fv+Z/nXO4XX58/23b/+FDyH1/acnnZc1dLMW0wrRV7cfRsdDCLK6HRpAZhKm3AWGdk + KuRdC4bUl+1aZsBZfPHJdXBgoKYNAKWK6ck8CDeHCRCngYNj+Xx5GZbd8aYdo8hO4kJTEUlnFom0 + wqiDvxoMfGs1nkPrai5bWouqAW1W0zZZC7wJivwqVh3u9dulxHXqRfQS8PsxqGmGrp1Sq7PjOL5+ + vesH4REJKFrVsqGngXkD98icdpmHO+fV09iCeK2AnEyKyFxDAyXKtjwm0WNmuP6dfMMH7+LOmMH+ + GqYEpuB9zYTw4FDOtoglZ0jApFzvrGER8ogK+EaB0EDcExXRuEIGOvgUWBHrYQDNsPf4/kITOYJi + AyxQkIqeX/D/hLPHBFx8UGk0TwMI71d30/0DZQmTSTkGAAA= headers: ["Content-Encoding: gzip\r\n", "Content-Type: application/json\r\n", - "Date: Fri, 06 Jun 2014 02:27:21 GMT\r\n", "Server: ngx_openresty/1.2.6.3\r\n", - "X-Balanced-Guru: OHM166d7396ed2211e3a4e806429171ffad\r\n", "X-Balanced-Host:\ - \ bapi-integration-prod-8u30f7-10-3-5-201\r\n", "X-Balanced-Marketplace: TEST-MP1wTyxDAJKCaWPjBS4Oli4d\r\ + "Date: Fri, 06 Jun 2014 16:33:41 GMT\r\n", "Server: ngx_openresty/1.2.6.3\r\n", + "X-Balanced-Guru: OHM5186cdb2ed9811e3ac8902b12035401b\r\n", "X-Balanced-Host:\ + \ bapi-integration-prod-8u30f7-10-3-4-8\r\n", "X-Balanced-Marketplace: TEST-MP1wTyxDAJKCaWPjBS4Oli4d\r\ \n", "X-Balanced-Merchant: TEST-MR1wiJbFKXFmlmVIyaPiduUV\r\n", "X-Balanced-Revision:\ - \ 1.1\r\n", "X-Balanced-Software-Build: 1.2.50\r\n", "Content-Length: 487\r\ + \ 1.1\r\n", "X-Balanced-Software-Build: 1.2.50\r\n", "Content-Length: 491\r\ \n", "Connection: keep-alive\r\n"] status: {code: 200, message: OK} - request: !!python/object:vcr.request.Request @@ -172,29 +172,29 @@ - !!python/tuple [User-Agent, !!python/unicode 'balanced-python/1.0.1beta2'] host: api.balancedpayments.com method: GET - path: /customers/CUCrb24TQtaUrFR69oRDmLP/bank_accounts?limit=10&offset=0 + path: /customers/CU2qN2ov0SNIumvBA8iWOm81/bank_accounts?limit=10&offset=0 port: 443 protocol: https response: body: string: !!binary | - H4sIAAAAAAAAA61UXWvbMBR9768wftjTlki28gllpEk6ytp0SxMYHcPI8vUq4thBlkND8H+fpNix - nSajDzXG4Pt57j2Hu7+yLNun8cqjjCVZLFN7aP1WRsvam69yiySTPP7rxdnaB6H8tutg3CP9DrY/ - l0GmRkzXoP1Po5n1MFpMH63xfDq5W1jL2d3jrAouenlytzHx7AXYSrWoIspKNOIMKnPE45VGWGJT - 6FiWymR9wDVejoXvkMVPSZfidt4dJPPJ+v7HMf9kVm8LgoecUcmTWFWNsygq5smPgzEae0xAwKWK - kCKDyiOASgg8qj22gzD5grrqXSBn6PSGuN8adDou6j5X+EM1JIiN4LHJ6YTIp4OQuiHGjgMhdv0B - IZQg6odACPRAuQF3By6BLnEQGmCX+YQR5FAGmFSFs03wPywOanUR6fd6NSwvAkINot2gv30zmtxP - Ot+e+8tt/+lXlNzF09vs+03Vag2SagqqFZV0VgJ5NQ9CqKYQGgQC0lP2uNwVmy/3qkhSNINz3ozf - mlOpRn9r3iTKEXksCc44jdbFru49y3wAviE+pFFaMc8DvbhLmzKFcvX9o0eyy30Zsx3R1FDfLnWb - ti+otsnL14ivubzG6FMShinIa1QwYsfwqitq8R5WaB+Z/bAeprlqglHR4gBCWUrDRsCWJ5mmt4ZE - JooCnVekhVx82Phqn0aCWi21o9C8Zi1DoEZ1qvN9Q/YtHuTtIrbA2vTX/xp3452166cmLalrtqhf - spo8ToCWUfmFIuZUvRPU4a5dgHNxYrPMxjztE4wXU3Nbs3aV/wN20hiEdwYAAA== + H4sIAAAAAAAAA61UbWvbMBD+3l9h/GGftkSy1SQOlJF2ZSvrkrGm7I1hZPm0avFLKsuhIeS/T1Ls + 2k6T0UFDMPie891z9zzc5sRx3Ihmi5AylpeZKtyx81MHHWdjnxqWealE9jvMyjQCqXHX9zAektEp + dl/XSbZGRlMw+M1k6nyazC9nzsWXy3dXc+d2ejWbNslVr1Ctlzaf3QFb6BZNRl1pkggGzvue81FC + VDR4IrKFoVqT1DRZWag83RG8uPXup16+QjfTqzJdnU9G4ussHTV896YOVyAFF4wqkWe6bFYmSTXZ + 9nFERrOQSYiF0hlKltAgEqiCOKQGcT2EyRs00P85Hox9f+wHPRIMMB78aAbgelyQSyky+80pRxEN + OPU5xp4HHPtRQAgliEYcCIEhaBjwIPAJDIiHUIB9FhFGkEcZYNIULpfxv7gQ1PPIyEfDFpc7CdyQ + 6HeM0D+feEXw4fP1tUgX3//EwbeS30Pk86ZXCooaEZod1co2XnmwP4RQyyw0jiUU+/oJta5WXy9W + q6SFBu9wGD8NF0rP/jS8zDWQhCyPD4DW9nLdRg9KH0Nklec0KRrpRWw2d3RVttJWP3+Zmdx6YTbs + JrSw4vdr6xb9Y8btSvM2EalQZxi9yjkvQJ2hShM3gwdT0vh3t0T3UdyXa2K76y4YVT12LHSkDiwl + rEReGoVbVFSuVTDfVZ9xIV9uAXql1obGMa3T0D1uPSuiobVv9k3H+z0Rb/tVbkW2i7ffOsfjmbXb + 96Y+a3tU2/es5ZA9onXWtnZAl+fuXj2TVJ38fxPbZXbm6e9xPLqsrWtUO9n+BfdCL06GBgAA headers: ["Content-Encoding: gzip\r\n", "Content-Type: application/json\r\n", - "Date: Fri, 06 Jun 2014 02:27:21 GMT\r\n", "Server: ngx_openresty/1.2.6.3\r\n", - "X-Balanced-Guru: OHM16bd64f0ed2211e3ac8902b12035401b\r\n", "X-Balanced-Host:\ - \ bapi-integration-prod-8u30f7-10-3-5-201\r\n", "X-Balanced-Marketplace: TEST-MP1wTyxDAJKCaWPjBS4Oli4d\r\ + "Date: Fri, 06 Jun 2014 16:33:41 GMT\r\n", "Server: ngx_openresty/1.2.6.3\r\n", + "X-Balanced-Guru: OHM51d1db18ed9811e38ad106429171ffad\r\n", "X-Balanced-Host:\ + \ bapi-integration-prod-8u30f7-10-3-4-8\r\n", "X-Balanced-Marketplace: TEST-MP1wTyxDAJKCaWPjBS4Oli4d\r\ \n", "X-Balanced-Merchant: TEST-MR1wiJbFKXFmlmVIyaPiduUV\r\n", "X-Balanced-Revision:\ - \ 1.1\r\n", "X-Balanced-Software-Build: 1.2.50\r\n", "Content-Length: 616\r\ + \ 1.1\r\n", "X-Balanced-Software-Build: 1.2.50\r\n", "Content-Length: 624\r\ \n", "Connection: keep-alive\r\n"] status: {code: 200, message: OK} - request: !!python/object:vcr.request.Request @@ -208,27 +208,27 @@ - !!python/tuple [accept, !!python/unicode 'application/vnd.api+json;revision=1.1'] host: api.balancedpayments.com method: POST - path: /bank_accounts/BADLD5GZ8Uv8SXloInEFuKB/credits + path: /bank_accounts/BA2s9HPLLimkYjd9Xufqeb3f/credits port: 443 protocol: https response: body: {string: "{\n \"credits\": [\n {\n \"status\": \"succeeded\",\n\ \ \"description\": \"alice\",\n \"links\": {\n \"customer\"\ - : \"CUCrb24TQtaUrFR69oRDmLP\",\n \"destination\": \"BADLD5GZ8Uv8SXloInEFuKB\"\ - ,\n \"order\": null\n },\n \"updated_at\": \"2014-06-06T02:27:22.635298Z\"\ - ,\n \"created_at\": \"2014-06-06T02:27:22.431185Z\",\n \"transaction_number\"\ - : \"CR813-132-7215\",\n \"failure_reason\": null,\n \"currency\"\ + : \"CU2qN2ov0SNIumvBA8iWOm81\",\n \"destination\": \"BA2s9HPLLimkYjd9Xufqeb3f\"\ + ,\n \"order\": null\n },\n \"updated_at\": \"2014-06-06T16:33:42.493090Z\"\ + ,\n \"created_at\": \"2014-06-06T16:33:42.183974Z\",\n \"transaction_number\"\ + : \"CR937-364-6500\",\n \"failure_reason\": null,\n \"currency\"\ : \"USD\",\n \"amount\": 1000,\n \"failure_reason_code\": null,\n\ - \ \"meta\": {},\n \"href\": \"/credits/CRHG1lNaScFGB1lgZC2azOx\"\ - ,\n \"appears_on_statement_as\": \"example.com\",\n \"id\": \"CRHG1lNaScFGB1lgZC2azOx\"\ + \ \"meta\": {},\n \"href\": \"/credits/CR2vaCzFWKJc9Ngzf4hRRmbX\"\ + ,\n \"appears_on_statement_as\": \"example.com\",\n \"id\": \"CR2vaCzFWKJc9Ngzf4hRRmbX\"\ \n }\n ],\n \"links\": {\n \"credits.order\": \"/orders/{credits.order}\"\ ,\n \"credits.customer\": \"/customers/{credits.customer}\",\n \"credits.destination\"\ : \"/resources/{credits.destination}\",\n \"credits.reversals\": \"/credits/{credits.id}/reversals\"\ ,\n \"credits.events\": \"/credits/{credits.id}/events\"\n }\n}"} - headers: ["Content-Type: application/json\r\n", "Date: Fri, 06 Jun 2014 02:27:22\ - \ GMT\r\n", "Server: ngx_openresty/1.2.6.3\r\n", "X-Balanced-Guru: OHM172078f6ed2211e3aecc02b12035401b\r\ - \n", "X-Balanced-Host: bapi-integration-prod-8u30f7-10-3-5-201\r\n", "X-Balanced-Marketplace:\ + headers: ["Content-Type: application/json\r\n", "Date: Fri, 06 Jun 2014 16:33:42\ + \ GMT\r\n", "Server: ngx_openresty/1.2.6.3\r\n", "X-Balanced-Guru: OHM52369bc0ed9811e3aecc02b12035401b\r\ + \n", "X-Balanced-Host: bapi-integration-prod-8u30f7-10-3-4-8\r\n", "X-Balanced-Marketplace:\ \ TEST-MP1wTyxDAJKCaWPjBS4Oli4d\r\n", "X-Balanced-Merchant: TEST-MR1wiJbFKXFmlmVIyaPiduUV\r\ \n", "X-Balanced-Revision: 1.1\r\n", "X-Balanced-Software-Build: 1.2.50\r\n", - "Content-Length: 951\r\n", "Connection: keep-alive\r\n"] + "Content-Length: 955\r\n", "Connection: keep-alive\r\n"] status: {code: 201, message: CREATED} diff --git a/tests/py/test_cancel.py b/tests/py/test_close.py similarity index 89% rename from tests/py/test_cancel.py rename to tests/py/test_close.py index c2d0b5360d..bf170e33c8 100644 --- a/tests/py/test_cancel.py +++ b/tests/py/test_close.py @@ -5,16 +5,17 @@ import balanced import pytest +from gittip.billing.payday import Payday from gittip.models.community import Community from gittip.models.participant import Participant from gittip.testing import Harness -class TestCanceling(Harness): +class TestClosing(Harness): - # cancel + # close - def test_cancel_cancels(self): + def test_close_closes(self): alice = self.make_participant('alice', balance=D('10.00')) bob = self.make_participant('bob', claimed_time='now') carl = self.make_participant('carl') @@ -22,15 +23,45 @@ def test_cancel_cancels(self): alice.set_tip_to(bob, D('3.00')) carl.set_tip_to(alice, D('2.00')) - alice.cancel('downstream') + alice.close('downstream') assert carl.get_tip_to('alice') == 0 assert alice.balance == 0 - def test_cancel_raises_for_unknown_disbursement_strategy(self): + def test_close_raises_for_unknown_disbursement_strategy(self): alice = self.make_participant('alice', balance=D('0.00')) with pytest.raises(alice.UnknownDisbursementStrategy): - alice.cancel('cheese') + alice.close('cheese') + + def test_close_page_is_usually_available(self): + self.make_participant('alice', claimed_time='now') + body = self.client.GET('/alice/account/close', auth_as='alice').body + assert 'Personal Information' in body + + def test_close_page_is_not_available_during_payday(self): + Payday(self.db).start() + self.make_participant('alice', claimed_time='now') + body = self.client.GET('/alice/account/close', auth_as='alice').body + assert 'Personal Information' not in body + assert 'Try Again Later' in body + + def test_can_post_to_close_page(self): + alice = self.make_participant('alice', claimed_time='now', balance=7) + bob = self.make_participant('bob', claimed_time='now') + alice.set_tip_to(bob, D('10.00')) + + data = {'disbursement_strategy': 'downstream'} + response = self.client.PxST('/alice/account/close', auth_as='alice', data=data) + assert response.code == 302 + assert response.headers['Location'] == '/alice/' + assert Participant.from_username('alice').balance == 0 + assert Participant.from_username('bob').balance == 7 + + def test_cant_post_to_close_page_during_payday(self): + Payday(self.db).start() + self.make_participant('alice', claimed_time='now') + body = self.client.POST('/alice/account/close', auth_as='alice').body + assert 'Try Again Later' in body # wbtba - withdraw_balance_to_bank_account @@ -50,7 +81,7 @@ def test_wbtba_withdraws_balance_to_bank_account(self): , balanced_customer_href=customer.href ) - alice.cancel('bank') + alice.close('bank') def test_wbtba_raises_NoBalancedCustomerHref_if_no_balanced_customer_href(self): alice = self.make_participant('alice', balance=D('10.00'), is_suspicious=False) diff --git a/www/%username/account/close.spt b/www/%username/account/close.spt new file mode 100644 index 0000000000..8fce29ffdb --- /dev/null +++ b/www/%username/account/close.spt @@ -0,0 +1,153 @@ +from gittip.utils import get_participant +[---] +participant = get_participant(request, restrict=True) +hero = "Close Account" +title = participant.username # used in the title tag +username = participant.username # used in footer shared with on/$platform/ + # pages + +payday_is_running = website.db.one(""" + + SELECT ts_start FROM paydays WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz + +""") is not None + +if POST: + if payday_is_running: + pass # User will get the "Try Again Later" message. + else: + disbursement_strategy = request.body.get('disbursement_strategy') + participant.close(disbursement_strategy) + request.redirect('/%s/' % participant.username) +[---] text/html +{% extends "templates/base.html" %} + +{% block heading %} +

Close Account

+{% endblock %} + +{% block box %} +
+ {% if payday_is_running %} + +
+ +

Try Again Later

+ +

Sorry, we're running payday right now, and we're not set up + to close accounts while payday is running. Please check back in + a few hours.

+ +
+ + {% else %} +
+
+ + {% if participant.balance > 0 %} +

Balance

+

You have a balance of ${{ participant.balance }}.
+ What should we do with it?

+ +
    + + {% if participant.last_ach_result == '' %} + {% if participant.is_whitelisted %} +
  • +
  • + {% else %} + +
  • + + {% endif %} + {% else %} +
  • + {% endif %} + + {% if participant.giving %} +
  • +
  • + {% else %} +
  • Go set up some tips to be able to distribute your + balance as a final gift.
  • + {% endif %} + +
+ +

If neither option works for you, please contact + support to otherwise deal with your balance before + closing your account.

+ + {% endif %} + + +

Personal Information

+ +

We immediately clear out most of the information in your + profile (though it may still exist in our database in event + logs, to be fully deleted once we sort out our data + retention policy).

+ +

Things we clear immediately include your “making the + world better” statement, any funding goal, the tips + you're receiving, and those you're giving. You'll also be + removed from any communities and teams you were a part of. If + you're closing a team account, all team members will be removed + from the team.

+ +

We specifically don't delete your past giving and + receiving history on the site, because that information also + belongs equally to other users (the ones you gave to and + received from).

+ +

After you close your account, your profile page will say, + “The account owner has closed this account.”

+ + +

Username

+ +

We may give your username to someone else if they ask for + it, but not for at least a year after you close your account + (unless we determine that you've been infringing a + trademark).

+ +

Until we give your username to someone else, you can use it + again if you ever decide to rejoin Gittip. Simply sign in.

+ + +

Remember

+ +

We have no control over links to your profile from other + places on the Internet.

+ +

We have no control over content indexed by search engines + like Google.

+ + +

Ready?

+ +
+ +
+ {% endif %} +
+{% endblock %} diff --git a/www/%username/account/index.html.spt b/www/%username/account/index.html.spt index 58a470e428..221ee01fc5 100644 --- a/www/%username/account/index.html.spt +++ b/www/%username/account/index.html.spt @@ -184,6 +184,15 @@ locked = False + + +
+

Close

+
+ +
+
+ {% endblock %} diff --git a/www/about/faq.html.spt b/www/about/faq.html.spt index 5d1ec71dd2..9fbb6d6302 100644 --- a/www/about/faq.html.spt +++ b/www/about/faq.html.spt @@ -151,11 +151,9 @@ title = "Frequently Asked Questions" buttons to others on your profile. -
How do I cancel my account?
+
How do I close my account?
-
Sorry that this is manual right now. Email us - and we'll take care of it for you.
+
Close account
diff --git a/www/about/me/%redirect_to.spt b/www/about/me/%redirect_to.spt index 1ef552b49d..2f0e258a23 100644 --- a/www/about/me/%redirect_to.spt +++ b/www/about/me/%redirect_to.spt @@ -1,5 +1,11 @@ [---] -if user.ANON: - request.redirect('/') -request.redirect(u'/' + user.participant.username + u'/' + path['redirect_to']) -[---] text/plain +if not user.ANON: + request.redirect('/' + user.participant.username + '/' + path['redirect_to']) +title = "Sign In" +[---] text/html +{% extends "templates/base.html" %} +{% block box %} +
+

You need to sign in to access this page.

+
+{% endblock %}