Merge branch 'master' into almet/fix-supervisord-template

This commit is contained in:
Alexis Metaireau 2018-07-16 22:58:48 +02:00 committed by GitHub
commit 1d0880f3cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 817 additions and 556 deletions

View file

@ -3,13 +3,34 @@ Changelog
This document describes changes between each past release. This document describes changes between each past release.
2.1 (unreleased) 2.1.1 (unreleased)
------------------
- Regenerate translations (#338)
2.1 (2018-02-16)
---------------- ----------------
Changed
=======
- Use flask-restful instead of deprecated flask-rest for the REST API (#315)
- Make sidebar scrollable. Usefull for large groups (#316)
Fixed Fixed
===== =====
- Fix the "IOError" crash when running `ihatemoney generate-config` (#308) - Fix the "IOError" crash when running `ihatemoney generate-config` (#308)
- Made the left-hand sidebar scrollable (#318)
- Fix and enhanche Docker support (#320, #321)
Added
=====
- Statistics API (#343)
- Allow to disable/enable member via API (#301)
- Enable basic Apache auth passthrough for API (#303)
2.0 (2017-12-27) 2.0 (2017-12-27)

View file

@ -9,6 +9,7 @@ Alexis Metaireau <alexis@notmyidea.org>
Arnaud Bos <arnaud.tlse@gmail.com> Arnaud Bos <arnaud.tlse@gmail.com>
Baptiste Jonglez <git@bitsofnetworks.org> Baptiste Jonglez <git@bitsofnetworks.org>
Berteh <berteh@gmail.com> Berteh <berteh@gmail.com>
donkers <thedonkers@gmail.com>
Feth AREZKI <feth@tuttu.info> Feth AREZKI <feth@tuttu.info>
Frédéric Sureau <fredericsureau@gmail.com> Frédéric Sureau <fredericsureau@gmail.com>
Jocelyn Delalande <jocelyn@crapouillou.net> Jocelyn Delalande <jocelyn@crapouillou.net>

View file

@ -4,12 +4,11 @@ RUN mkdir /ihatemoney &&\
mkdir -p /etc/ihatemoney &&\ mkdir -p /etc/ihatemoney &&\
pip install --no-cache-dir gunicorn pymysql pip install --no-cache-dir gunicorn pymysql
WORKDIR /ihatemoney COPY . /ihatemoney
COPY . .
ARG INSTALL_FROM_PYPI="False" ARG INSTALL_FROM_PYPI="False"
RUN if [ "$INSTALL_FROM_PYPI" = True ]; then\ RUN if [ "$INSTALL_FROM_PYPI" = True ]; then\
pip install --no-cache-dir ihatemoney ; else\ pip install --no-cache-dir ihatemoney ; else\
pip install --no-cache-dir -e . ; \ pip install --no-cache-dir -e /ihatemoney ; \
fi fi
ENV DEBUG="False" \ ENV DEBUG="False" \
@ -26,9 +25,8 @@ ENV DEBUG="False" \
ACTIVATE_DEMO_PROJECT="True" \ ACTIVATE_DEMO_PROJECT="True" \
ADMIN_PASSWORD="" \ ADMIN_PASSWORD="" \
ALLOW_PUBLIC_PROJECT_CREATION="True" \ ALLOW_PUBLIC_PROJECT_CREATION="True" \
ACTIVATE_ADMIN_DASHBOARD="False" \ ACTIVATE_ADMIN_DASHBOARD="False"
GUNICORN_NUM_WORKERS="3"
VOLUME /database VOLUME /database
EXPOSE 8000 EXPOSE 8000
CMD ["/ihatemoney/conf/confandrun.sh"] ENTRYPOINT ["/ihatemoney/conf/confandrun.sh"]

View file

@ -17,7 +17,8 @@ ADMIN_PASSWORD = "$ADMIN_PASSWORD"
ALLOW_PUBLIC_PROJECT_CREATION = $ALLOW_PUBLIC_PROJECT_CREATION ALLOW_PUBLIC_PROJECT_CREATION = $ALLOW_PUBLIC_PROJECT_CREATION
ACTIVATE_ADMIN_DASHBOARD = $ACTIVATE_ADMIN_DASHBOARD ACTIVATE_ADMIN_DASHBOARD = $ACTIVATE_ADMIN_DASHBOARD
EOF EOF
gunicorn ihatemoney.wsgi:application \ # Start gunicorn without forking
exec gunicorn ihatemoney.wsgi:application \
-b 0.0.0.0:8000 \ -b 0.0.0.0:8000 \
--log-syslog \ --log-syslog \
-w "$GUNICORN_NUM_WORKERS" "$@"

View file

@ -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\ $ curl --basic -u demo:demo -X DELETE\
https://ihatemoney.org/api/projects/demo/bills/80\ https://ihatemoney.org/api/projects/demo/bills/80\
"OK" "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
}
]

View file

@ -154,9 +154,10 @@ A volume can also be specified to persist the default database file::
docker run -d -p 8000:8000 -v /host/path/to/database:/database ihatemoney docker run -d -p 8000:8000 -v /host/path/to/database:/database ihatemoney
The following gunicorn parameters are also available:: Additional gunicorn parameters can be passed using the docker ``CMD`` parameter.
For example, use the following command to add more gunicorn workers::
GUNICORN_NUM_WORKERS (default: 3) docker run -d -p 8000:8000 ihatemoney -w 3
Configuration Configuration
============= =============

View file

@ -1,62 +1,75 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from flask import Blueprint, request from flask import Blueprint, request
from flask_rest import RESTResource, need_auth from flask_restful import Resource, Api, abort
from wtforms.fields.core import BooleanField from wtforms.fields.core import BooleanField
from ihatemoney.models import db, Project, Person, Bill from ihatemoney.models import db, Project, Person, Bill
from ihatemoney.forms import (ProjectForm, EditProjectForm, MemberForm, from ihatemoney.forms import (ProjectForm, EditProjectForm, MemberForm,
get_billform_for) get_billform_for)
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
from functools import wraps
api = Blueprint("api", __name__, url_prefix="/api") api = Blueprint("api", __name__, url_prefix="/api")
restful_api = Api(api)
def check_project(*args, **kwargs): def need_auth(f):
"""Check the request for basic authentication for a given project. """Check the request for basic authentication for a given project.
Return the project if the authorization is good, False otherwise Return the project if the authorization is good, abort the request with a 401 otherwise
""" """
@wraps(f)
def wrapper(*args, **kwargs):
auth = request.authorization auth = request.authorization
project_id = kwargs.get("project_id")
# project_id should be contained in kwargs and equal to the username if auth and project_id and auth.username == project_id:
if auth and "project_id" in kwargs and \
auth.username == kwargs["project_id"]:
project = Project.query.get(auth.username) project = Project.query.get(auth.username)
if project and check_password_hash(project.password, auth.password): if project and check_password_hash(project.password, auth.password):
return project # The whole project object will be passed instead of project_id
return False kwargs.pop("project_id")
return f(*args, project=project, **kwargs)
abort(401)
return wrapper
class ProjectHandler(object): class ProjectsHandler(Resource):
def post(self):
def add(self):
form = ProjectForm(meta={'csrf': False}) form = ProjectForm(meta={'csrf': False})
if form.validate(): if form.validate():
project = form.save() project = form.save()
db.session.add(project) db.session.add(project)
db.session.commit() db.session.commit()
return 201, project.id return project.id, 201
return 400, form.errors return form.errors, 400
class ProjectHandler(Resource):
method_decorators = [need_auth]
@need_auth(check_project, "project")
def get(self, project): def get(self, project):
return 200, project return project
@need_auth(check_project, "project")
def delete(self, project): def delete(self, project):
db.session.delete(project) db.session.delete(project)
db.session.commit() db.session.commit()
return 200, "DELETED" return "DELETED"
@need_auth(check_project, "project") def put(self, project):
def update(self, project):
form = EditProjectForm(meta={'csrf': False}) form = EditProjectForm(meta={'csrf': False})
if form.validate(): if form.validate():
form.update(project) form.update(project)
db.session.commit() db.session.commit()
return 200, "UPDATED" return "UPDATED"
return 400, form.errors return form.errors, 400
class ProjectStatsHandler(Resource):
method_decorators = [need_auth]
def get(self, project):
return project.members_stats
class APIMemberForm(MemberForm): class APIMemberForm(MemberForm):
@ -71,98 +84,93 @@ class APIMemberForm(MemberForm):
return super(APIMemberForm, self).save(project, person) return super(APIMemberForm, self).save(project, person)
class MemberHandler(object): class MembersHandler(Resource):
method_decorators = [need_auth]
def get(self, project, member_id): def get(self, project):
member = Person.query.get(member_id, project) return project.members
if not member or member.project != project:
return 404, "Not Found"
return 200, member
def list(self, project): def post(self, project):
return 200, project.members
def add(self, project):
form = MemberForm(project, meta={'csrf': False}) form = MemberForm(project, meta={'csrf': False})
if form.validate(): if form.validate():
member = Person() member = Person()
form.save(project, member) form.save(project, member)
db.session.commit() db.session.commit()
return 201, member.id return member.id, 201
return 400, form.errors return form.errors, 400
def update(self, project, member_id):
class MemberHandler(Resource):
method_decorators = [need_auth]
def get(self, project, member_id):
member = Person.query.get(member_id, project)
if not member or member.project != project:
return "Not Found", 404
return member
def put(self, project, member_id):
form = APIMemberForm(project, meta={'csrf': False}, edit=True) form = APIMemberForm(project, meta={'csrf': False}, edit=True)
if form.validate(): if form.validate():
member = Person.query.get(member_id, project) member = Person.query.get(member_id, project)
form.save(project, member) form.save(project, member)
db.session.commit() db.session.commit()
return 200, member return member
return 400, form.errors return form.errors, 400
def delete(self, project, member_id): def delete(self, project, member_id):
if project.remove_member(member_id): if project.remove_member(member_id):
return 200, "OK" return "OK"
return 404, "Not Found" return "Not Found", 404
class BillHandler(object): class BillsHandler(Resource):
method_decorators = [need_auth]
def get(self, project, bill_id): def get(self, project):
bill = Bill.query.get(project, bill_id)
if not bill:
return 404, "Not Found"
return 200, bill
def list(self, project):
return project.get_bills().all() return project.get_bills().all()
def add(self, project): def post(self, project):
form = get_billform_for(project, True, meta={'csrf': False}) form = get_billform_for(project, True, meta={'csrf': False})
if form.validate(): if form.validate():
bill = Bill() bill = Bill()
form.save(bill, project) form.save(bill, project)
db.session.add(bill) db.session.add(bill)
db.session.commit() db.session.commit()
return 201, bill.id return bill.id, 201
return 400, form.errors return form.errors, 400
def update(self, project, bill_id):
class BillHandler(Resource):
method_decorators = [need_auth]
def get(self, project, bill_id):
bill = Bill.query.get(project, bill_id)
if not bill:
return "Not Found", 404
return bill, 200
def put(self, project, bill_id):
form = get_billform_for(project, True, meta={'csrf': False}) form = get_billform_for(project, True, meta={'csrf': False})
if form.validate(): if form.validate():
bill = Bill.query.get(project, bill_id) bill = Bill.query.get(project, bill_id)
form.save(bill, project) form.save(bill, project)
db.session.commit() db.session.commit()
return 200, bill.id return bill.id, 200
return 400, form.errors return form.errors, 400
def delete(self, project, bill_id): def delete(self, project, bill_id):
bill = Bill.query.delete(project, bill_id) bill = Bill.query.delete(project, bill_id)
db.session.commit() db.session.commit()
if not bill: if not bill:
return 404, "Not Found" return "Not Found", 404
return 200, "OK" return "OK", 200
project_resource = RESTResource( restful_api.add_resource(ProjectsHandler, '/projects')
name="project", restful_api.add_resource(ProjectHandler, '/projects/<string:project_id>')
route="/projects", restful_api.add_resource(MembersHandler, "/projects/<string:project_id>/members")
app=api, restful_api.add_resource(ProjectStatsHandler, "/projects/<string:project_id>/statistics")
actions=["add", "update", "delete", "get"], restful_api.add_resource(MemberHandler, "/projects/<string:project_id>/members/<int:member_id>")
handler=ProjectHandler()) restful_api.add_resource(BillsHandler, "/projects/<string:project_id>/bills")
restful_api.add_resource(BillHandler, "/projects/<string:project_id>/bills/<int:bill_id>")
member_resource = RESTResource(
name="member",
inject_name="project",
route="/projects/<project_id>/members",
app=api,
handler=MemberHandler(),
authentifier=check_project)
bill_resource = RESTResource(
name="bill",
inject_name="project",
route="/projects/<project_id>/bills",
app=api,
handler=BillHandler(),
authentifier=check_project)

View file

@ -1,111 +1,127 @@
# Translations template for PROJECT. # Translations template for PROJECT.
# Copyright (C) 2013 ORGANIZATION # Copyright (C) 2018 ORGANIZATION
# This file is distributed under the same license as the PROJECT project. # This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2013. # FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
# #
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2013-10-13 21:32+0200\n" "POT-Creation-Date: 2018-05-15 21:43+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n" "Generated-By: Babel 2.5.3\n"
#: forms.py:22 #: forms.py:46
msgid "Select all"
msgstr ""
#: forms.py:22
msgid "Select none"
msgstr ""
#: forms.py:61
msgid "Project name" msgid "Project name"
msgstr "" msgstr ""
#: forms.py:62 forms.py:86 forms.py:102 #: forms.py:47 forms.py:71 forms.py:88
msgid "Private code" msgid "Private code"
msgstr "" msgstr ""
#: forms.py:63 #: forms.py:48
msgid "Email" msgid "Email"
msgstr "" msgstr ""
#: forms.py:85 forms.py:101 forms.py:107 #: forms.py:70 forms.py:87 forms.py:98
msgid "Project identifier" msgid "Project identifier"
msgstr "" msgstr ""
#: forms.py:87 templates/send_invites.html:5 #: forms.py:72
msgid "Create the project" msgid "Create the project"
msgstr "" msgstr ""
#: forms.py:92 #: forms.py:77
msgid "" msgid ""
"The project identifier is used to log in and for the URL of the project. " "The project identifier is used to log in and for the URL of the project. "
"We tried to generate an identifier for you but a project with this " "We tried to generate an identifier for you but a project with this "
"identifier already exists. Please create a new identifier that you will " "identifier already exists. Please create a new identifier that you will "
"be able to remember." "be able to remember"
msgstr "" msgstr ""
#: forms.py:103 #: forms.py:89 forms.py:94
msgid "Get in" msgid "Get in"
msgstr "" msgstr ""
#: forms.py:108 #: forms.py:93
msgid "Admin password"
msgstr ""
#: forms.py:99
msgid "Send me the code by email" msgid "Send me the code by email"
msgstr "" msgstr ""
#: forms.py:112 #: forms.py:103
msgid "This project does not exists" msgid "This project does not exists"
msgstr "" msgstr ""
#: forms.py:116 #: forms.py:108
msgid "Password mismatch"
msgstr ""
#: forms.py:109
msgid "Password"
msgstr ""
#: forms.py:110
msgid "Password confirmation"
msgstr ""
#: forms.py:111
msgid "Reset password"
msgstr ""
#: forms.py:115
msgid "Date" msgid "Date"
msgstr "" msgstr ""
#: forms.py:117 #: forms.py:116
msgid "What?" msgid "What?"
msgstr "" msgstr ""
#: forms.py:118 #: forms.py:117
msgid "Payer" msgid "Payer"
msgstr "" msgstr ""
#: forms.py:119 #: forms.py:118
msgid "Amount paid" msgid "Amount paid"
msgstr "" msgstr ""
#: forms.py:120 templates/list_bills.html:103 #: forms.py:119 templates/forms.html:100 templates/list_bills.html:101
msgid "For whom?" msgid "For whom?"
msgstr "" msgstr ""
#: forms.py:122 #: forms.py:121
msgid "Submit" msgid "Submit"
msgstr "" msgstr ""
#: forms.py:123 #: forms.py:122
msgid "Submit and add a new one" msgid "Submit and add a new one"
msgstr "" msgstr ""
#: forms.py:149 #: forms.py:146
msgid "Bills can't be null" msgid "Bills can't be null"
msgstr "" msgstr ""
#: forms.py:154 #: forms.py:151
msgid "Name" msgid "Name"
msgstr "" msgstr ""
#: forms.py:155 templates/forms.html:95 #: forms.py:152
msgid "Weight"
msgstr ""
#: forms.py:153 templates/forms.html:123
msgid "Add" msgid "Add"
msgstr "" msgstr ""
#: forms.py:163 #: forms.py:162
msgid "User name incorrect" msgid "User name incorrect"
msgstr "" msgstr ""
@ -113,105 +129,151 @@ msgstr ""
msgid "This project already have this member" msgid "This project already have this member"
msgstr "" msgstr ""
#: forms.py:178 #: forms.py:183
msgid "People to notify" msgid "People to notify"
msgstr "" msgstr ""
#: forms.py:179 #: forms.py:184
msgid "Send invites" msgid "Send invites"
msgstr "" msgstr ""
#: forms.py:185 #: forms.py:190
#, python-format #, python-format
msgid "The email %(email)s is not valid" msgid "The email %(email)s is not valid"
msgstr "" msgstr ""
#: forms.py:191 #: forms.py:196
msgid "Start date" msgid "What do you want to download ?"
msgstr "" msgstr ""
#: forms.py:192 #: forms.py:199
msgid "End date" msgid "bills"
msgstr "" msgstr ""
#: web.py:95 #: forms.py:199
msgid "transactions"
msgstr ""
#: forms.py:201
msgid "Export file format"
msgstr ""
#: web.py:129
msgid "Too many failed login attempts, please retry later."
msgstr ""
#: web.py:144
#, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left."
msgstr ""
#: web.py:167
msgid "You either provided a bad token or no project identifier."
msgstr ""
#: web.py:195
msgid "This private code is not the right one" msgid "This private code is not the right one"
msgstr "" msgstr ""
#: web.py:147 #: web.py:242
#, python-format #, python-format
msgid "You have just created '%(project)s' to share your expenses" msgid "You have just created '%(project)s' to share your expenses"
msgstr "" msgstr ""
#: web.py:165 #: web.py:260
#, python-format #, python-format
msgid "%(msg_compl)sThe project identifier is %(project)s" msgid "%(msg_compl)sThe project identifier is %(project)s"
msgstr "" msgstr ""
#: web.py:185 #: web.py:281
msgid "a mail has been sent to you with the password" msgid "A link to reset your password has been sent to your email."
msgstr "" msgstr ""
#: web.py:211 #: web.py:291
msgid "No token provided"
msgstr ""
#: web.py:294
msgid "Invalid token"
msgstr ""
#: web.py:297
msgid "Unknown project"
msgstr ""
#: web.py:303
msgid "Password successfully reset."
msgstr ""
#: web.py:351
msgid "Project successfully deleted" msgid "Project successfully deleted"
msgstr "" msgstr ""
#: web.py:254 #: web.py:401
#, python-format #, python-format
msgid "You have been invited to share your expenses for %(project)s" msgid "You have been invited to share your expenses for %(project)s"
msgstr "" msgstr ""
#: web.py:261 #: web.py:408
msgid "Your invitations have been sent" msgid "Your invitations have been sent"
msgstr "" msgstr ""
#: web.py:290 #: web.py:439
#, python-format #, python-format
msgid "%(member)s had been added" msgid "%(member)s had been added"
msgstr "" msgstr ""
#: web.py:303 #: web.py:452
#, python-format #, python-format
msgid "%(name)s is part of this project again" msgid "%(name)s is part of this project again"
msgstr "" msgstr ""
#: web.py:312 #: web.py:461
#, python-format #, python-format
msgid "User '%(name)s' has been deactivated" msgid ""
"User '%(name)s' has been deactivated. It will still appear in the users "
"list until its balance becomes zero."
msgstr "" msgstr ""
#: web.py:314 #: web.py:465
#, python-format #, python-format
msgid "User '%(name)s' has been removed" msgid "User '%(name)s' has been removed"
msgstr "" msgstr ""
#: web.py:331 #: web.py:480
#, python-format
msgid "User '%(name)s' has been edited"
msgstr ""
#: web.py:500
msgid "The bill has been added" msgid "The bill has been added"
msgstr "" msgstr ""
#: web.py:351 #: web.py:520
msgid "The bill has been deleted" msgid "The bill has been deleted"
msgstr "" msgstr ""
#: web.py:369 #: web.py:538
msgid "The bill has been modified" msgid "The bill has been modified"
msgstr "" msgstr ""
#: templates/add_bill.html:9 #: templates/add_bill.html:9 templates/edit_member.html:9
msgid "Back to the list" msgid "Back to the list"
msgstr "" msgstr ""
#: templates/authenticate.html:6 #: templates/admin.html:10
msgid "" msgid "Administration tasks are currently disabled."
"The project you are trying to access do not exist, do you want \n"
"to"
msgstr "" msgstr ""
#: templates/authenticate.html:7 #: templates/authenticate.html:6
msgid "The project you are trying to access do not exist, do you want to"
msgstr ""
#: templates/authenticate.html:8
msgid "create it" msgid "create it"
msgstr "" msgstr ""
#: templates/authenticate.html:7 #: templates/authenticate.html:8
msgid "?" msgid "?"
msgstr "" msgstr ""
@ -239,6 +301,24 @@ msgstr ""
msgid "Oldest bill" msgid "Oldest bill"
msgstr "" msgstr ""
#: templates/dashboard.html:5 templates/list_bills.html:101
msgid "Actions"
msgstr ""
#: templates/dashboard.html:17 templates/list_bills.html:65
#: templates/list_bills.html:111
msgid "edit"
msgstr ""
#: templates/dashboard.html:18 templates/forms.html:83
#: templates/list_bills.html:112
msgid "delete"
msgstr ""
#: templates/dashboard.html:25
msgid "The Dashboard is currently deactivated."
msgstr ""
#: templates/edit_project.html:6 templates/list_bills.html:24 #: templates/edit_project.html:6 templates/list_bills.html:24
msgid "you sure?" msgid "you sure?"
msgstr "" msgstr ""
@ -247,44 +327,59 @@ msgstr ""
msgid "Edit this project" msgid "Edit this project"
msgstr "" msgstr ""
#: templates/forms.html:23 #: templates/edit_project.html:15
msgid "Download this project's data"
msgstr ""
#: templates/forms.html:27
msgid "Can't remember the password?" msgid "Can't remember the password?"
msgstr "" msgstr ""
#: templates/forms.html:26 #: templates/forms.html:30
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
#: templates/forms.html:68 #: templates/forms.html:82
msgid "Edit the project" msgid "Edit the project"
msgstr "" msgstr ""
#: templates/forms.html:69 templates/list_bills.html:70 #: templates/forms.html:91
#: templates/list_bills.html:114
msgid "delete"
msgstr ""
#: templates/forms.html:77
msgid "Edit this bill" msgid "Edit this bill"
msgstr "" msgstr ""
#: templates/forms.html:77 templates/list_bills.html:94 #: templates/forms.html:91 templates/list_bills.html:89
msgid "Add a bill" msgid "Add a bill"
msgstr "" msgstr ""
#: templates/forms.html:95 #: templates/forms.html:103
msgid "Type user name here" msgid "Select all"
msgstr ""
#: templates/forms.html:102
msgid "Send the invitations"
msgstr "" msgstr ""
#: templates/forms.html:103 #: templates/forms.html:103
msgid "Select none"
msgstr ""
#: templates/forms.html:122
msgid "Type user name here"
msgstr ""
#: templates/forms.html:129
msgid "Edit this member"
msgstr ""
#: templates/forms.html:145
msgid "Send the invitations"
msgstr ""
#: templates/forms.html:146
msgid "No, thanks" msgid "No, thanks"
msgstr "" msgstr ""
#: templates/home.html:8 #: templates/forms.html:157
msgid "Download"
msgstr ""
#: templates/home.html:7
msgid "Manage your shared <br>expenses, easily" msgid "Manage your shared <br>expenses, easily"
msgstr "" msgstr ""
@ -292,39 +387,39 @@ msgstr ""
msgid "Try out the demo" msgid "Try out the demo"
msgstr "" msgstr ""
#: templates/home.html:12 #: templates/home.html:13
msgid "You're sharing a house?" msgid "You're sharing a house?"
msgstr "" msgstr ""
#: templates/home.html:12 #: templates/home.html:13
msgid "Going on holidays with friends?" msgid "Going on holidays with friends?"
msgstr "" msgstr ""
#: templates/home.html:12 #: templates/home.html:13
msgid "Simply sharing money with others?" msgid "Simply sharing money with others?"
msgstr "" msgstr ""
#: templates/home.html:12 #: templates/home.html:13
msgid "We can help!" msgid "We can help!"
msgstr "" msgstr ""
#: templates/home.html:24 #: templates/home.html:21
msgid "Log to an existing project" msgid "Log to an existing project"
msgstr "" msgstr ""
#: templates/home.html:28 #: templates/home.html:25
msgid "log in" msgid "log in"
msgstr "" msgstr ""
#: templates/home.html:29 #: templates/home.html:26
msgid "can't remember your password?" msgid "can't remember your password?"
msgstr "" msgstr ""
#: templates/home.html:36 #: templates/home.html:34 templates/home.html:42
msgid "or create a new one" msgid "or create a new one"
msgstr "" msgstr ""
#: templates/home.html:40 #: templates/home.html:38
msgid "let's get started" msgid "let's get started"
msgstr "" msgstr ""
@ -338,91 +433,91 @@ msgstr ""
msgid "Account manager" msgid "Account manager"
msgstr "" msgstr ""
#: templates/layout.html:45 templates/settle_bills.html:4 #: templates/layout.html:39
msgid "Bills" msgid "Bills"
msgstr "" msgstr ""
#: templates/layout.html:46 templates/settle_bills.html:5 #: templates/layout.html:40
msgid "Settle" msgid "Settle"
msgstr "" msgstr ""
#: templates/layout.html:53 #: templates/layout.html:41
msgid "Statistics"
msgstr ""
#: templates/layout.html:48
msgid "options" msgid "options"
msgstr "" msgstr ""
#: templates/layout.html:55 #: templates/layout.html:50
msgid "Project settings" msgid "Project settings"
msgstr "" msgstr ""
#: templates/layout.html:59 #: templates/layout.html:54
msgid "switch to" msgid "switch to"
msgstr "" msgstr ""
#: templates/layout.html:62 #: templates/layout.html:57
msgid "Start a new project" msgid "Start a new project"
msgstr "" msgstr ""
#: templates/layout.html:64 #: templates/layout.html:59
msgid "Logout" msgid "Logout"
msgstr "" msgstr ""
#: templates/layout.html:92 #: templates/layout.html:66
msgid "Dashboard"
msgstr ""
#: templates/layout.html:89
msgid "This is a free software" msgid "This is a free software"
msgstr "" msgstr ""
#: templates/layout.html:92 #: templates/layout.html:89
msgid "you can contribute and improve it!" msgid "you can contribute and improve it!"
msgstr "" msgstr ""
#: templates/list_bills.html:74 #: templates/list_bills.html:63
msgid "deactivate"
msgstr ""
#: templates/list_bills.html:70
msgid "reactivate" msgid "reactivate"
msgstr "" msgstr ""
#: templates/list_bills.html:88 #: templates/list_bills.html:82
msgid "The project identifier is" msgid "Invite people to join this project!"
msgstr "" msgstr ""
#: templates/list_bills.html:88 #: templates/list_bills.html:83
msgid "remember it!"
msgstr ""
#: templates/list_bills.html:89
msgid "Add a new bill" msgid "Add a new bill"
msgstr "" msgstr ""
#: templates/list_bills.html:103 #: templates/list_bills.html:101
msgid "When?" msgid "When?"
msgstr "" msgstr ""
#: templates/list_bills.html:103 #: templates/list_bills.html:101
msgid "Who paid?" msgid "Who paid?"
msgstr "" msgstr ""
#: templates/list_bills.html:103 #: templates/list_bills.html:101
msgid "For what?" msgid "For what?"
msgstr "" msgstr ""
#: templates/list_bills.html:103 templates/settle_bills.html:31 #: templates/list_bills.html:101 templates/settle_bills.html:22
msgid "How much?" msgid "How much?"
msgstr "" msgstr ""
#: templates/list_bills.html:103 #: templates/list_bills.html:109
msgid "Actions"
msgstr ""
#: templates/list_bills.html:111
msgid "each" msgid "each"
msgstr "" msgstr ""
#: templates/list_bills.html:113 #: templates/list_bills.html:120
msgid "edit"
msgstr ""
#: templates/list_bills.html:122
msgid "Nothing to list yet. You probably want to" msgid "Nothing to list yet. You probably want to"
msgstr "" msgstr ""
#: templates/list_bills.html:122 #: templates/list_bills.html:120
msgid "add a bill" msgid "add a bill"
msgstr "" msgstr ""
@ -434,43 +529,50 @@ msgstr ""
msgid "Your projects" msgid "Your projects"
msgstr "" msgstr ""
#: templates/send_invites.html:6 #: templates/reset_password.html:7
msgid "Invite people" msgid "Reset your password"
msgstr "" msgstr ""
#: templates/send_invites.html:7 #: templates/send_invites.html:4
msgid "Use it!"
msgstr ""
#: templates/send_invites.html:11
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "" msgstr ""
#: templates/send_invites.html:12 #: templates/send_invites.html:5
msgid "" msgid ""
"Specify a (coma separated) list of email adresses you want to notify " "Specify a (comma separated) list of email adresses you want to notify "
"about the\n" "about the\n"
"creation of this budget management project and we will send them an email" "creation of this budget management project and we will send them an email"
" for you." " for you."
msgstr "" msgstr ""
#: templates/send_invites.html:14 #: templates/send_invites.html:7
msgid "If you prefer, you can" msgid ""
"If you prefer, you can share the project identifier and the shared\n"
"password by other communication means. Or even directly share the "
"following link:"
msgstr "" msgstr ""
#: templates/send_invites.html:14 #: templates/settle_bills.html:22
msgid "skip this step"
msgstr ""
#: templates/send_invites.html:14
msgid "and notify them yourself"
msgstr ""
#: templates/settle_bills.html:31
msgid "Who pays?" msgid "Who pays?"
msgstr "" msgstr ""
#: templates/settle_bills.html:31 #: templates/settle_bills.html:22
msgid "To whom?" msgid "To whom?"
msgstr "" msgstr ""
#: templates/statistics.html:21
msgid "Who?"
msgstr ""
#: templates/statistics.html:21
msgid "Paid"
msgstr ""
#: templates/statistics.html:21
msgid "Spent"
msgstr ""
#: templates/statistics.html:21
msgid "Balance"
msgstr ""

View file

@ -52,6 +52,26 @@ class Project(db.Model):
return balances 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 @property
def uses_weights(self): def uses_weights(self):
return len([i for i in self.members if i.weight != 1]) > 0 return len([i for i in self.members if i.weight != 1]) > 0

View file

@ -11,7 +11,7 @@ from werkzeug.contrib.fixers import ProxyFix
from ihatemoney.api import api from ihatemoney.api import api
from ihatemoney.models import db from ihatemoney.models import db
from ihatemoney.utils import PrefixedWSGI, minimal_round from ihatemoney.utils import PrefixedWSGI, minimal_round, IhmJSONEncoder
from ihatemoney.web import main as web_interface from ihatemoney.web import main as web_interface
from ihatemoney import default_settings from ihatemoney import default_settings
@ -68,6 +68,8 @@ def load_configuration(app, configuration=None):
app.config.from_pyfile(env_var_config) app.config.from_pyfile(env_var_config)
else: else:
app.config.from_pyfile('ihatemoney.cfg', silent=True) app.config.from_pyfile('ihatemoney.cfg', silent=True)
# Configure custom JSONEncoder used by the API
app.config['RESTFUL_JSON'] = {'cls': IhmJSONEncoder}
def validate_configuration(app): def validate_configuration(app):

View file

@ -74,11 +74,13 @@ body {
background-repeat: no-repeat; background-repeat: no-repeat;
height: 100%; height: 100%;
color: black; color: black;
overflow-y: auto;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.sidebar { .sidebar {
position: fixed; position: fixed;
padding-bottom: 4.5rem;
} }
} }

View file

@ -4,7 +4,7 @@
<table id="bill_table" class="table table-striped"> <table id="bill_table" class="table table-striped">
<thead><tr><th>{{ _("Project") }}</th><th>{{ _("Number of members") }}</th><th>{{ _("Number of bills") }}</th><th>{{_("Newest bill")}}</th><th>{{_("Oldest bill")}}</th><th>{{_("Actions")}}</th></tr></thead> <thead><tr><th>{{ _("Project") }}</th><th>{{ _("Number of members") }}</th><th>{{ _("Number of bills") }}</th><th>{{_("Newest bill")}}</th><th>{{_("Oldest bill")}}</th><th>{{_("Actions")}}</th></tr></thead>
<tbody>{% for project in projects|sort(attribute='name') %} <tbody>{% for project in projects|sort(attribute='name') %}
<tr class="{{ loop.cycle("odd", "even") }}"> <tr>
<td>{{ project.name }}</td><td>{{ project.members | count }}</td><td>{{ project.get_bills().count() }}</td> <td>{{ project.name }}</td><td>{{ project.members | count }}</td><td>{{ project.get_bills().count() }}</td>
{% if project.has_bills() %} {% if project.has_bills() %}
<td>{{ project.get_bills().all()[0].date }}</td> <td>{{ project.get_bills().all()[0].date }}</td>

View file

@ -22,7 +22,7 @@
<thead><tr><th>{{ _("Who pays?") }}</th><th>{{ _("To whom?") }}</th><th>{{ _("How much?") }}</th></tr></thead> <thead><tr><th>{{ _("Who pays?") }}</th><th>{{ _("To whom?") }}</th><th>{{ _("How much?") }}</th></tr></thead>
<tbody> <tbody>
{% for bill in bills %} {% for bill in bills %}
<tr class="{{ loop.cycle("odd", "even") }}" receiver={{bill.receiver.id}}> <tr receiver={{bill.receiver.id}}>
<td>{{ bill.ower }}</td> <td>{{ bill.ower }}</td>
<td>{{ bill.receiver }}</td> <td>{{ bill.receiver }}</td>
<td>{{ "%0.2f"|format(bill.amount) }}</td> <td>{{ "%0.2f"|format(bill.amount) }}</td>

View file

@ -3,12 +3,11 @@
{% block sidebar %} {% block sidebar %}
<div id="table_overflow"> <div id="table_overflow">
<table class="balance table"> <table class="balance table">
{% set balance = g.project.balance %} {% for stat in members_stats| sort(attribute='member.name') %}
{% for member in g.project.members | sort(attribute='name') if member.activated or balance[member.id]|round(2) != 0 %} <tr>
<tr id="bal-member-{{ member.id }}" action={% if member.activated %}delete{% else %}reactivate{% endif %}> <td class="balance-name">{{ stat.member.name }}</td>
<td class="balance-name">{{ member.name }}</td> <td class="balance-value {% if stat.balance|round(2) > 0 %}positive{% elif stat.balance|round(2) < 0 %}negative{% endif %}">
<td class="balance-value {% if balance[member.id]|round(2) > 0 %}positive{% elif balance[member.id]|round(2) < 0 %}negative{% endif %}"> {% if stat.balance|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(stat.balance) }}
{% if balance[member.id]|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -21,12 +20,12 @@
<table id="bill_table" class="split_bills table table-striped"> <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> <thead><tr><th>{{ _("Who?") }}</th><th>{{ _("Paid") }}</th><th>{{ _("Spent") }}</th><th>{{ _("Balance") }}</th></tr></thead>
<tbody> <tbody>
{% for member in members %} {% for stat in members_stats %}
<tr class="{{ loop.cycle("odd", "even") }}"> <tr>
<td>{{ member.name }}</td> <td>{{ stat.member.name }}</td>
<td>{{ "%0.2f"|format(paid[member.id]) }}</td> <td>{{ "%0.2f"|format(stat.paid) }}</td>
<td>{{ "%0.2f"|format(spent[member.id]) }}</td> <td>{{ "%0.2f"|format(stat.spent) }}</td>
<td>{{ "%0.2f"|format(balance[member.id]) }}</td> <td>{{ "%0.2f"|format(stat.balance) }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View file

@ -1053,7 +1053,7 @@ class APITestCase(IhatemoneyTestCase):
}) })
self.assertTrue(400, resp.status_code) self.assertTrue(400, resp.status_code)
self.assertEqual('{"contact_email": ["Invalid email address."]}', self.assertEqual('{"contact_email": ["Invalid email address."]}\n',
resp.data.decode('utf-8')) resp.data.decode('utf-8'))
# create it # create it
@ -1139,7 +1139,7 @@ class APITestCase(IhatemoneyTestCase):
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual('[]', req.data.decode('utf-8')) self.assertEqual('[]\n', req.data.decode('utf-8'))
# add a member # add a member
req = self.client.post("/api/projects/raclette/members", data={ req = self.client.post("/api/projects/raclette/members", data={
@ -1148,7 +1148,7 @@ class APITestCase(IhatemoneyTestCase):
# the id of the new member should be returned # the id of the new member should be returned
self.assertStatus(201, req) self.assertStatus(201, req)
self.assertEqual("1", req.data.decode('utf-8')) self.assertEqual("1\n", req.data.decode('utf-8'))
# the list of members should contain one member # the list of members should contain one member
req = self.client.get("/api/projects/raclette/members", req = self.client.get("/api/projects/raclette/members",
@ -1223,7 +1223,7 @@ class APITestCase(IhatemoneyTestCase):
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual('[]', req.data.decode('utf-8')) self.assertEqual('[]\n', req.data.decode('utf-8'))
def test_bills(self): def test_bills(self):
# create a project # create a project
@ -1239,7 +1239,7 @@ class APITestCase(IhatemoneyTestCase):
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual("[]", req.data.decode('utf-8')) self.assertEqual("[]\n", req.data.decode('utf-8'))
# add a bill # add a bill
req = self.client.post("/api/projects/raclette/bills", data={ req = self.client.post("/api/projects/raclette/bills", data={
@ -1252,7 +1252,7 @@ class APITestCase(IhatemoneyTestCase):
# should return the id # should return the id
self.assertStatus(201, req) self.assertStatus(201, req)
self.assertEqual(req.data.decode('utf-8'), "1") self.assertEqual(req.data.decode('utf-8'), "1\n")
# get this bill details # get this bill details
req = self.client.get("/api/projects/raclette/bills/1", req = self.client.get("/api/projects/raclette/bills/1",
@ -1288,7 +1288,7 @@ class APITestCase(IhatemoneyTestCase):
}, headers=self.get_auth("raclette")) }, headers=self.get_auth("raclette"))
self.assertStatus(400, req) self.assertStatus(400, req)
self.assertEqual('{"date": ["This field is required."]}', req.data.decode('utf-8')) self.assertEqual('{"date": ["This field is required."]}\n', req.data.decode('utf-8'))
# edit a bill # edit a bill
req = self.client.put("/api/projects/raclette/bills/1", data={ req = self.client.put("/api/projects/raclette/bills/1", data={
@ -1325,6 +1325,40 @@ class APITestCase(IhatemoneyTestCase):
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(404, req) 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): def test_username_xss(self):
# create a project # create a project
# self.api_create("raclette") # self.api_create("raclette")

View file

@ -2,293 +2,289 @@
# Copyright (C) 2011 ORGANIZATION # Copyright (C) 2011 ORGANIZATION
# This file is distributed under the same license as the PROJECT project. # This file is distributed under the same license as the PROJECT project.
# Alexis Métaireau <alexis@notmyidea.org>, 2011. # Alexis Métaireau <alexis@notmyidea.org>, 2011.
# # Adrien CLERC, 2018.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2013-10-13 21:32+0200\n" "POT-Creation-Date: 2018-05-15 21:43+0200\n"
"PO-Revision-Date: 2011-10-14 23:51+0200\n" "PO-Revision-Date: 2018-05-15 22:00+0200\n"
"Last-Translator: Quentin Roy <royque@gmail.com>\n" "Last-Translator: Adrien CLERC <>\n"
"Language-Team: fr <LL@li.org>\n" "Language-Team: fr <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n > 1)\n" "Language: fr\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Virtaal 0.7.1\n"
"Generated-By: Babel 2.5.3\n"
#: forms.py:22 #: forms.py:46
msgid "Select all"
msgstr "Tout cocher"
#: forms.py:22
msgid "Select none"
msgstr "Tout décocher"
#: forms.py:61
msgid "Project name" msgid "Project name"
msgstr "Nom de projet" msgstr "Nom de projet"
#: forms.py:62 forms.py:86 forms.py:102 #: forms.py:47 forms.py:71 forms.py:88
msgid "Private code" msgid "Private code"
msgstr "Code d'accès" msgstr "Code daccès"
#: forms.py:63 #: forms.py:48
msgid "Email" msgid "Email"
msgstr "Email" msgstr "Email"
#: forms.py:85 forms.py:101 forms.py:107 #: forms.py:70 forms.py:87 forms.py:98
msgid "Project identifier" msgid "Project identifier"
msgstr "Identifiant du projet" msgstr "Identifiant du projet"
#: forms.py:87 #: forms.py:72
msgid "Admin password"
msgstr "Mot de passe administrateur"
#: forms.py:87 templates/send_invites.html:5
msgid "Create the project" msgid "Create the project"
msgstr "Créer le projet" msgstr "Créer le projet"
#: forms.py:92 #: forms.py:77
msgid "" msgid ""
"The project identifier is used to log in and for the URL of the project. " "The project identifier is used to log in and for the URL of the project. "
"We tried to generate an identifier for you but a project with this " "We tried to generate an identifier for you but a project with this "
"identifier already exists. Please create a new identifier that you will " "identifier already exists. Please create a new identifier that you will "
"be able to remember." "be able to remember"
msgstr "" msgstr ""
"L'identifiant du projet est utilisé pour se connecter." "Lidentifiant du projet est utilisé pour se connecter et pour lURL du "
"Nous avons essayé de générer un identifiant mais " "projet. Nous avons essayé de générer un identifiant mais celui ci existe "
"celui ci existe déjà. Merci de créer un nouvel identifiant que vous serez" "déjà. Merci de créer un nouvel identifiant que vous serez capable de retenir"
" capable de retenir"
#: forms.py:103 #: forms.py:89 forms.py:94
msgid "Get in" msgid "Get in"
msgstr "Entrer" msgstr "Entrer"
#: forms.py:107 #: forms.py:93
msgid "Password mismatch" msgid "Admin password"
msgstr "Les mots de passe fournis ne sont pas les mêmes." msgstr "Mot de passe administrateur"
#: forms.py:109 #: forms.py:99
msgid "Password confirmation"
msgstr "Confirmation du mot de passe"
#: forms.py:107
msgid "Password"
msgstr "Mot de passe"
#: forms.py:108
msgid "Send me the code by email" msgid "Send me the code by email"
msgstr "Envoyez moi le code par email" msgstr "Envoyez moi le code par email"
#: forms.py:112 #: forms.py:103
msgid "This project does not exists" msgid "This project does not exists"
msgstr "Ce projet n'existe pas" msgstr "Ce projet nexiste pas"
#: forms.py:116 #: forms.py:108
msgid "Password mismatch"
msgstr "Les mots de passe sont différents"
#: forms.py:109
msgid "Password"
msgstr "Mot de passe"
#: forms.py:110
msgid "Password confirmation"
msgstr "Confirmation du mot de passe"
#: forms.py:111
msgid "Reset password"
msgstr "Réinitialiser le mot de passe"
#: forms.py:115
msgid "Date" msgid "Date"
msgstr "Date" msgstr "Date"
#: forms.py:117 #: forms.py:116
msgid "What?" msgid "What?"
msgstr "Quoi ?" msgstr "Quoi ?"
#: forms.py:118 #: forms.py:117
msgid "Payer" msgid "Payer"
msgstr "Payeur" msgstr "Payeur"
#: forms.py:119 #: forms.py:118
msgid "Amount paid" msgid "Amount paid"
msgstr "Montant" msgstr "Montant"
#: forms.py:120 templates/list_bills.html:103 #: forms.py:119 templates/forms.html:100 templates/list_bills.html:101
msgid "For whom?" msgid "For whom?"
msgstr "Pour qui ?" msgstr "Pour qui ?"
#: forms.py:122 #: forms.py:121
msgid "Submit" msgid "Submit"
msgstr "Valider" msgstr "Valider"
#: forms.py:123 #: forms.py:122
msgid "Submit and add a new one" msgid "Submit and add a new one"
msgstr "Valider et ajouter une autre facture" msgstr "Valider et ajouter une autre facture"
#: forms.py:149 #: forms.py:146
msgid "Bills can't be null" msgid "Bills can't be null"
msgstr "Le montant d'une facture ne peut pas être nul." msgstr "Le montant dune facture ne peut pas être nul"
#: forms.py:154 #: forms.py:151
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
#: forms.py:155 #: forms.py:152
msgid "Weight" msgid "Weight"
msgstr "Parts" msgstr "Parts"
#: forms.py:155 templates/forms.html:95 #: forms.py:153 templates/forms.html:123
msgid "Add" msgid "Add"
msgstr "Ajouter" msgstr "Ajouter"
#: forms.py:163 #: forms.py:162
msgid "User name incorrect" msgid "User name incorrect"
msgstr "Nom d'utilisateur incorrect" msgstr "Nom dutilisateur incorrect"
#: forms.py:167 #: forms.py:167
msgid "This project already have this member" msgid "This project already have this member"
msgstr "Ce membre existe déjà pour ce projet" msgstr "Ce membre existe déjà pour ce projet"
#: forms.py:178 #: forms.py:183
msgid "People to notify" msgid "People to notify"
msgstr "Personnes à prévenir" msgstr "Personnes à prévenir"
#: forms.py:179 #: forms.py:184
msgid "Send invites" msgid "Send invites"
msgstr "Envoyer les invitations" msgstr "Envoyer les invitations"
#: forms.py:185 #: forms.py:190
#, python-format #, python-format
msgid "The email %(email)s is not valid" msgid "The email %(email)s is not valid"
msgstr "L'email %(email)s est invalide" msgstr "Lemail %(email)s est invalide"
#: forms.py:191 #: forms.py:196
msgid "Start date"
msgstr "Date de départ"
#: forms.py:192
msgid "End date"
msgstr "Date de fin"
#: forms.py:202
msgid "What do you want to download ?" msgid "What do you want to download ?"
msgstr "Que voulez-vous télécharger ?" msgstr "Que voulez-vous télécharger ?"
#: forms.py:205 #: forms.py:199
msgid "bills" msgid "bills"
msgstr "factures" msgstr "factures"
#: forms.py:205 #: forms.py:199
msgid "transactions" msgid "transactions"
msgstr "remboursements" msgstr "remboursements"
#: forms.py:206 #: forms.py:201
msgid "Export file format" msgid "Export file format"
msgstr "Format du fichier d'export" msgstr "Format du fichier dexport"
#: web.py:95 #: web.py:129
msgid "You either provided a bad token or no project identifier."
msgstr "L'identifiant du projet ou le token fourni n'est pas correct."
#: web.py:95
msgid "This private code is not the right one"
msgstr "Le code que vous avez entré n'est pas correct"
#: web.py:106
msgid "This admin password is not the right one. Only %(num)d attempts left."
msgstr "Le mot de passe administrateur que vous avez entré n'est pas correct. Plus que %(num)d tentatives."
#: web.py:106
msgid "Too many failed login attempts, please retry later." msgid "Too many failed login attempts, please retry later."
msgstr "Trop d'échecs d'authentification successifs, veuillez réessayer plus tard." msgstr "Trop d'échecs dauthentification successifs, veuillez réessayer plus tard."
#: web.py:147 #: web.py:144
#, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left."
msgstr ""
"Le mot de passe administrateur que vous avez entré nest pas correct. "
"Plus que %(num)d tentatives."
#: web.py:167
msgid "You either provided a bad token or no project identifier."
msgstr "Lidentifiant du projet ou le token fourni nest pas correct."
#: web.py:195
msgid "This private code is not the right one"
msgstr "Le code que vous avez entré nest pas correct"
#: web.py:242
#, python-format #, python-format
msgid "You have just created '%(project)s' to share your expenses" msgid "You have just created '%(project)s' to share your expenses"
msgstr "Vous venez de créer '%(project)s' pour partager vos dépenses" msgstr "Vous venez de créer « %(project)s » pour partager vos dépenses"
#: web.py:165 #: web.py:260
#, python-format #, python-format
msgid "%(msg_compl)sThe project identifier is %(project)s" msgid "%(msg_compl)sThe project identifier is %(project)s"
msgstr "L'identifiant de ce projet est '%(project)s'" msgstr "%(msg_compl)sLidentifiant de ce projet est %(project)s"
#: web.py:185 #: web.py:281
msgid "A link to reset your password has been sent to your email." msgid "A link to reset your password has been sent to your email."
msgstr "Un lien pour changer votre mot de passe vous a été envoyé par mail." msgstr "Un lien pour changer votre mot de passe vous a été envoyé par mail."
#: web.py:211 #: web.py:291
msgid "No token provided"
msgstr "Aucun token na été fourni"
#: web.py:294
msgid "Invalid token"
msgstr "Token invalide"
#: web.py:297
msgid "Unknown project"
msgstr "Project inconnu"
#: web.py:303
msgid "Password successfully reset."
msgstr "Le mot de passe a été changé avec succès."
#: web.py:351
msgid "Project successfully deleted" msgid "Project successfully deleted"
msgstr "Projet supprimé" msgstr "Projet supprimé"
#: web.py:254 #: web.py:401
#, python-format #, python-format
msgid "You have been invited to share your expenses for %(project)s" msgid "You have been invited to share your expenses for %(project)s"
msgstr "Vous avez été invité à partager vos dépenses pour %(project)s" msgstr "Vous avez été invité à partager vos dépenses pour %(project)s"
#: web.py:259 #: web.py:408
#, python-format
msgid ""No token provided""
msgstr "Aucun token n'a été fourni."
#: web.py:259
#, python-format
msgid "Unknown project"
msgstr "Project inconnu"
#: web.py:261
#, python-format
msgid "Invalid token"
msgstr "Token invalide"
#: web.py:267
#, python-format
msgid "Password successfully reset."
msgstr "Le mot de passe a été changé avec succès."
#: web.py:261
msgid "Your invitations have been sent" msgid "Your invitations have been sent"
msgstr "Vos invitations ont bien été envoyées" msgstr "Vos invitations ont bien été envoyées"
#: web.py:290 #: web.py:439
#, python-format #, python-format
msgid "%(member)s had been added" msgid "%(member)s had been added"
msgstr "%(member)s a bien été ajouté" msgstr "%(member)s a bien été ajouté"
#: web.py:303 #: web.py:452
#, python-format #, python-format
msgid "%(name)s is part of this project again" msgid "%(name)s is part of this project again"
msgstr "%(name)s a rejoint le projet" msgstr "%(name)s a rejoint le projet"
#: web.py:312 #: web.py:461
#, python-format #, python-format
msgid "User '%(name)s' has been deactivated" msgid ""
msgstr "Le membre '%(name)s' a été désactivé" "User '%(name)s' has been deactivated. It will still appear in the users "
"list until its balance becomes zero."
msgstr ""
"Le membre « %(name)s » a été désactivé. Il continuera dapparaître jusqu'à "
"ce que son solde soit nul."
#: web.py:314 #: web.py:465
#, python-format #, python-format
msgid "User '%(name)s' has been deactivated. It will still appear in the users list until its balance becomes zero." msgid "User '%(name)s' has been removed"
msgstr "Le membre '%(name)s' a été désactivé. Il continuera d'apparaître jusqu'à ce que sa balance devienne égale à zéro." msgstr "Le membre « %(name)s » a été supprimé"
#: web.py:331 #: web.py:480
#, python-format
msgid "User '%(name)s' has been edited"
msgstr "Le membre « %(name)s » a été édité"
#: web.py:500
msgid "The bill has been added" msgid "The bill has been added"
msgstr "La facture a bien été ajoutée" msgstr "La facture a bien été ajoutée"
#: web.py:351 #: web.py:520
msgid "The bill has been deleted" msgid "The bill has been deleted"
msgstr "La facture a été supprimée" msgstr "La facture a été supprimée"
#: web.py:369 #: web.py:538
msgid "The bill has been modified" msgid "The bill has been modified"
msgstr "La facture a été modifiée" msgstr "La facture a été modifiée"
#: templates/add_bill.html:9 #: templates/add_bill.html:9 templates/edit_member.html:9
msgid "Back to the list" msgid "Back to the list"
msgstr "Retourner à la liste" msgstr "Retourner à la liste"
#: templates/authenticate.html:6 #: templates/admin.html:10
msgid "" msgid "Administration tasks are currently disabled."
"The project you are trying to access do not exist, do you want to" msgstr "Les tâches dadministration sont actuellement désactivées."
msgstr "Le projet auquel vous essayez d'acceder n'existe pas. Souhaitez vous"
#: templates/authenticate.html:7 #: templates/authenticate.html:6
msgid "The project you are trying to access do not exist, do you want to"
msgstr "Le projet auquel vous essayez daccéder nexiste pas, souhaitez vous"
#: templates/authenticate.html:8
msgid "create it" msgid "create it"
msgstr "le créer" msgstr "le créer"
#: templates/authenticate.html:7 #: templates/authenticate.html:8
msgid "?" msgid "?"
msgstr " ?" msgstr " ?"
#: templates/authenticate.html:7
msgid "Administration tasks are currently disabled."
msgstr "Les tâches d'administration sont actuellement désactivées."
#: templates/create_project.html:4 #: templates/create_project.html:4
msgid "Create a new project" msgid "Create a new project"
@ -314,72 +310,85 @@ msgstr "Facture la plus récente"
msgid "Oldest bill" msgid "Oldest bill"
msgstr "Facture la plus ancienne" msgstr "Facture la plus ancienne"
#: templates/dashboard.html:5 templates/list_bills.html:101
msgid "Actions"
msgstr "Actions"
#: templates/dashboard.html:17 templates/list_bills.html:65
#: templates/list_bills.html:111
msgid "edit"
msgstr "éditer"
#: templates/dashboard.html:18 templates/forms.html:83
#: templates/list_bills.html:112
msgid "delete"
msgstr "supprimer"
#: templates/dashboard.html:25 #: templates/dashboard.html:25
msgid "The Dashboard is currently deactivated." msgid "The Dashboard is currently deactivated."
msgstr "La page d'administration est actuellement désactivée." msgstr "Le tableau de bord est actuellement désactivée."
#: templates/edit_project.html:6 templates/list_bills.html:24 #: templates/edit_project.html:6 templates/list_bills.html:24
msgid "you sure?" msgid "you sure?"
msgstr "c'est sûr ?" msgstr "cest sûr ?"
#: templates/edit_project.html:11 #: templates/edit_project.html:11
msgid "Edit this project" msgid "Edit this project"
msgstr "Éditer ce projet" msgstr "Éditer ce projet"
#: templates/forms.html:23 #: templates/edit_project.html:15
msgid "Can't remember the password?"
msgstr "Vous ne vous souvenez plus du code d'accès ?"
#: templates/forms.html:26
msgid "Cancel"
msgstr "Annuler"
#: templates/forms.html:68
msgid "Edit the project"
msgstr "Éditer le projet"
#: templates/list_bills.html:70
msgid "deactivate"
msgstr "désactiver"
#: templates/forms.html:69 templates/list_bills.html:70
#: templates/list_bills.html:114
msgid "delete"
msgstr "supprimer"
#: templates/forms.html:77
msgid "Edit this bill"
msgstr "Éditer cette facture"
#: templates/forms.html:77 templates/list_bills.html:94
msgid "Add a bill"
msgstr "Ajouter une facture"
#: templates/forms.html:95
msgid "Type user name here"
msgstr "Nouveau participant"
#: templates/forms.html:100
msgid "Edit this member"
msgstr "Éditer ce participant"
#: templates/forms.html:102
msgid "Send the invitations"
msgstr "Envoyer les invitations"
#: templates/forms.html:103
msgid "No, thanks"
msgstr "Non merci"
#: templates/forms.html:136
msgid "Download this project's data" msgid "Download this project's data"
msgstr "Télécharger les données de ce projet" msgstr "Télécharger les données de ce projet"
#: templates/forms.html:136 #: templates/forms.html:27
msgid "Can't remember the password?"
msgstr "Vous ne vous souvenez plus du code daccès ?"
#: templates/forms.html:30
msgid "Cancel"
msgstr "Annuler"
#: templates/forms.html:82
msgid "Edit the project"
msgstr "Éditer le projet"
#: templates/forms.html:91
msgid "Edit this bill"
msgstr "Éditer cette facture"
#: templates/forms.html:91 templates/list_bills.html:89
msgid "Add a bill"
msgstr "Ajouter une facture"
#: templates/forms.html:103
msgid "Select all"
msgstr "Tout cocher"
#: templates/forms.html:103
msgid "Select none"
msgstr "Tout décocher"
#: templates/forms.html:122
msgid "Type user name here"
msgstr "Nouveau participant"
#: templates/forms.html:129
msgid "Edit this member"
msgstr "Éditer ce participant"
#: templates/forms.html:145
msgid "Send the invitations"
msgstr "Envoyer les invitations"
#: templates/forms.html:146
msgid "No, thanks"
msgstr "Non merci"
#: templates/forms.html:157
msgid "Download" msgid "Download"
msgstr "Télécharger" msgstr "Télécharger"
#: templates/home.html:8 #: templates/home.html:7
msgid "Manage your shared <br>expenses, easily" msgid "Manage your shared <br>expenses, easily"
msgstr "Gérez vos dépenses <br>partagées, facilement" msgstr "Gérez vos dépenses <br>partagées, facilement"
@ -387,199 +396,230 @@ msgstr "Gérez vos dépenses<br> partagées, facilement"
msgid "Try out the demo" msgid "Try out the demo"
msgstr "Essayez la démo" msgstr "Essayez la démo"
#: templates/home.html:12 #: templates/home.html:13
msgid "You're sharing a house?" msgid "You're sharing a house?"
msgstr "Vous êtes en colocation ?" msgstr "Vous êtes en colocation ?"
#: templates/home.html:12 #: templates/home.html:13
msgid "Going on holidays with friends?" msgid "Going on holidays with friends?"
msgstr "Partez en vacances avec des amis ?" msgstr "Partez en vacances avec des amis ?"
#: templates/home.html:12 #: templates/home.html:13
msgid "Simply sharing money with others?" msgid "Simply sharing money with others?"
msgstr "Ça vous arrive de partager de l'argent avec d'autres ?" msgstr "Ça vous arrive de partager de largent avec dautres ?"
#: templates/home.html:12 #: templates/home.html:13
msgid "We can help!" msgid "We can help!"
msgstr "On peut vous aider !" msgstr "On peut vous aider !"
#: templates/home.html:24 #: templates/home.html:21
msgid "Log to an existing project" msgid "Log to an existing project"
msgstr "Se connecter à un projet existant" msgstr "Se connecter à un projet existant"
#: templates/home.html:28 #: templates/home.html:25
msgid "log in" msgid "log in"
msgstr "se connecter" msgstr "se connecter"
#: templates/home.html:29 #: templates/home.html:26
msgid "can't remember your password?" msgid "can't remember your password?"
msgstr "vous ne vous souvenez plus du code d'accès ?" msgstr "vous ne vous souvenez plus du code daccès ?"
#: templates/home.html:36 #: templates/home.html:34 templates/home.html:42
msgid "or create a new one" msgid "or create a new one"
msgstr "ou créez en un nouveau" msgstr "ou créez en un nouveau"
#: templates/home.html:40 #: templates/home.html:38
msgid "let's get started" msgid "let's get started"
msgstr "c'est parti !" msgstr "cest parti !"
#: templates/home.html:51 #: templates/home.html:51
msgid "" msgid ""
"This access code will be sent to your friends. It is stored as-is by the " "This access code will be sent to your friends. It is stored as-is by the "
"server, so don\\'t reuse a personal password!" "server, so don\\'t reuse a personal password!"
msgstr "" msgstr ""
"Ce code d\\'accès va être envoyé à vos amis et stocké en clair sur le " "Ce code daccès va être envoyé à vos amis et stocké en clair sur le serveur. "
"serveur.N\\'utilisez pas un mot de passe personnel !" "Nutilisez pas un mot de passe personnel !"
#: templates/layout.html:5 #: templates/layout.html:5
msgid "Account manager" msgid "Account manager"
msgstr "Gestion de comptes" msgstr "Gestion de comptes"
#: templates/layout.html:45 templates/settle_bills.html:4 #: templates/layout.html:39
msgid "Bills" msgid "Bills"
msgstr "Factures" msgstr "Factures"
#: templates/layout.html:46 templates/settle_bills.html:5 #: templates/layout.html:40
msgid "Settle" msgid "Settle"
msgstr "Remboursements" msgstr "Remboursements"
#: templates/layout.html:50 #: templates/layout.html:41
msgid "Statistics" msgid "Statistics"
msgstr "Statistiques" msgstr "Statistiques"
#: templates/layout.html:53 #: templates/layout.html:48
msgid "options" msgid "options"
msgstr "options" msgstr "options"
#: templates/layout.html:55 #: templates/layout.html:50
msgid "Project settings" msgid "Project settings"
msgstr "Options du projet" msgstr "Options du projet"
#: templates/layout.html:59 #: templates/layout.html:54
msgid "switch to" msgid "switch to"
msgstr "aller à" msgstr "aller à"
#: templates/layout.html:62 #: templates/layout.html:57
msgid "Start a new project" msgid "Start a new project"
msgstr "Nouveau projet" msgstr "Nouveau projet"
#: templates/layout.html:64 #: templates/layout.html:59
msgid "Logout" msgid "Logout"
msgstr "Se déconnecter" msgstr "Se déconnecter"
#: templates/layout.html:92 #: templates/layout.html:66
msgid "Dashboard"
msgstr "Tableau de bord"
#: templates/layout.html:89
msgid "This is a free software" msgid "This is a free software"
msgstr "Ceci est un logiciel libre" msgstr "Ceci est un logiciel libre"
#: templates/layout.html:92 #: templates/layout.html:89
msgid "you can contribute and improve it!" msgid "you can contribute and improve it!"
msgstr "vous pouvez y contribuer et l'améliorer" msgstr "vous pouvez y contribuer et laméliorer !"
#: templates/list_bills.html:74 #: templates/list_bills.html:63
msgid "deactivate"
msgstr "désactiver"
#: templates/list_bills.html:70
msgid "reactivate" msgid "reactivate"
msgstr "ré-activer" msgstr "ré-activer"
#: templates/list_bills.html:88 #: templates/list_bills.html:82
msgid "Invite"
msgstr "Invitez"
#: templates/list_bills.html:88
msgid "Invite people to join this project!" msgid "Invite people to join this project!"
msgstr "Invitez d'autres personnes à rejoindre ce projet !" msgstr "Invitez dautres personnes à rejoindre ce projet !"
#: templates/list_bills.html:89 #: templates/list_bills.html:83
msgid "Add a new bill" msgid "Add a new bill"
msgstr "Nouvelle facture" msgstr "Nouvelle facture"
#: templates/list_bills.html:103 #: templates/list_bills.html:101
msgid "When?" msgid "When?"
msgstr "Quand ?" msgstr "Quand ?"
#: templates/list_bills.html:103 #: templates/list_bills.html:101
msgid "Who paid?" msgid "Who paid?"
msgstr "Qui a payé ?" msgstr "Qui a payé ?"
#: templates/list_bills.html:103 #: templates/list_bills.html:101
msgid "For what?" msgid "For what?"
msgstr "Pour quoi ?" msgstr "Pour quoi ?"
#: templates/list_bills.html:103 templates/settle_bills.html:31 #: templates/list_bills.html:101 templates/settle_bills.html:22
msgid "How much?" msgid "How much?"
msgstr "Combien ?" msgstr "Combien ?"
#: templates/list_bills.html:103 #: templates/list_bills.html:109
msgid "Actions"
msgstr "Actions"
#: templates/list_bills.html:111
msgid "each" msgid "each"
msgstr "chacun" msgstr "chacun"
#: templates/list_bills.html:113 #: templates/list_bills.html:120
msgid "edit"
msgstr "éditer"
#: templates/list_bills.html:122
msgid "Nothing to list yet. You probably want to" msgid "Nothing to list yet. You probably want to"
msgstr "Rien à lister pour l'instant. Vous voulez surement" msgstr "Rien à lister pour linstant. Vous voulez surement"
#: templates/list_bills.html:122 #: templates/list_bills.html:120
msgid "add a bill" msgid "add a bill"
msgstr "ajouter une facture" msgstr "ajouter une facture"
#: templates/password_reminder.html:4 #: templates/password_reminder.html:4
msgid "Password reminder" msgid "Password reminder"
msgstr "Rappel du code d'accès" msgstr "Rappel du code daccès"
#: templates/recent_projects.html:2 #: templates/recent_projects.html:2
msgid "Your projects" msgid "Your projects"
msgstr "Vos projets" msgstr "Vos projets"
#: templates/reset_password.html:2 #: templates/reset_password.html:7
msgid "Reset your password" msgid "Reset your password"
msgstr "Changez votre mot de passe" msgstr "Changez votre mot de passe"
#: templates/send_invites.html:11 #: templates/send_invites.html:4
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "Invitez des personnes à rejoindre ce projet" msgstr "Invitez des personnes à rejoindre ce projet"
#: templates/send_invites.html:12 #: templates/send_invites.html:5
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a (comma separated) list of email adresses you want to notify "
"about the\n" "about the\n"
"creation of this budget management project and we will send them an email" "creation of this budget management project and we will send them an email"
" for you." " for you."
msgstr "" msgstr ""
"Entrez les addresses des personnes que vous souhaitez inviter, séparées " "Entrez les adresses des personnes que vous souhaitez inviter,\n"
"par des virgules. On s'occupe de leur envoyer un email." "séparées par des virgules, on soccupe de leur envoyer un email."
#: templates/send_invites.html:14 #: templates/send_invites.html:7
msgid "If you prefer, you can share the project identifier and the shared\n" msgid ""
"password by other communication means. Or even directly share the following link:" "If you prefer, you can share the project identifier and the shared\n"
msgstr "Si vous préférez vous pouvez partager l'identifiant du projet et son mot " "password by other communication means. Or even directly share the "
"de passe par un autre moyen de communication. Ou directement partager le lien " "following link:"
"suivant :" msgstr ""
"Si vous préférez vous pouvez partager lidentifiant du projet\n"
"et son mot de passe par un autre moyen de communication. Ou directement "
"partager le lien suivant :"
#: templates/settle_bills.html:31 #: templates/settle_bills.html:22
msgid "Who pays?" msgid "Who pays?"
msgstr "Qui doit payer ?" msgstr "Qui doit payer ?"
#: templates/settle_bills.html:31 #: templates/settle_bills.html:22
msgid "To whom?" msgid "To whom?"
msgstr "Pour qui ?" msgstr "Pour qui ?"
#: templates/statistics.html:22 #: templates/statistics.html:21
msgid "Who?" msgid "Who?"
msgstr "Qui ?" msgstr "Qui ?"
#: templates/statistics.html:22 #: templates/statistics.html:21
msgid "Paid" msgid "Paid"
msgstr "A payé" msgstr "A payé"
#: templates/statistics.html:22 #: templates/statistics.html:21
msgid "Spent" msgid "Spent"
msgstr "A dépensé" msgstr "A dépensé"
#: templates/statistics.html:22 #: templates/statistics.html:21
msgid "Balance" msgid "Balance"
msgstr "Solde" msgstr "Solde"
#~ msgid ""
#~ "The project identifier is used to "
#~ "log in and for the URL of "
#~ "the project. We tried to generate "
#~ "an identifier for you but a "
#~ "project with this identifier already "
#~ "exists. Please create a new identifier"
#~ " that you will be able to "
#~ "remember."
#~ msgstr ""
#~ "Lidentifiant du projet est utilisé pour"
#~ " se connecter.Nous avons essayé de "
#~ "générer un identifiant mais celui ci "
#~ "existe déjà. Merci de créer un "
#~ "nouvel identifiant que vous serez "
#~ "capable de retenir"
#~ msgid "Start date"
#~ msgstr "Date de départ"
#~ msgid "End date"
#~ msgstr "Date de fin"
#~ msgid "\"No token provided\""
#~ msgstr "Aucun token na été fourni."
#~ msgid "User '%(name)s' has been deactivated"
#~ msgstr "Le membre '%(name)s' a été désactivé"
#~ msgid "Invite"
#~ msgstr "Invitez"

View file

@ -3,7 +3,7 @@ import re
from io import BytesIO, StringIO from io import BytesIO, StringIO
import jinja2 import jinja2
from json import dumps from json import dumps, JSONEncoder
from flask import redirect from flask import redirect
from werkzeug.routing import HTTPException, RoutingException from werkzeug.routing import HTTPException, RoutingException
import six import six
@ -185,3 +185,28 @@ def create_jinja_env(folder, strict_rendering=False):
if strict_rendering: if strict_rendering:
kwargs['undefined'] = jinja2.StrictUndefined kwargs['undefined'] = jinja2.StrictUndefined
return jinja2.Environment(**kwargs) return jinja2.Environment(**kwargs)
class IhmJSONEncoder(JSONEncoder):
"""Subclass of the default encoder to support custom objects.
Taken from the deprecated flask-rest package."""
def default(self, o):
if hasattr(o, "_to_serialize"):
# build up the object
data = {}
for attr in o._to_serialize:
data[attr] = getattr(o, attr)
return data
elif hasattr(o, "isoformat"):
return o.isoformat()
else:
try:
from flask_babel import speaklater
if isinstance(o, speaklater.LazyString):
try:
return unicode(o) # For python 2.
except NameError:
return str(o) # For python 3.
except ImportError:
pass
return JSONEncoder.default(self, o)

View file

@ -566,21 +566,9 @@ def settle_bill():
@main.route("/<project_id>/statistics") @main.route("/<project_id>/statistics")
def statistics(): def statistics():
"""Compute what each member has paid and spent and display it""" """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( return render_template(
"statistics.html", "statistics.html",
members=members, members_stats=g.project.members_stats,
balance=balance,
paid=paid,
spent=spent,
current_view='statistics', current_view='statistics',
) )

View file

@ -5,7 +5,7 @@ flask-mail>=0.8
Flask-Migrate>=1.8.0 Flask-Migrate>=1.8.0
Flask-script Flask-script
flask-babel flask-babel
flask-rest>=1.3 flask-restful>=0.3.6
jinja2>=2.6 jinja2>=2.6
raven raven
blinker blinker

View file

@ -2,16 +2,6 @@
import codecs import codecs
import os import os
from setuptools import setup, find_packages from setuptools import setup, find_packages
try:
from pip.req import parse_requirements
from pip.download import PipSession
except ImportError:
print('Cannot find pip.')
raise
# Get requirements from the requirements.txt file.
pip_requirements = parse_requirements("requirements.txt", session=PipSession())
install_requires = [str(ir.req) for ir in pip_requirements]
here = os.path.abspath(os.path.dirname(__file__)) here = os.path.abspath(os.path.dirname(__file__))
@ -23,6 +13,13 @@ def read_file(filename):
return content return content
def parse_requirements(filename):
""" load requirements from a pip requirements file """
with open(filename) as lines:
lineiter = (line.strip() for line in lines)
return [line for line in lineiter if line and not line.startswith("#")]
README = read_file('README.rst') README = read_file('README.rst')
CHANGELOG = read_file('CHANGELOG.rst') CHANGELOG = read_file('CHANGELOG.rst')
@ -37,7 +34,7 @@ ENTRY_POINTS = {
setup(name='ihatemoney', setup(name='ihatemoney',
version='2.1.dev0', version='2.1.1.dev0',
description='A simple shared budget manager web application.', description='A simple shared budget manager web application.',
long_description="{}\n\n{}".format(README.encode('utf-8'), CHANGELOG.encode('utf-8')), long_description="{}\n\n{}".format(README.encode('utf-8'), CHANGELOG.encode('utf-8')),
license='Custom BSD Beerware', license='Custom BSD Beerware',
@ -59,5 +56,5 @@ setup(name='ihatemoney',
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
install_requires=install_requires, install_requires=parse_requirements('requirements.txt'),
entry_points=ENTRY_POINTS) entry_points=ENTRY_POINTS)