Skip to content
This repository has been archived by the owner on Feb 8, 2018. It is now read-only.

Team creation #3412

Merged
merged 17 commits into from
May 14, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions gratipay/models/participant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions gratipay/models/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

31 changes: 31 additions & 0 deletions js/gratipay/new_team.js
Original file line number Diff line number Diff line change
@@ -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( "<p>Thank you! We will follow up shortly with an email to <b>"
+ d.email + "</b>. Please <a hef=\"mailto:[email protected]\">email "
+ "us</a> with any questions.</p>"
)
},
error: [Gratipay.error, function () { $input.prop('disable', false); }]
});
}
3 changes: 3 additions & 0 deletions tests/py/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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'
Expand Down
60 changes: 60 additions & 0 deletions tests/py/test_teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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='[email protected]')
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='[email protected]', 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='[email protected]', 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='[email protected]', 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):

Expand Down
93 changes: 93 additions & 0 deletions www/new.spt
Original file line number Diff line number Diff line change
@@ -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 %}
<script>$(document).ready(Gratipay.new_team.initForm);</script>
{{ super() }}
{% endblock %}

{% block content %}
<div class="col0">
<style>
textarea {
width: 100%;
height: 200px;
}
</style>
<form action="/teams/create.json" method="POST" id="new-team">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />

<label><h2>{{ _("Team Name") }}</h2></label>
<input type="text" name="name" required autofocus />

<label><h2>{{ _("Homepage") }}</h2></label>
<input type="text" name="homepage" required />

<label><h2>{{ _("Product or Service") }}</h2></label>
<p>{{ _("What product or service does your team provide?") }}</p>
<textarea name="product_or_service" required></textarea>

<label><h2>{{ _("Contributing") }}</h2></label>
<p>{{ _("How can other people get involved with your team?") }}</p>
<textarea name="getting_involved" required></textarea>

<label><h2>{{ _("Revenue") }}</h2></label>
<p>{{ _("What is your revenue model? How do you share revenue with contributors?") }}</p>
<textarea name="getting_paid" required></textarea>

<br>
<br>
<input type="checkbox" value="true" name="agree_terms" id="agree_terms">
<label for="agree_terms">
{{ _( "I agree to the {0}terms of service{1}"
, '<a href="/about/policies/terms-of-service">'|safe
, '</a>'|safe
) }}
</label>
<br>
<br>

{% if ntips %}
<p>The {{ ntips }} weekly payments totalling ${{ receiving }} that
previously were directed at you will be <b>redirected to your new team</b>,
pending approval of your application.</p>
{% endif %}

<button type="submit">{{ _("Apply") }}</button>
</form>
</div>
{% endblock %}
54 changes: 54 additions & 0 deletions www/teams/create.json.spt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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': 'Team Name',
'homepage': 'Homepage',
'product_or_service': 'Product or Service',
'getting_paid': 'Revenue',
'getting_involved': 'Contributing'
}

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."))

if request.method == 'POST':
fields = {}

# Validate inputs

for field in field_names.keys():
value = request.body.get(field, '')
if not value:
raise Response(400, _("Please fill out the '{0}' field.", field_names[field]))

fields[field] = value

fields['slug'] = slugize(fields['name'])

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
{'email': user.participant.email_address}