mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-04-28 17:32:38 +02:00
Merge pull request #324 from JocelynDelalande/jd-stats-api
Add an API endpoint for statistics
This commit is contained in:
commit
667e555d67
7 changed files with 112 additions and 36 deletions
|
@ -17,6 +17,11 @@ Fixed
|
|||
- Fix the "IOError" crash when running `ihatemoney generate-config` (#308)
|
||||
- Made the left-hand sidebar scrollable (#318)
|
||||
|
||||
Added
|
||||
=====
|
||||
|
||||
- Statistics API (#343)
|
||||
|
||||
|
||||
2.0 (2017-12-27)
|
||||
----------------
|
||||
|
|
22
docs/api.rst
22
docs/api.rst
|
@ -164,3 +164,25 @@ And you can of course `DELETE` them at `/api/projects/<id>/bills/<bill-id>`::
|
|||
$ curl --basic -u demo:demo -X DELETE\
|
||||
https://ihatemoney.org/api/projects/demo/bills/80\
|
||||
"OK"
|
||||
|
||||
|
||||
Statistics
|
||||
----------
|
||||
|
||||
You can get some project stats with a `GET` on `/api/projects/<id>/statistics`::
|
||||
|
||||
$ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/statistics
|
||||
[
|
||||
{
|
||||
"balance": 12.5,
|
||||
"member": {"activated": True, "id": 1, "name": "alexis", "weight": 1.0},
|
||||
"paid": 25.0,
|
||||
"spent": 12.5
|
||||
},
|
||||
{
|
||||
"balance": -12.5,
|
||||
"member": {"activated": True, "id": 2, "name": "fred", "weight": 1.0},
|
||||
"paid": 0,
|
||||
"spent": 12.5
|
||||
}
|
||||
]
|
||||
|
|
|
@ -65,6 +65,13 @@ class ProjectHandler(Resource):
|
|||
return form.errors, 400
|
||||
|
||||
|
||||
class ProjectStatsHandler(Resource):
|
||||
method_decorators = [need_auth]
|
||||
|
||||
def get(self, project):
|
||||
return project.members_stats
|
||||
|
||||
|
||||
class APIMemberForm(MemberForm):
|
||||
""" Member is not disablable via a Form.
|
||||
|
||||
|
@ -163,6 +170,7 @@ class BillHandler(Resource):
|
|||
restful_api.add_resource(ProjectsHandler, '/projects')
|
||||
restful_api.add_resource(ProjectHandler, '/projects/<string:project_id>')
|
||||
restful_api.add_resource(MembersHandler, "/projects/<string:project_id>/members")
|
||||
restful_api.add_resource(ProjectStatsHandler, "/projects/<string:project_id>/statistics")
|
||||
restful_api.add_resource(MemberHandler, "/projects/<string:project_id>/members/<int:member_id>")
|
||||
restful_api.add_resource(BillsHandler, "/projects/<string:project_id>/bills")
|
||||
restful_api.add_resource(BillHandler, "/projects/<string:project_id>/bills/<int:bill_id>")
|
||||
|
|
|
@ -52,6 +52,26 @@ class Project(db.Model):
|
|||
|
||||
return balances
|
||||
|
||||
@property
|
||||
def members_stats(self):
|
||||
"""Compute what each member has paid
|
||||
|
||||
:return: one stat dict per member
|
||||
:rtype list:
|
||||
"""
|
||||
return [{
|
||||
'member': member,
|
||||
'paid': sum([
|
||||
bill.amount
|
||||
for bill in self.get_member_bills(member.id).all()
|
||||
]),
|
||||
'spent': sum([
|
||||
bill.pay_each() * member.weight
|
||||
for bill in self.get_bills().all() if member in bill.owers
|
||||
]),
|
||||
'balance': self.balance[member.id]
|
||||
} for member in self.active_members]
|
||||
|
||||
@property
|
||||
def uses_weights(self):
|
||||
return len([i for i in self.members if i.weight != 1]) > 0
|
||||
|
|
|
@ -3,12 +3,11 @@
|
|||
{% block sidebar %}
|
||||
<div id="table_overflow">
|
||||
<table class="balance table">
|
||||
{% set balance = g.project.balance %}
|
||||
{% for member in g.project.members | sort(attribute='name') if member.activated or balance[member.id]|round(2) != 0 %}
|
||||
<tr id="bal-member-{{ member.id }}" action={% if member.activated %}delete{% else %}reactivate{% endif %}>
|
||||
<td class="balance-name">{{ member.name }}</td>
|
||||
<td class="balance-value {% if balance[member.id]|round(2) > 0 %}positive{% elif balance[member.id]|round(2) < 0 %}negative{% endif %}">
|
||||
{% if balance[member.id]|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }}
|
||||
{% for stat in members_stats| sort(attribute='member.name') %}
|
||||
<tr>
|
||||
<td class="balance-name">{{ stat.member.name }}</td>
|
||||
<td class="balance-value {% if stat.balance|round(2) > 0 %}positive{% elif stat.balance|round(2) < 0 %}negative{% endif %}">
|
||||
{% if stat.balance|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(stat.balance) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -21,12 +20,12 @@
|
|||
<table id="bill_table" class="split_bills table table-striped">
|
||||
<thead><tr><th>{{ _("Who?") }}</th><th>{{ _("Paid") }}</th><th>{{ _("Spent") }}</th><th>{{ _("Balance") }}</th></tr></thead>
|
||||
<tbody>
|
||||
{% for member in members %}
|
||||
{% for stat in members_stats %}
|
||||
<tr>
|
||||
<td>{{ member.name }}</td>
|
||||
<td>{{ "%0.2f"|format(paid[member.id]) }}</td>
|
||||
<td>{{ "%0.2f"|format(spent[member.id]) }}</td>
|
||||
<td>{{ "%0.2f"|format(balance[member.id]) }}</td>
|
||||
<td>{{ stat.member.name }}</td>
|
||||
<td>{{ "%0.2f"|format(stat.paid) }}</td>
|
||||
<td>{{ "%0.2f"|format(stat.spent) }}</td>
|
||||
<td>{{ "%0.2f"|format(stat.balance) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
@ -750,24 +750,24 @@ class BudgetTestCase(IhatemoneyTestCase):
|
|||
})
|
||||
|
||||
response = self.client.get("/raclette/statistics")
|
||||
self.assertIn("<td>alexis</td>\n "
|
||||
+ "<td>20.00</td>\n "
|
||||
+ "<td>31.67</td>\n "
|
||||
self.assertIn("<td>alexis</td>\n "
|
||||
+ "<td>20.00</td>\n "
|
||||
+ "<td>31.67</td>\n "
|
||||
+ "<td>-11.67</td>\n",
|
||||
response.data.decode('utf-8'))
|
||||
self.assertIn("<td>fred</td>\n "
|
||||
+ "<td>20.00</td>\n "
|
||||
+ "<td>5.83</td>\n "
|
||||
self.assertIn("<td>fred</td>\n "
|
||||
+ "<td>20.00</td>\n "
|
||||
+ "<td>5.83</td>\n "
|
||||
+ "<td>14.17</td>\n",
|
||||
response.data.decode('utf-8'))
|
||||
self.assertIn("<td>tata</td>\n "
|
||||
+ "<td>0.00</td>\n "
|
||||
+ "<td>2.50</td>\n "
|
||||
self.assertIn("<td>tata</td>\n "
|
||||
+ "<td>0.00</td>\n "
|
||||
+ "<td>2.50</td>\n "
|
||||
+ "<td>-2.50</td>\n",
|
||||
response.data.decode('utf-8'))
|
||||
self.assertIn("<td>toto</td>\n "
|
||||
+ "<td>0.00</td>\n "
|
||||
+ "<td>0.00</td>\n "
|
||||
self.assertIn("<td>toto</td>\n "
|
||||
+ "<td>0.00</td>\n "
|
||||
+ "<td>0.00</td>\n "
|
||||
+ "<td>0.00</td>\n",
|
||||
response.data.decode('utf-8'))
|
||||
|
||||
|
@ -1325,6 +1325,40 @@ class APITestCase(IhatemoneyTestCase):
|
|||
headers=self.get_auth("raclette"))
|
||||
self.assertStatus(404, req)
|
||||
|
||||
def test_statistics(self):
|
||||
# create a project
|
||||
self.api_create("raclette")
|
||||
|
||||
# add members
|
||||
self.api_add_member("raclette", "alexis")
|
||||
self.api_add_member("raclette", "fred")
|
||||
|
||||
# add a bill
|
||||
req = self.client.post("/api/projects/raclette/bills", data={
|
||||
'date': '2011-08-10',
|
||||
'what': 'fromage',
|
||||
'payer': "1",
|
||||
'payed_for': ["1", "2"],
|
||||
'amount': '25',
|
||||
}, headers=self.get_auth("raclette"))
|
||||
|
||||
# get the list of bills (should be empty)
|
||||
req = self.client.get("/api/projects/raclette/statistics",
|
||||
headers=self.get_auth("raclette"))
|
||||
self.assertStatus(200, req)
|
||||
self.assertEqual([
|
||||
{'balance': 12.5,
|
||||
'member': {'activated': True, 'id': 1,
|
||||
'name': 'alexis', 'weight': 1.0},
|
||||
'paid': 25.0,
|
||||
'spent': 12.5},
|
||||
{'balance': -12.5,
|
||||
'member': {'activated': True, 'id': 2,
|
||||
'name': 'fred', 'weight': 1.0},
|
||||
'paid': 0,
|
||||
'spent': 12.5}],
|
||||
json.loads(req.data.decode('utf-8')))
|
||||
|
||||
def test_username_xss(self):
|
||||
# create a project
|
||||
# self.api_create("raclette")
|
||||
|
|
|
@ -566,21 +566,9 @@ def settle_bill():
|
|||
@main.route("/<project_id>/statistics")
|
||||
def statistics():
|
||||
"""Compute what each member has paid and spent and display it"""
|
||||
members = g.project.active_members
|
||||
balance = g.project.balance
|
||||
paid = {}
|
||||
spent = {}
|
||||
for member in members:
|
||||
paid[member.id] = sum([bill.amount
|
||||
for bill in g.project.get_member_bills(member.id).all()])
|
||||
spent[member.id] = sum([bill.pay_each() * member.weight
|
||||
for bill in g.project.get_bills().all() if member in bill.owers])
|
||||
return render_template(
|
||||
"statistics.html",
|
||||
members=members,
|
||||
balance=balance,
|
||||
paid=paid,
|
||||
spent=spent,
|
||||
members_stats=g.project.members_stats,
|
||||
current_view='statistics',
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in a new issue