From 6d8a436daa0bb92d2c86993dfe3de68898b60168 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Wed, 13 May 2015 07:21:07 -0400 Subject: [PATCH 01/17] Stub out a file for creating a new team --- www/new.spt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 www/new.spt diff --git a/www/new.spt b/www/new.spt new file mode 100644 index 0000000000..8e79a597d3 --- /dev/null +++ b/www/new.spt @@ -0,0 +1,13 @@ +from gratipay.models.team import Team +[---] +if request.method == 'POST': + Team.create_new() +title = _("New Team") +[---] text/html +{% extends "templates/base.html" %} +{% block content %} +
+
+
+
+{% endblock %} From dbf17fa138556d26d1be2eb0c93b2bc957ee9a47 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Wed, 13 May 2015 07:24:17 -0400 Subject: [PATCH 02/17] Stub out form a bit more --- www/new.spt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/www/new.spt b/www/new.spt index 8e79a597d3..abb5d9af78 100644 --- a/www/new.spt +++ b/www/new.spt @@ -2,12 +2,18 @@ from gratipay.models.team import Team [---] if request.method == 'POST': Team.create_new() -title = _("New Team") +title = _("Apply for a New Team") [---] text/html {% extends "templates/base.html" %} {% block content %}
+

Name

+

Homepage

+

What product or service does your team provide?

+

How can other people get involved with your team?

+

How do you share revenue with contributors?

+
{% endblock %} From 54080678de49bd5fc51e1da495496812c00d95a3 Mon Sep 17 00:00:00 2001 From: Rohit Paul Kuruvilla Date: Wed, 13 May 2015 17:08:30 +0530 Subject: [PATCH 03/17] Show a 401 to anons --- www/new.spt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/www/new.spt b/www/new.spt index abb5d9af78..3d4111c56e 100644 --- a/www/new.spt +++ b/www/new.spt @@ -1,7 +1,13 @@ +from aspen import Response + from gratipay.models.team import Team [---] +if user.ANON: + raise Response(401, _("You must sign in to apply for a new team")) + if request.method == 'POST': Team.create_new() + title = _("Apply for a New Team") [---] text/html {% extends "templates/base.html" %} From 2068e180824e0cb1fec0f48bf86764c16af6995f Mon Sep 17 00:00:00 2001 From: Rohit Paul Kuruvilla Date: Wed, 13 May 2015 17:48:24 +0530 Subject: [PATCH 04/17] Create form fields --- www/new.spt | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/www/new.spt b/www/new.spt index 3d4111c56e..3d4598314d 100644 --- a/www/new.spt +++ b/www/new.spt @@ -14,12 +14,24 @@ title = _("Apply for a New Team") {% block content %}
-

Name

-

Homepage

-

What product or service does your team provide?

-

How can other people get involved with your team?

-

How do you share revenue with contributors?

- + + + + + + + + + + + + + + + +
+ +
{% endblock %} From 812c694ecfb97693daa492703c206bc8b69bf75e Mon Sep 17 00:00:00 2001 From: Rohit Paul Kuruvilla Date: Wed, 13 May 2015 18:17:34 +0530 Subject: [PATCH 05/17] Wireup form + add CSRF token --- gratipay/models/team.py | 15 +++++++++++++++ www/new.spt | 14 +++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) 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/www/new.spt b/www/new.spt index 3d4598314d..182bb5d1a5 100644 --- a/www/new.spt +++ b/www/new.spt @@ -1,12 +1,22 @@ from aspen import Response +from gratipay.models.community import slugize from gratipay.models.team import Team [---] if user.ANON: raise Response(401, _("You must sign in to apply for a new team")) if request.method == 'POST': - Team.create_new() + fields = {} + + # Validate inputs + + for field in ['name', 'homepage', 'product_or_service', 'getting_paid', 'getting_involved']: + fields[field] = request.body.get(field, '') + + fields['slug'] = slugize(fields['name']) + + Team.create_new(user.participant, fields) title = _("Apply for a New Team") [---] text/html @@ -14,6 +24,8 @@ title = _("Apply for a New Team") {% block content %}
+ + From 77f2c6b87cae807287c3d61c2e5e81e7c71ca7be Mon Sep 17 00:00:00 2001 From: Rohit Paul Kuruvilla Date: Wed, 13 May 2015 21:51:14 +0530 Subject: [PATCH 06/17] Basic tests --- tests/py/test_teams.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/py/test_teams.py b/tests/py/test_teams.py index a7f12f0c39..c29bcf9399 100644 --- a/tests/py/test_teams.py +++ b/tests/py/test_teams.py @@ -8,6 +8,15 @@ class TestNewTeams(Harness): + valid_data = { + 'name': 'Gratiteam' + } + + def post_new(self, data, auth_as='alice', expected=200): + r = self.client.POST('/new', 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 +33,17 @@ 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') + self.post_new(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 + class TestOldTeams(Harness): From 938db28118c3701daac77db4dc1721ff28fd0dc3 Mon Sep 17 00:00:00 2001 From: Rohit Paul Kuruvilla Date: Wed, 13 May 2015 22:32:48 +0530 Subject: [PATCH 07/17] Move create from new.spt to a JSON endpoint teams/create.json --- tests/py/test_teams.py | 24 ++++++++++++++++++++++-- www/new.spt | 13 ++----------- www/teams/create.json.spt | 29 +++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 www/teams/create.json.spt diff --git a/tests/py/test_teams.py b/tests/py/test_teams.py index c29bcf9399..7d99e3dcc0 100644 --- a/tests/py/test_teams.py +++ b/tests/py/test_teams.py @@ -9,11 +9,16 @@ class TestNewTeams(Harness): valid_data = { - 'name': 'Gratiteam' + '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('/new', data=data, auth_as=auth_as, raise_immediately=False) + r = self.client.POST('/teams/create.json', data=data, auth_as=auth_as, raise_immediately=False) assert r.code == expected return r @@ -44,6 +49,21 @@ 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_terms(self): + self.make_participant('alice') + data = 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 and conditions" in r.body + + def test_error_message_for_missing_fields(self): + self.make_participant('alice') + data = 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 'name' field" in r.body class TestOldTeams(Harness): diff --git a/www/new.spt b/www/new.spt index 182bb5d1a5..c449cc15a8 100644 --- a/www/new.spt +++ b/www/new.spt @@ -3,20 +3,11 @@ 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 request.method == 'POST': - fields = {} - - # Validate inputs - - for field in ['name', 'homepage', 'product_or_service', 'getting_paid', 'getting_involved']: - fields[field] = request.body.get(field, '') - - fields['slug'] = slugize(fields['name']) - - Team.create_new(user.participant, fields) title = _("Apply for a New Team") [---] text/html diff --git a/www/teams/create.json.spt b/www/teams/create.json.spt new file mode 100644 index 0000000000..631e1ee029 --- /dev/null +++ b/www/teams/create.json.spt @@ -0,0 +1,29 @@ +from aspen import Response + +from gratipay.models.community import slugize +from gratipay.models.team import Team +[---] +request.allow('POST') + +if user.ANON: + raise Response(401, _("You must sign in to apply for a new team")) + +if not request.body.get('agree_terms', False): + raise Response(400, _("Please agree to the terms and conditions")) + +if request.method == 'POST': + fields = {} + + # Validate inputs + + for field in ['name', 'homepage', 'product_or_service', 'getting_paid', 'getting_involved']: + if not request.body.get(field, ''): + raise Response(400, _("Please fill out the '{0}' field", field)) + + fields[field] = request.body.get(field, '') + + fields['slug'] = slugize(fields['name']) + + Team.create_new(user.participant, fields) +[---] application/json via json_dump +{} From 3e09ffc5bdc07c14f48a05523024257a618d29c1 Mon Sep 17 00:00:00 2001 From: Rohit Paul Kuruvilla Date: Wed, 13 May 2015 23:15:47 +0530 Subject: [PATCH 08/17] Add JS functions and wireup --- js/gratipay/new_team.js | 28 ++++++++++++++++++++++++++++ www/new.spt | 8 +++++++- 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 js/gratipay/new_team.js diff --git a/js/gratipay/new_team.js b/js/gratipay/new_team.js new file mode 100644 index 0000000000..669ec12082 --- /dev/null +++ b/js/gratipay/new_team.js @@ -0,0 +1,28 @@ +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) { + Gratipay.notification("Success! Your application has been submitted.", "success") + }, + error: [Gratipay.error, function () { $input.prop('disable', false); }] + }); +} diff --git a/www/new.spt b/www/new.spt index c449cc15a8..a4d14ee035 100644 --- a/www/new.spt +++ b/www/new.spt @@ -12,9 +12,15 @@ if user.ANON: title = _("Apply for a New Team") [---] text/html {% extends "templates/base.html" %} + +{% block scripts %} + +{{ super() }} +{% endblock %} + {% block content %}
- + From b7cb3d28e512ed04e28b01ec7fd879b10aa4f85e Mon Sep 17 00:00:00 2001 From: Rohit Paul Kuruvilla Date: Wed, 13 May 2015 23:19:59 +0530 Subject: [PATCH 09/17] Add checkbox for terms and conditions --- www/new.spt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/www/new.spt b/www/new.spt index a4d14ee035..6f2c02b27f 100644 --- a/www/new.spt +++ b/www/new.spt @@ -38,6 +38,8 @@ title = _("Apply for a New Team") +
+ {{ _("I agree to the terms and conditions") }}
From a1dd9e7fd8b73a0be1ceace419502614de55214d Mon Sep 17 00:00:00 2001 From: Rohit Paul Kuruvilla Date: Wed, 13 May 2015 23:30:20 +0530 Subject: [PATCH 10/17] Friendly error messages --- tests/py/test_teams.py | 2 +- www/teams/create.json.spt | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/py/test_teams.py b/tests/py/test_teams.py index 7d99e3dcc0..0de5c8881d 100644 --- a/tests/py/test_teams.py +++ b/tests/py/test_teams.py @@ -63,7 +63,7 @@ def test_error_message_for_missing_fields(self): del data['name'] r = self.post_new(data, expected=400) assert self.db.one("SELECT COUNT(*) FROM teams") == 0 - assert "Please fill out the 'name' field" in r.body + assert "Please fill out the 'Name' field" in r.body class TestOldTeams(Harness): diff --git a/www/teams/create.json.spt b/www/teams/create.json.spt index 631e1ee029..c7788d53e8 100644 --- a/www/teams/create.json.spt +++ b/www/teams/create.json.spt @@ -5,20 +5,28 @@ from gratipay.models.team import Team [---] request.allow('POST') +field_names = { + 'name': 'Name', + 'homepage': 'Homepage', + 'product_or_service': 'Product or Service', + 'getting_paid': 'How do you share revenue', + 'getting_involved': 'How can other people get involved' +} + if user.ANON: raise Response(401, _("You must sign in to apply for a new team")) if not request.body.get('agree_terms', False): - raise Response(400, _("Please agree to the terms and conditions")) + raise Response(400, _("Please agree to the terms and conditions")) if request.method == 'POST': fields = {} # Validate inputs - for field in ['name', 'homepage', 'product_or_service', 'getting_paid', 'getting_involved']: - if not request.body.get(field, ''): - raise Response(400, _("Please fill out the '{0}' field", field)) + for field in field_names.keys(): + if not request.body.get(field, ''): + raise Response(400, _("Please fill out the '{0}' field", field_names[field])) fields[field] = request.body.get(field, '') From d295589851b9fc6db2a0cdc7ac94fdb65b18f23d Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Wed, 13 May 2015 18:12:55 -0400 Subject: [PATCH 11/17] Buff the UI just a little bit --- tests/py/test_teams.py | 4 ++-- www/new.spt | 25 +++++++++++++++++++------ www/teams/create.json.spt | 18 ++++++++++-------- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/tests/py/test_teams.py b/tests/py/test_teams.py index 0de5c8881d..ca433d3228 100644 --- a/tests/py/test_teams.py +++ b/tests/py/test_teams.py @@ -55,7 +55,7 @@ def test_error_message_for_terms(self): 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 and conditions" in r.body + assert "Please agree to the terms of service." in r.body def test_error_message_for_missing_fields(self): self.make_participant('alice') @@ -63,7 +63,7 @@ def test_error_message_for_missing_fields(self): del data['name'] r = self.post_new(data, expected=400) assert self.db.one("SELECT COUNT(*) FROM teams") == 0 - assert "Please fill out the 'Name' field" in r.body + assert "Please fill out the 'Team Name' field." in r.body class TestOldTeams(Harness): diff --git a/www/new.spt b/www/new.spt index 6f2c02b27f..5d5de7e43f 100644 --- a/www/new.spt +++ b/www/new.spt @@ -20,11 +20,17 @@ title = _("Apply for a New Team") {% block content %}
+ - - + + @@ -38,10 +44,17 @@ title = _("Apply for a New Team") -
- {{ _("I agree to the terms and conditions") }} -
- +
+
+ + +
+
diff --git a/www/teams/create.json.spt b/www/teams/create.json.spt index c7788d53e8..8e9fd58a4f 100644 --- a/www/teams/create.json.spt +++ b/www/teams/create.json.spt @@ -2,22 +2,23 @@ from aspen import Response from gratipay.models.community import slugize from gratipay.models.team import Team +from psycopg2 import IntegrityError [---] request.allow('POST') field_names = { - 'name': 'Name', + 'name': 'Team Name', 'homepage': 'Homepage', 'product_or_service': 'Product or Service', - 'getting_paid': 'How do you share revenue', - 'getting_involved': 'How can other people get involved' + 'getting_paid': 'How do you share revenue?', + 'getting_involved': 'How can other people get involved?' } if user.ANON: - raise Response(401, _("You must sign in to apply for a new team")) + raise Response(401, _("You must sign in to apply for a new team.")) if not request.body.get('agree_terms', False): - raise Response(400, _("Please agree to the terms and conditions")) + raise Response(400, _("Please agree to the terms of service.")) if request.method == 'POST': fields = {} @@ -25,10 +26,11 @@ if request.method == 'POST': # Validate inputs for field in field_names.keys(): - if not request.body.get(field, ''): - raise Response(400, _("Please fill out the '{0}' field", field_names[field])) + value = request.body.get(field, '') + if not value: + raise Response(400, _("Please fill out the '{0}' field.", field_names[field])) - fields[field] = request.body.get(field, '') + fields[field] = value fields['slug'] = slugize(fields['name']) From 12e2f842f53135b9425a3d8ca5f783f9c61c4652 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Wed, 13 May 2015 18:34:00 -0400 Subject: [PATCH 12/17] Copy dicts to avoid test bugs Python lesson time! :D Python dicts are mutable, which means that this will modify foo: >>> foo = {1: 2} >>> bar = foo >>> bar[1] = 3 >>> foo {1: 3} You can tell that foo and bar are the same identical object by comparing their memory address: >>> id(foo) == id(bar) True An easy way around this is to make a shallow copy using the dict constructor (shallow means non-recursive): >>> foo = {1: 2} >>> bar = dict(foo) >>> bar[1] = 3 >>> foo {1: 2} >>> id(foo) == id(bar) False --- tests/py/test_teams.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/py/test_teams.py b/tests/py/test_teams.py index ca433d3228..836c4b0d86 100644 --- a/tests/py/test_teams.py +++ b/tests/py/test_teams.py @@ -40,7 +40,7 @@ def test_can_construct_from_id(self): def test_can_create_new_team(self): self.make_participant('alice') - self.post_new(self.valid_data) + self.post_new(dict(self.valid_data)) team = self.db.one("SELECT * FROM teams") assert team assert team.owner == 'alice' @@ -51,7 +51,7 @@ def test_401_for_anon_creating_new_team(self): def test_error_message_for_terms(self): self.make_participant('alice') - data = self.valid_data + 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 @@ -59,7 +59,7 @@ def test_error_message_for_terms(self): def test_error_message_for_missing_fields(self): self.make_participant('alice') - data = self.valid_data + data = dict(self.valid_data) del data['name'] r = self.post_new(data, expected=400) assert self.db.one("SELECT COUNT(*) FROM teams") == 0 From 222ddea366e63aa01aa1d2cec9441efa612284c0 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Wed, 13 May 2015 18:40:39 -0400 Subject: [PATCH 13/17] Error nicely on slug collision --- tests/py/test_teams.py | 7 +++++++ www/teams/create.json.spt | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/py/test_teams.py b/tests/py/test_teams.py index 836c4b0d86..17b555478d 100644 --- a/tests/py/test_teams.py +++ b/tests/py/test_teams.py @@ -65,6 +65,13 @@ def test_error_message_for_missing_fields(self): 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') + 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): def setUp(self): diff --git a/www/teams/create.json.spt b/www/teams/create.json.spt index 8e9fd58a4f..746e79d28d 100644 --- a/www/teams/create.json.spt +++ b/www/teams/create.json.spt @@ -34,6 +34,10 @@ if request.method == 'POST': fields['slug'] = slugize(fields['name']) - Team.create_new(user.participant, fields) + try: + Team.create_new(user.participant, fields) + except IntegrityError: + raise Response(400, _("Sorry, there is already a team using '{}'.", fields['slug'])) + [---] application/json via json_dump {} From 8378394271ee8cab11b405ab39b4510c59d280e2 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Wed, 13 May 2015 18:52:15 -0400 Subject: [PATCH 14/17] Provide a nicer success experience --- js/gratipay/new_team.js | 5 ++++- www/teams/create.json.spt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/js/gratipay/new_team.js b/js/gratipay/new_team.js index 669ec12082..11b9bd3389 100644 --- a/js/gratipay/new_team.js +++ b/js/gratipay/new_team.js @@ -21,7 +21,10 @@ Gratipay.new_team.submitForm = function (e) { data: data, dataType: 'json', success: function (d) { - Gratipay.notification("Success! Your application has been submitted.", "success") + $('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/www/teams/create.json.spt b/www/teams/create.json.spt index 746e79d28d..6d6a973684 100644 --- a/www/teams/create.json.spt +++ b/www/teams/create.json.spt @@ -40,4 +40,4 @@ if request.method == 'POST': raise Response(400, _("Sorry, there is already a team using '{}'.", fields['slug'])) [---] application/json via json_dump -{} +{'email': user.participant.email_address} From 161d52d0900d512d112af0cd00315704a5089fc1 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Wed, 13 May 2015 19:20:35 -0400 Subject: [PATCH 15/17] Perform validation on team owners - must have a verified email - must have a payout route - can't be stub, closed, or suspicious --- gratipay/models/participant.py | 8 ++++++++ tests/py/test_routes.py | 3 +++ tests/py/test_teams.py | 21 +++++++++++++++++---- www/new.spt | 8 +++++++- www/teams/create.json.spt | 11 +++++++++++ 5 files changed, 46 insertions(+), 5 deletions(-) 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/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 17b555478d..eba47633b7 100644 --- a/tests/py/test_teams.py +++ b/tests/py/test_teams.py @@ -39,7 +39,7 @@ def test_can_construct_from_id(self): assert team.owner == 'hannibal' def test_can_create_new_team(self): - self.make_participant('alice') + 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 @@ -49,8 +49,20 @@ 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') + 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) @@ -58,7 +70,7 @@ def test_error_message_for_terms(self): assert "Please agree to the terms of service." in r.body def test_error_message_for_missing_fields(self): - self.make_participant('alice') + 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) @@ -66,12 +78,13 @@ def test_error_message_for_missing_fields(self): assert "Please fill out the 'Team Name' field." in r.body def test_error_message_for_slug_collision(self): - self.make_participant('alice') + 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): def setUp(self): diff --git a/www/new.spt b/www/new.spt index 5d5de7e43f..3c42b3bb59 100644 --- a/www/new.spt +++ b/www/new.spt @@ -6,7 +6,13 @@ 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")) + 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.")) title = _("Apply for a New Team") diff --git a/www/teams/create.json.spt b/www/teams/create.json.spt index 6d6a973684..f5d05e94bd 100644 --- a/www/teams/create.json.spt +++ b/www/teams/create.json.spt @@ -17,6 +17,17 @@ field_names = { 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.")) + +if user.participant.claimed_time is None \ +or user.participant.is_suspicious is True \ +or user.participant.is_closed: # sanity checks + raise Response(400, _("How are you applying for a team!?")) + if not request.body.get('agree_terms', False): raise Response(400, _("Please agree to the terms of service.")) From 240aa19878dfd3d9b2b67539de501c6961182ab2 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Wed, 13 May 2015 20:47:53 -0400 Subject: [PATCH 16/17] Let's short-cut migration We don't need to migrate when the user applies. We need to migrate before we run payday. Users can apply for a team and we can migrate later. --- www/new.spt | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/www/new.spt b/www/new.spt index 3c42b3bb59..cc3e955a1a 100644 --- a/www/new.spt +++ b/www/new.spt @@ -15,6 +15,22 @@ 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" %} @@ -61,6 +77,13 @@ title = _("Apply for a New Team")

+ + {% if ntips %} +

The {{ ntips }} weekly payments totalling ${{ receiving }} that + previously were directed at you will be redirected to your new team, + pending approval of your application.

+ {% endif %} +
From ffd407b6d14784aaae134b3209cd8b01bf86cdd2 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Thu, 14 May 2015 09:38:08 -0400 Subject: [PATCH 17/17] Add revenue model question to team application Restructured a bit to fit this. --- www/new.spt | 9 ++++++--- www/teams/create.json.spt | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/www/new.spt b/www/new.spt index cc3e955a1a..d18a64be6c 100644 --- a/www/new.spt +++ b/www/new.spt @@ -57,13 +57,16 @@ title = _("Apply for a New Team") - + +

{{ _("What product or service does your team provide?") }}

- + +

{{ _("How can other people get involved with your team?") }}

- + +

{{ _("What is your revenue model? How do you share revenue with contributors?") }}


diff --git a/www/teams/create.json.spt b/www/teams/create.json.spt index f5d05e94bd..2194fb17d1 100644 --- a/www/teams/create.json.spt +++ b/www/teams/create.json.spt @@ -10,8 +10,8 @@ field_names = { 'name': 'Team Name', 'homepage': 'Homepage', 'product_or_service': 'Product or Service', - 'getting_paid': 'How do you share revenue?', - 'getting_involved': 'How can other people get involved?' + 'getting_paid': 'Revenue', + 'getting_involved': 'Contributing' } if user.ANON: