diff --git a/gratipay/models/participant.py b/gratipay/models/participant.py index 139ab5732d..ac20d92bb8 100644 --- a/gratipay/models/participant.py +++ b/gratipay/models/participant.py @@ -776,6 +776,14 @@ def get_cryptocoin_addresses(self): """, (self.id,)) return {r.network: r.address for r in routes} + @property + def has_payout_route(self): + for network in ('balanced-ba', 'paypal'): + route = ExchangeRoute.from_network(self, 'balanced-ba') + if route and not route.error: + return True + return False + def get_balanced_account(self): """Fetch or create the balanced account for this participant. diff --git a/gratipay/models/team.py b/gratipay/models/team.py index 907d11750e..63b7b40aa0 100644 --- a/gratipay/models/team.py +++ b/gratipay/models/team.py @@ -35,3 +35,18 @@ def _from_thing(cls, thing, value): WHERE {}=%s """.format(thing), (value,)) + + @classmethod + def create_new(cls, owner, fields): + return cls.db.one(""" + + INSERT INTO teams + (slug, slug_lower, name, homepage, product_or_service, + getting_involved, getting_paid, owner) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + RETURNING teams.*::teams + + """, (fields['slug'], fields['slug'].lower(), fields['name'], fields['homepage'], + fields['product_or_service'], fields['getting_involved'], fields['getting_paid'], + owner.username)) + diff --git a/js/gratipay/new_team.js b/js/gratipay/new_team.js new file mode 100644 index 0000000000..11b9bd3389 --- /dev/null +++ b/js/gratipay/new_team.js @@ -0,0 +1,31 @@ +Gratipay.new_team = {} + +Gratipay.new_team.initForm = function () { + $form = $('#new-team'); + $button = $form.find('button'); + $button.on('click', Gratipay.new_team.submitForm); +} + +Gratipay.new_team.submitForm = function (e) { + e.preventDefault(); + + $input = $(this) + $form = $(this).parent('form'); + var data = $form.serializeArray(); + + $input.prop('disable', true); + + $.ajax({ + url: $form.attr('action'), + type: 'POST', + data: data, + dataType: 'json', + success: function (d) { + $('form').html( "
Thank you! We will follow up shortly with an email to " + + d.email + ". Please email " + + "us with any questions.
" + ) + }, + error: [Gratipay.error, function () { $input.prop('disable', false); }] + }); +} diff --git a/tests/py/test_routes.py b/tests/py/test_routes.py index 842948dc75..2760f7eb50 100644 --- a/tests/py/test_routes.py +++ b/tests/py/test_routes.py @@ -69,6 +69,7 @@ def test_associate_and_delete_bank_account_valid(self): assert bank_accounts[0].href == bank_account.href assert self.david.get_bank_account_error() == '' + assert self.david.has_payout_route self.hit('david', 'delete', 'balanced-ba', bank_account.href) @@ -80,12 +81,14 @@ def test_associate_and_delete_bank_account_valid(self): # Check that update_error doesn't update an invalidated route route.update_error('some error') assert route.error == david.get_bank_account_error() == 'invalidated' + assert not self.david.has_payout_route @mock.patch.object(Participant, 'get_balanced_account') def test_associate_bank_account_invalid(self, gba): gba.return_value.merchant_status = 'underwritten' self.hit('david', 'associate', 'balanced-ba', '/bank_accounts/BA123123123', expected=400) assert self.david.get_bank_account_error() is None + assert not self.david.has_payout_route def test_associate_bitcoin(self): addr = '17NdbrSGoUotzeGCcMMCqnFkEvLymoou9j' diff --git a/tests/py/test_teams.py b/tests/py/test_teams.py index a7f12f0c39..eba47633b7 100644 --- a/tests/py/test_teams.py +++ b/tests/py/test_teams.py @@ -8,6 +8,20 @@ class TestNewTeams(Harness): + valid_data = { + 'name': 'Gratiteam', + 'homepage': 'http://gratipay.com/', + 'agree_terms': 'true', + 'product_or_service': 'Sample Product', + 'getting_paid': 'Getting Paid', + 'getting_involved': 'Getting Involved' + } + + def post_new(self, data, auth_as='alice', expected=200): + r = self.client.POST('/teams/create.json', data=data, auth_as=auth_as, raise_immediately=False) + assert r.code == expected + return r + def test_harness_can_make_a_team(self): team = self.make_team() assert team.name == 'The A Team' @@ -24,6 +38,52 @@ def test_can_construct_from_id(self): assert team.name == 'The A Team' assert team.owner == 'hannibal' + def test_can_create_new_team(self): + self.make_participant('alice', claimed_time='now', email_address='', last_ach_result='') + self.post_new(dict(self.valid_data)) + team = self.db.one("SELECT * FROM teams") + assert team + assert team.owner == 'alice' + + def test_401_for_anon_creating_new_team(self): + self.post_new(self.valid_data, auth_as=None, expected=401) + assert self.db.one("SELECT COUNT(*) FROM teams") == 0 + + def test_error_message_for_no_valid_email(self): + self.make_participant('alice', claimed_time='now') + r = self.post_new(dict(self.valid_data), expected=400) + assert self.db.one("SELECT COUNT(*) FROM teams") == 0 + assert "You must have a verified email address to apply for a new team." in r.body + + def test_error_message_for_no_payout_route(self): + self.make_participant('alice', claimed_time='now', email_address='alice@example.com') + r = self.post_new(dict(self.valid_data), expected=400) + assert self.db.one("SELECT COUNT(*) FROM teams") == 0 + assert "You must attach a bank account or PayPal to apply for a new team." in r.body + + def test_error_message_for_terms(self): + self.make_participant('alice', claimed_time='now', email_address='alice@example.com', last_ach_result='') + data = dict(self.valid_data) + del data['agree_terms'] + r = self.post_new(data, expected=400) + assert self.db.one("SELECT COUNT(*) FROM teams") == 0 + assert "Please agree to the terms of service." in r.body + + def test_error_message_for_missing_fields(self): + self.make_participant('alice', claimed_time='now', email_address='alice@example.com', last_ach_result='') + data = dict(self.valid_data) + del data['name'] + r = self.post_new(data, expected=400) + assert self.db.one("SELECT COUNT(*) FROM teams") == 0 + assert "Please fill out the 'Team Name' field." in r.body + + def test_error_message_for_slug_collision(self): + self.make_participant('alice', claimed_time='now', email_address='alice@example.com', last_ach_result='') + self.post_new(dict(self.valid_data)) + r = self.post_new(dict(self.valid_data), expected=400) + assert self.db.one("SELECT COUNT(*) FROM teams") == 1 + assert "Sorry, there is already a team using 'gratiteam'." in r.body + class TestOldTeams(Harness): diff --git a/www/new.spt b/www/new.spt new file mode 100644 index 0000000000..d18a64be6c --- /dev/null +++ b/www/new.spt @@ -0,0 +1,93 @@ +from aspen import Response + +from gratipay.models.community import slugize +from gratipay.models.team import Team +[---] +request.allow('GET') + +if user.ANON: + raise Response(401, _("You must sign in to apply for a new team.")) + +if user.participant.email_address is None: + raise Response(400, _("You must have a verified email address to apply for a new team.")) + +if not user.participant.has_payout_route: + raise Response(400, _("You must attach a bank account or PayPal to apply for a new team.")) + + +# We'll actually migrate *all* non-zero tips from non-closed, non-suspicious +# users, in case someone responds to a "failing card" notification at some +# point. But let's only tell them about the funded tips. + +receiving, ntips = website.db.one( """ + SELECT sum(amount), count(amount) + FROM current_tips + JOIN participants p ON p.username = tipper + WHERE tippee = %s + AND p.claimed_time IS NOT null + AND p.is_suspicious IS NOT true + AND p.is_closed IS NOT true + AND is_funded + AND amount > 0 +""", (user.participant.username,)) + +title = _("Apply for a New Team") +[---] text/html +{% extends "templates/base.html" %} + +{% block scripts %} + +{{ super() }} +{% endblock %} + +{% block content %} +