diff options
| -rw-r--r-- | CHANGELOG.rst | 5 | ||||
| -rw-r--r-- | docs/api.rst | 22 | ||||
| -rw-r--r-- | ihatemoney/api.py | 8 | ||||
| -rw-r--r-- | ihatemoney/models.py | 20 | ||||
| -rw-r--r-- | ihatemoney/templates/statistics.html | 21 | ||||
| -rw-r--r-- | ihatemoney/tests/tests.py | 58 | ||||
| -rw-r--r-- | ihatemoney/web.py | 14 |
7 files changed, 112 insertions, 36 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ea2b61e..fccb08d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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) ---------------- diff --git a/docs/api.rst b/docs/api.rst index b82c6f3..0ae4214 100644 --- a/docs/api.rst +++ b/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 + } + ] diff --git a/ihatemoney/api.py b/ihatemoney/api.py index 31ed06c..6068cf7 100644 --- a/ihatemoney/api.py +++ b/ihatemoney/api.py @@ -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>") diff --git a/ihatemoney/models.py b/ihatemoney/models.py index aa3083d..c6ce23f 100644 --- a/ihatemoney/models.py +++ b/ihatemoney/models.py @@ -53,6 +53,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 diff --git a/ihatemoney/templates/statistics.html b/ihatemoney/templates/statistics.html index ae1c80e..1b07a33 100644 --- a/ihatemoney/templates/statistics.html +++ b/ihatemoney/templates/statistics.html @@ -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> diff --git a/ihatemoney/tests/tests.py b/ihatemoney/tests/tests.py index c13131c..3797f09 100644 --- a/ihatemoney/tests/tests.py +++ b/ihatemoney/tests/tests.py @@ -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") diff --git a/ihatemoney/web.py b/ihatemoney/web.py index 6b1b358..1e16202 100644 --- a/ihatemoney/web.py +++ b/ihatemoney/web.py @@ -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', ) |
