diff --git a/gratipay/models/participant.py b/gratipay/models/participant.py index 4a30ba7146..4de957e6e3 100644 --- a/gratipay/models/participant.py +++ b/gratipay/models/participant.py @@ -11,7 +11,7 @@ from __future__ import print_function, unicode_literals from datetime import timedelta -from decimal import Decimal, ROUND_DOWN +from decimal import Decimal import pickle from time import sleep from urllib import quote @@ -319,25 +319,11 @@ def set_as_claimed(self): # Closing # ======= - class UnknownDisbursementStrategy(Exception): pass - - def close(self, disbursement_strategy): + def close(self): """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. - # XXX Bring me back! - #elif disbursement_strategy == 'bank': - # self.withdraw_balance_to_bank_account() - #elif disbursement_strategy == 'downstream': - # # This in particular needs to come before clear_tips_giving. - # self.distribute_balance_as_final_gift(cursor) - else: - raise self.UnknownDisbursementStrategy - - self.clear_tips_giving(cursor) - self.clear_tips_receiving(cursor) + self.clear_subscriptions(cursor) self.clear_personal_information(cursor) self.final_check(cursor) self.update_is_closed(True, cursor) @@ -354,99 +340,23 @@ def update_is_closed(self, is_closed, cursor=None): ) self.set_attributes(is_closed=is_closed) - class BankWithdrawalFailed(Exception): pass - - def withdraw_balance_to_bank_account(self): - from gratipay.billing.exchanges import ach_credit - error = ach_credit( self.db - , self - , Decimal('0.00') # don't withhold anything - , Decimal('0.00') # send it all - ) - if error: - raise self.BankWithdrawalFailed(error) - - - class NoOneToGiveFinalGiftTo(Exception): pass - def distribute_balance_as_final_gift(self, cursor): - """Distribute a balance as a final gift. + def clear_subscriptions(self, cursor): + """Zero out the participant's subscriptions. """ - raise NotImplementedError # XXX Bring me back! - if self.balance == 0: - return - - claimed_tips, claimed_total = self.get_giving_for_profile() - transfers = [] - distributed = Decimal('0.00') - - for tip in claimed_tips: - rate = tip.amount / claimed_total - pro_rated = (self.balance * rate).quantize(Decimal('0.01'), ROUND_DOWN) - if pro_rated == 0: - continue - distributed += pro_rated - transfers.append([tip.tippee, pro_rated]) - - if not transfers: - raise self.NoOneToGiveFinalGiftTo - - diff = self.balance - distributed - if diff != 0: - transfers[0][1] += diff # Give it to the highest receiver. - - for tippee, amount in transfers: - assert amount > 0 - balance = cursor.one( "UPDATE participants SET balance=balance - %s " - "WHERE username=%s RETURNING balance" - , (amount, self.username) - ) - assert balance >= 0 # sanity check - cursor.run( "UPDATE participants SET balance=balance + %s WHERE username=%s" - , (amount, tippee) - ) - cursor.run( "INSERT INTO transfers (tipper, tippee, amount, context) " - "VALUES (%s, %s, %s, 'final-gift')" - , (self.username, tippee, amount) - ) - - assert balance == 0 - self.set_attributes(balance=balance) - - - def clear_tips_giving(self, cursor): - """Zero out tips from a given user. - """ - tippees = cursor.all(""" - - SELECT ( SELECT participants.*::participants - FROM participants - WHERE username=tippee - ) AS tippee - FROM current_tips - WHERE tipper = %s - AND amount > 0 - - """, (self.username,)) - for tippee in tippees: - self.set_tip_to(tippee, '0.00', update_self=False, cursor=cursor) - - def clear_tips_receiving(self, cursor): - """Zero out tips to a given user. - """ - tippers = cursor.all(""" - - SELECT ( SELECT participants.*::participants - FROM participants - WHERE username=tipper - ) AS tipper - FROM current_tips - WHERE tippee = %s + teams = cursor.all(""" + + SELECT ( SELECT teams.*::teams + FROM teams + WHERE slug=team + ) AS team + FROM current_subscriptions + WHERE subscriber = %s AND amount > 0 """, (self.username,)) - for tipper in tippers: - tipper.set_tip_to(self, '0.00', update_tippee=False, cursor=cursor) + for team in teams: + self.set_subscription_to(team, '0.00', update_self=False, cursor=cursor) def clear_takes(self, cursor): @@ -460,9 +370,6 @@ def clear_takes(self, cursor): def clear_personal_information(self, cursor): """Clear personal information such as statements. """ - if self.IS_PLURAL: - self.remove_all_members(cursor) - self.clear_takes(cursor) r = cursor.one(""" INSERT INTO community_members (slug, participant, ctime, name, is_member) ( @@ -787,7 +694,7 @@ def get_cryptocoin_addresses(self): @property def has_payout_route(self): - for network in ('balanced-ba', 'paypal'): + for network in ('paypal',): route = ExchangeRoute.from_network(self, network) if route and not route.error: return True @@ -931,10 +838,12 @@ def profile_url(self): return '{base_url}/{username}/'.format(**locals()) - def get_teams(self, only_approved=False): - """Return a list of teams this user is a member or owner of. + def get_teams(self, only_approved=False, cursor=None): + """Return a list of teams this user is the owner of. """ - teams = self.db.all("SELECT teams.*::teams FROM teams WHERE owner=%s", (self.username,)) + teams = (cursor or self.db).all( "SELECT teams.*::teams FROM teams WHERE owner=%s" + , (self.username,) + ) if only_approved: teams = [t for t in teams if t.is_approved] return teams @@ -1394,15 +1303,14 @@ def get_age_in_seconds(self): return out - class StillReceivingTips(Exception): pass + class StillATeamOwner(Exception): pass class BalanceIsNotZero(Exception): pass def final_check(self, cursor): """Sanity-check that balance and tips have been dealt with. """ - INCOMING = "SELECT count(*) FROM current_tips WHERE tippee = %s AND amount > 0" - if cursor.one(INCOMING, (self.username,)) > 0: - raise self.StillReceivingTips + if self.get_teams(cursor=cursor): + raise self.StillATeamOwner if self.balance != 0: raise self.BalanceIsNotZero diff --git a/tests/py/test_close.py b/tests/py/test_close.py index 4f23b8c5ac..0016dbcfbe 100644 --- a/tests/py/test_close.py +++ b/tests/py/test_close.py @@ -7,7 +7,6 @@ import pytest from gratipay.billing.payday import Payday -from gratipay.exceptions import NotWhitelisted from gratipay.models.community import Community from gratipay.models.participant import Participant from gratipay.testing import Harness @@ -17,32 +16,22 @@ class TestClosing(Harness): # close - @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3454') def test_close_closes(self): - team = self.make_participant('team', claimed_time='now', number='plural', balance=50) - alice = self.make_participant('alice', claimed_time='now', balance=D('10.00')) - bob = self.make_participant('bob', claimed_time='now') - carl = self.make_participant('carl', claimed_time='now') - - alice.set_tip_to(bob, D('3.00')) - carl.set_tip_to(alice, D('2.00')) - - team.add_member(alice) - team.add_member(bob) - assert len(team.get_current_takes()) == 2 # sanity check - - alice.close('downstream') + alice = self.make_participant('alice', claimed_time='now') + alice.close() + assert Participant.from_username('alice').is_closed - assert carl.get_tip_to('alice')['amount'] == 0 - assert alice.balance == 0 - assert len(team.get_current_takes()) == 1 + def test_close_fails_if_still_a_balance(self): + alice = self.make_participant('alice', claimed_time='now', balance=D('10.00')) + with pytest.raises(alice.BalanceIsNotZero): + alice.close() - def test_close_raises_for_unknown_disbursement_strategy(self): - alice = self.make_participant('alice', balance=D('0.00')) - with pytest.raises(alice.UnknownDisbursementStrategy): - alice.close('cheese') + def test_close_fails_if_still_owns_a_team(self): + alice = self.make_participant('alice', claimed_time='now') + self.make_team(owner=alice) + with pytest.raises(alice.StillATeamOwner): + alice.close() - @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3454') def test_close_page_is_usually_available(self): self.make_participant('alice', claimed_time='now') body = self.client.GET('/~alice/settings/close', auth_as='alice').body @@ -55,18 +44,12 @@ def test_close_page_is_not_available_during_payday(self): assert 'Personal Information' not in body assert 'Try Again Later' in body - @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3454') 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/settings/close', auth_as='alice', data=data) + self.make_participant('alice', claimed_time='now') + response = self.client.PxST('/~alice/settings/close', auth_as='alice') assert response.code == 302 assert response.headers['Location'] == '/~alice/' - assert Participant.from_username('alice').balance == 0 - assert Participant.from_username('bob').balance == 7 + assert Participant.from_username('alice').is_closed def test_cant_post_to_close_page_during_payday(self): Payday.start() @@ -74,213 +57,74 @@ def test_cant_post_to_close_page_during_payday(self): body = self.client.POST('/~alice/settings/close', auth_as='alice').body assert 'Try Again Later' in body - @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3454') - @mock.patch('gratipay.billing.exchanges.ach_credit') - def test_ach_credit_failure_doesnt_cause_500(self, ach_credit): - ach_credit.side_effect = 'some error' - self.make_participant('alice', claimed_time='now', balance=384) - data = {'disbursement_strategy': 'bank'} - r = self.client.POST('/~alice/settings/close', auth_as='alice', data=data) - assert r.code == 200 - - - # wbtba - withdraw_balance_to_bank_account - - @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3454') - @mock.patch('gratipay.billing.exchanges.thing_from_href') - def test_wbtba_withdraws_balance_to_bank_account(self, tfh): - alice = self.make_participant( 'alice' - , balance=D('10.00') - , is_suspicious=False - , last_paypal_result='' - ) - alice.close('bank') - - def test_wbtba_raises_NotWhitelisted_if_not_whitelisted(self): - alice = self.make_participant('alice', balance=D('10.00')) - with pytest.raises(NotWhitelisted): - alice.withdraw_balance_to_bank_account() - - def test_wbtba_raises_NotWhitelisted_if_blacklisted(self): - alice = self.make_participant('alice', balance=D('10.00'), is_suspicious=True) - with pytest.raises(NotWhitelisted): - alice.withdraw_balance_to_bank_account() - - - # 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') - carl = self.make_participant('carl', claimed_time='now') - alice.set_tip_to(bob, D('3.00')) - alice.set_tip_to(carl, D('2.00')) - with self.db.get_cursor() as cursor: - alice.distribute_balance_as_final_gift(cursor) - assert Participant.from_username('bob').balance == D('6.00') - assert Participant.from_username('carl').balance == D('4.00') - assert Participant.from_username('alice').balance == D('0.00') + def test_close_page_shows_a_message_to_team_owners(self): + alice = self.make_participant('alice', claimed_time='now') + self.make_team('A', alice) + body = self.client.GET('/~alice/settings/close', auth_as='alice').body + assert 'You are the owner of the A team.' in body - @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') - carl = self.make_participant('carl') - alice.set_tip_to(bob, D('3.00')) - alice.set_tip_to(carl, D('2.00')) - with self.db.get_cursor() as cursor: - with pytest.raises(alice.NoOneToGiveFinalGiftTo): - alice.distribute_balance_as_final_gift(cursor) - assert Participant.from_username('bob').balance == D('0.00') - 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') - carl = self.make_participant('carl') - alice.set_tip_to(bob, D('3.00')) - alice.set_tip_to(carl, D('2.00')) - with self.db.get_cursor() as cursor: - alice.distribute_balance_as_final_gift(cursor) - assert Participant.from_username('bob').balance == D('10.00') - assert Participant.from_username('carl').balance == D('0.00') - assert Participant.from_username('alice').balance == D('0.00') + def test_close_page_shows_a_message_to_owners_of_two_teams(self): + alice = self.make_participant('alice', claimed_time='now') + self.make_team('A', alice) + self.make_team('B', alice) + body = self.client.GET('/~alice/settings/close', auth_as='alice').body + assert 'You are the owner of the A and B teams.' in body - @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') - carl = self.make_participant('carl', claimed_time='now') - alice.set_tip_to(bob, D('0.00')) - alice.set_tip_to(carl, D('2.00')) - with self.db.get_cursor() as cursor: - alice.distribute_balance_as_final_gift(cursor) - assert self.db.one("SELECT count(*) FROM tips WHERE tippee='bob'") == 1 - assert Participant.from_username('bob').balance == D('0.00') - 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') - carl = self.make_participant('carl', claimed_time='now') - alice.set_tip_to(bob, D('3.00')) - alice.set_tip_to(carl, D('6.00')) - with self.db.get_cursor() as cursor: - alice.distribute_balance_as_final_gift(cursor) - assert Participant.from_username('bob').balance == D('3.33') - 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') - carl = self.make_participant('carl', claimed_time='now') - alice.set_tip_to(bob, D('3.00')) - alice.set_tip_to(carl, D('6.00')) - with self.db.get_cursor() as cursor: - alice.distribute_balance_as_final_gift(cursor) - assert self.db.one("SELECT count(*) FROM tips") == 2 - assert Participant.from_username('bob').balance == D('0.00') - assert Participant.from_username('carl').balance == D('0.00') - assert Participant.from_username('alice').balance == D('0.00') + def test_close_page_shows_a_message_to_owners_of_three_teams(self): + alice = self.make_participant('alice', claimed_time='now') + self.make_team('A', alice) + self.make_team('B', alice) + self.make_team('C', alice) + body = self.client.GET('/~alice/settings/close', auth_as='alice').body + assert 'You are the owner of the A, B and C teams.' in body - # ctg - clear_tips_giving + # cs - clear_subscriptions - def test_ctg_clears_tips_giving(self): + def test_cs_clears_subscriptions(self): alice = self.make_participant('alice', claimed_time='now', last_bill_result='') - alice.set_tip_to(self.make_participant('bob', claimed_time='now').username, D('1.00')) - ntips = lambda: self.db.one("SELECT count(*) FROM current_tips " - "WHERE tipper='alice' AND amount > 0") - assert ntips() == 1 + alice.set_subscription_to(self.make_team(), D('1.00')) + nsubscriptions = lambda: self.db.one("SELECT count(*) FROM current_subscriptions " + "WHERE subscriber='alice' AND amount > 0") + assert nsubscriptions() == 1 with self.db.get_cursor() as cursor: - alice.clear_tips_giving(cursor) - assert ntips() == 0 + alice.clear_subscriptions(cursor) + assert nsubscriptions() == 0 - def test_ctg_doesnt_duplicate_zero_tips(self): + def test_cs_doesnt_duplicate_zero_subscriptions(self): alice = self.make_participant('alice', claimed_time='now') - bob = self.make_participant('bob') - alice.set_tip_to(bob, D('1.00')) - alice.set_tip_to(bob, D('0.00')) - ntips = lambda: self.db.one("SELECT count(*) FROM tips WHERE tipper='alice'") - assert ntips() == 2 + A = self.make_team() + alice.set_subscription_to(A, D('1.00')) + alice.set_subscription_to(A, D('0.00')) + nsubscriptions = lambda: self.db.one("SELECT count(*) FROM subscriptions " + "WHERE subscriber='alice'") + assert nsubscriptions() == 2 with self.db.get_cursor() as cursor: - alice.clear_tips_giving(cursor) - assert ntips() == 2 + alice.clear_subscriptions(cursor) + assert nsubscriptions() == 2 - def test_ctg_doesnt_zero_when_theres_no_tip(self): + def test_cs_doesnt_zero_when_theres_no_subscription(self): alice = self.make_participant('alice') - ntips = lambda: self.db.one("SELECT count(*) FROM tips WHERE tipper='alice'") - assert ntips() == 0 + nsubscriptions = lambda: self.db.one("SELECT count(*) FROM subscriptions " + "WHERE subscriber='alice'") + assert nsubscriptions() == 0 with self.db.get_cursor() as cursor: - alice.clear_tips_giving(cursor) - assert ntips() == 0 + alice.clear_subscriptions(cursor) + assert nsubscriptions() == 0 - def test_ctg_clears_multiple_tips_giving(self): + def test_cs_clears_multiple_subscriptions(self): alice = self.make_participant('alice', claimed_time='now') - alice.set_tip_to(self.make_participant('bob', claimed_time='now').username, D('1.00')) - alice.set_tip_to(self.make_participant('carl', claimed_time='now').username, D('1.00')) - alice.set_tip_to(self.make_participant('darcy', claimed_time='now').username, D('1.00')) - alice.set_tip_to(self.make_participant('evelyn', claimed_time='now').username, D('1.00')) - alice.set_tip_to(self.make_participant('francis', claimed_time='now').username, D('1.00')) - ntips = lambda: self.db.one("SELECT count(*) FROM current_tips " - "WHERE tipper='alice' AND amount > 0") - assert ntips() == 5 - with self.db.get_cursor() as cursor: - alice.clear_tips_giving(cursor) - assert ntips() == 0 - - - # ctr - clear_tips_receiving - - def test_ctr_clears_tips_receiving(self): - alice = self.make_participant('alice') - self.make_participant('bob', claimed_time='now').set_tip_to(alice, D('1.00')) - ntips = lambda: self.db.one("SELECT count(*) FROM current_tips " - "WHERE tippee='alice' AND amount > 0") - assert ntips() == 1 - with self.db.get_cursor() as cursor: - alice.clear_tips_receiving(cursor) - assert ntips() == 0 - - def test_ctr_doesnt_duplicate_zero_tips(self): - alice = self.make_participant('alice') - bob = self.make_participant('bob', claimed_time='now') - bob.set_tip_to(alice, D('1.00')) - bob.set_tip_to(alice, D('0.00')) - ntips = lambda: self.db.one("SELECT count(*) FROM tips WHERE tippee='alice'") - assert ntips() == 2 - with self.db.get_cursor() as cursor: - alice.clear_tips_receiving(cursor) - assert ntips() == 2 - - def test_ctr_doesnt_zero_when_theres_no_tip(self): - alice = self.make_participant('alice') - ntips = lambda: self.db.one("SELECT count(*) FROM tips WHERE tippee='alice'") - assert ntips() == 0 - with self.db.get_cursor() as cursor: - alice.clear_tips_receiving(cursor) - assert ntips() == 0 - - def test_ctr_clears_multiple_tips_receiving(self): - alice = self.make_participant('alice') - self.make_participant('bob', claimed_time='now').set_tip_to(alice, D('1.00')) - self.make_participant('carl', claimed_time='now').set_tip_to(alice, D('2.00')) - self.make_participant('darcy', claimed_time='now').set_tip_to(alice, D('3.00')) - self.make_participant('evelyn', claimed_time='now').set_tip_to(alice, D('4.00')) - self.make_participant('francis', claimed_time='now').set_tip_to(alice, D('5.00')) - ntips = lambda: self.db.one("SELECT count(*) FROM current_tips " - "WHERE tippee='alice' AND amount > 0") - assert ntips() == 5 - with self.db.get_cursor() as cursor: - alice.clear_tips_receiving(cursor) - assert ntips() == 0 + alice.set_subscription_to(self.make_team('A'), D('1.00')) + alice.set_subscription_to(self.make_team('B'), D('1.00')) + alice.set_subscription_to(self.make_team('C'), D('1.00')) + alice.set_subscription_to(self.make_team('D'), D('1.00')) + alice.set_subscription_to(self.make_team('E'), D('1.00')) + nsubscriptions = lambda: self.db.one("SELECT count(*) FROM current_subscriptions " + "WHERE subscriber='alice' AND amount > 0") + assert nsubscriptions() == 5 + with self.db.get_cursor() as cursor: + alice.clear_subscriptions(cursor) + assert nsubscriptions() == 0 # cpi - clear_personal_information diff --git a/tests/py/test_participant.py b/tests/py/test_participant.py index 6fd77635f3..b9081d2116 100644 --- a/tests/py/test_participant.py +++ b/tests/py/test_participant.py @@ -725,11 +725,11 @@ def test_ru_returns_openstreetmap_url_for_stub_from_openstreetmap(self): # archive - def test_archive_fails_if_ctr_not_run(self): + def test_archive_fails_for_team_owner(self): alice = self.make_participant('alice') - self.make_participant('bob', claimed_time='now').set_tip_to(alice, Decimal('1.00')) + self.make_team(owner=alice) with self.db.get_cursor() as cursor: - pytest.raises(alice.StillReceivingTips, alice.archive, cursor) + pytest.raises(alice.StillATeamOwner, alice.archive, cursor) def test_archive_fails_if_balance_is_positive(self): alice = self.make_participant('alice', balance=2) @@ -760,4 +760,4 @@ def test_archive_records_an_event(self): def test_suggested_payment_is_zero_for_new_user(self): alice = self.make_participant('alice') - assert alice.suggested_payment == 0 \ No newline at end of file + assert alice.suggested_payment == 0 diff --git a/tests/py/test_routes.py b/tests/py/test_routes.py index 39240ca70b..ff8211a410 100644 --- a/tests/py/test_routes.py +++ b/tests/py/test_routes.py @@ -89,4 +89,4 @@ def test_receipt_page_loads_for_braintree_cards(self): ex_id = self.make_exchange(self.obama_route, 113, 30, self.obama) url_receipt = '/~obama/receipts/{}.html'.format(ex_id) actual = self.client.GET(url_receipt, auth_as='obama').body.decode('utf8') - assert self.bt_card.card_type in actual \ No newline at end of file + assert self.bt_card.card_type in actual diff --git a/www/~/%username/settings/close.spt b/www/~/%username/settings/close.spt index ee8ebbeae8..3f93764d99 100644 --- a/www/~/%username/settings/close.spt +++ b/www/~/%username/settings/close.spt @@ -14,156 +14,113 @@ if request.method == 'POST': if payday_is_running: pass # User will get the "Try Again Later" message. else: - disbursement_strategy = request.body.get('disbursement_strategy') - if participant.balance and disbursement_strategy is None: - error = _("You still have money in this Gratipay account, please " - "choose a disbursement method below or contact support.") - else: - try: - participant.close(disbursement_strategy) - except participant.BankWithdrawalFailed as e: - error = _("The bank withdrawal failed. Error message: {0}", e) - else: - website.redirect('/~%s/' % participant.username) + participant.close() + website.redirect('/~%s/' % participant.username) +teams = participant.get_teams() +nteams = len(teams) +if nteams > 1: + teams = ', '.join([t.name for t in teams[:-1]]) + ' and ' + teams[-1].name + ' teams' +elif nteams == 1: + teams = teams[0].name + ' team' + [---] text/html {% extends "templates/base.html" %} {% block content %}