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

Add team receiving page #3873

Merged
merged 10 commits into from
Apr 9, 2016
96 changes: 96 additions & 0 deletions gratipay/models/team.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"""Teams on Gratipay receive payments and distribute payroll.
"""
import re
from decimal import Decimal

import requests
from aspen import json, log
from gratipay.exceptions import InvalidTeamName
from gratipay.models import add_event
from postgres.orm import Model

from gratipay.billing.exchanges import MINIMUM_CHARGE

# Should have at least one letter.
TEAM_NAME_PATTERN = re.compile(r'^(?=.*[A-Za-z])([A-Za-z0-9.,-_ ]+)$')
Expand Down Expand Up @@ -87,6 +90,60 @@ def insert(cls, owner, **fields):

""", fields)

def get_payment_distribution(self):
"""
Returns a data structure in the form of::
[
[PAYMENT1, PAYMENT2...PAYMENTN],
nreceiving_from,
total_amount_received
]
where each PAYMENTN is in the form::
[
amount,
number_of_tippers_for_this_amount,
total_amount_given_at_this_amount,
proportion_of_payments_at_this_amount,
proportion_of_total_amount_at_this_amount
]
"""
SQL = """
SELECT amount
, count(amount) AS nreceiving_from
FROM ( SELECT DISTINCT ON (participant)
amount
, participant
FROM payment_instructions
JOIN participants p ON p.username = participant
WHERE team=%s
AND is_funded
AND p.is_suspicious IS NOT true
ORDER BY participant
, mtime DESC
) AS foo
WHERE amount > 0
GROUP BY amount
ORDER BY amount
"""

tip_amounts = []

npatrons = 0.0 # float to trigger float division
total_amount = Decimal('0.00')
for rec in self.db.all(SQL, (self.slug,)):
tip_amounts.append([ rec.amount
, rec.nreceiving_from
, rec.amount * rec.nreceiving_from
])
total_amount += tip_amounts[-1][2]
npatrons += rec.nreceiving_from

for row in tip_amounts:
row.append((row[1] / npatrons) if npatrons > 0 else 0)
row.append((row[2] / total_amount) if total_amount > 0 else 0)

return tip_amounts, npatrons, total_amount


def update(self, **kw):
updateable = frozenset(['name', 'product_or_service', 'homepage',
Expand Down Expand Up @@ -115,6 +172,45 @@ def update(self, **kw):
))
self.set_attributes(**kw)


def get_dues(self):
rec = self.db.one("""
WITH our_cpi AS (
SELECT due, is_funded
FROM current_payment_instructions cpi
WHERE team=%(slug)s
)
SELECT (
SELECT COALESCE(SUM(due), 0)
FROM our_cpi
WHERE is_funded
) AS funded
, (
SELECT COALESCE(SUM(due), 0)
FROM our_cpi
WHERE NOT is_funded
) AS unfunded
""", {'slug': self.slug})

return rec.funded, rec.unfunded


def get_upcoming_payment(self):
return self.db.one("""
SELECT COALESCE(SUM(amount + due), 0)
FROM current_payment_instructions cpi
JOIN participants p ON cpi.participant = p.username
WHERE team = %(slug)s
AND is_funded -- Check whether the payment is funded
AND ( -- Check whether the user will hit the minimum charge
SELECT SUM(amount + due)
FROM current_payment_instructions cpi2
WHERE cpi2.participant = p.username
AND cpi2.is_funded
) >= %(mcharge)s
""", {'slug': self.slug, 'mcharge': MINIMUM_CHARGE})


def create_github_review_issue(self):
"""POST to GitHub, and return the URL of the new issue.
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.tip-distribution {
.payment-distribution {
font: normal 12px $Mono;
.dollar-sign {
padding: 0 2pt 0 24pt;
Expand Down
9 changes: 9 additions & 0 deletions templates/filtered-nav.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<ul class="nav">
{% for slug, name, show_them, show_others in pages %}
<li>
<a href="{{ nav_base }}{{ slug }}"{% if slug.strip('/') == current_page.strip('/') %} class="selected"{% endif %}>
{{ _(name) }}
</a>
</li>
{% endfor %}
</ul>
14 changes: 14 additions & 0 deletions templates/payment-distribution-amount.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<table class="payment-distribution">
{% for amount, ncontributors, summed, pcontributors, psummed in payment_distribution %}
<tr>
<td class="dollar-sign">$</td>
<td class="amount">{{ amount }}</td>
<td colspan="2"></td>
<td class="count">
<span class="green bar" style="width: {{ psummed * 240 }}pt"></span>
{{ "%5.1f" % (psummed * 100) }}%
<span class="number">(${{ summed }})</span>
</td>
</tr>
{% endfor %}
</table>
13 changes: 13 additions & 0 deletions templates/payment-distribution-number.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<table class="payment-distribution">
{% for amount, ncontributors, summed, pcontributors, psummed in payment_distribution %}
<tr>
<td class="dollar-sign">$</td>
<td class="amount">{{ amount }}</td>
<td class="count">
<span class="bar" style="width:{{ pcontributors * 240 }}pt"></span>
{{ "%5.1f" % (pcontributors * 100) }}%
<span class="number">({{ ncontributors }})</span>
</td>
</tr>
{% endfor %}
</table>
6 changes: 1 addition & 5 deletions templates/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,6 @@
] %}
{% set pages = filter_profile_nav(user, participant, pages) %}
{% if pages %}
<ul class="nav">
{% for slug, name, show_them, show_others in pages %}
<li><a href="{{ nav_base }}{{ slug }}"{% if slug.strip('/') == current_page.strip('/') %} class="selected"{% endif %}>{{ _(name) }}</a></li>
{% endfor %}
</ul>
{% include "templates/filtered-nav.html" %}
{% endif %}
{% endblock %}
77 changes: 77 additions & 0 deletions templates/team-base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{% extends "templates/base.html" %}

{% block banner %}
<div class="avatar">
<img class="avatar" src="{{ team.get_image_url('large') }}">
</div>

<div class="details mono{% if not team.is_approved %} luxury{% endif %}">
{% if team.is_approved %}
<table>
<tr>
<th class="label">{{ _("Weekly") }}</th>
<th>&#x03A3; &nbsp;</th>
<th>n</th>
</tr>
<tr>
<td class="label">{{ _("Receiving") }}</td>
<td class="total-receiving" data-team="{{ team.id }}">{{ format_currency(team.receiving, 'USD') }}</td>
<td class="nreceiving-from" data-team="{{ team.id }}">{{ team.nreceiving_from }}</td>
</tr>
<tr>
<td class="label">{{ _("Sharing") }}</td>
<td>{{ format_currency(0, 'USD') }}</td>
<td>0</td>
</tr>
</table>
{% endif %}

<p class="luxury">
{% if team.is_approved %}
{{ _('Joined {ago}', ago=to_age(team.ctime, add_direction=True)) }}
{% else %}
{{ _('Applied {ago}', ago=to_age(team.ctime, add_direction=True)) }}
{% endif %}
</p>
</div>

{{ super() }}
{% endblock %}

{% block sidebar %}

{% if user.ADMIN %}
<form id="status-knob" action="set-status.json">
<select name="status">
<option {{ 'selected' if team.is_approved == None else '' }}
value="unreviewed">Under Review</option>
<option {{ 'selected' if team.is_approved == False else '' }}
value="rejected">Rejected</option>
<option {{ 'selected' if team.is_approved == True else '' }}
value="approved">Approved</option>
</select>
<br>
<br>
<button type="submit">Set Status</button>
<br>
<br>
</form>
{% endif %}

{% if team.is_approved and (user.participant == team.owner or user.ADMIN) %}
{% set current_page = request.path.raw.split('/')[2] %}
{% set nav_base = '/' + team.slug %}
{% set pages = [ ('/', _('Profile'))
, ('/receiving/', _('Receiving'))
] %}
{% if pages %}
{% include "templates/nav.html" %}
<br>
{% endif %}
{% endif %}

{% if (user.participant.username != team.owner) and team.is_approved %}
{% include "templates/your-payment.html" %}
{% endif %}

{% endblock %}
13 changes: 0 additions & 13 deletions templates/tip-distribution.html

This file was deleted.

38 changes: 37 additions & 1 deletion tests/py/test_teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,9 @@ def test_stripping_required_inputs(self):
assert self.db.one("SELECT COUNT(*) FROM teams") == 0
assert "Please fill out the 'Team Name' field." in r.body

# Migrate Tips
# ============

def test_migrate_tips_to_payment_instructions(self):
alice = self.make_participant('alice', claimed_time='now')
bob = self.make_participant('bob', claimed_time='now')
Expand Down Expand Up @@ -390,7 +393,40 @@ def test_migrate_tips_checks_for_multiple_teams(self):
assert len(payment_instructions) == 1


# cached values - receiving, nreceiving_from
# Dues, Upcoming Payment
# ======================

def test_get_dues(self):
alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
bob = self.make_participant('bob', claimed_time='now', last_bill_result='Fail!')
team = self.make_team(is_approved=True)

alice.set_payment_instruction(team, '3.00') # Funded
bob.set_payment_instruction(team, '5.00') # Unfunded

# Simulate dues
self.db.run("UPDATE payment_instructions SET due = amount")

assert team.get_dues() == (3, 5)


def test_upcoming_payment(self):
alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
bob = self.make_participant('bob', claimed_time='now', last_bill_result='')
carl = self.make_participant('carl', claimed_time='now', last_bill_result='Fail!')
team = self.make_team(is_approved=True)

alice.set_payment_instruction(team, '5.00') # Funded
bob.set_payment_instruction(team, '3.00') # Funded, but won't hit minimum charge
carl.set_payment_instruction(team, '10.00') # Unfunded

# Simulate dues
self.db.run("UPDATE payment_instructions SET due = amount")

assert team.get_upcoming_payment() == 10 # 2 * Alice's $5

# Cached Values
# =============

def test_receiving_only_includes_funded_payment_instructions(self):
alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
Expand Down
Loading