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

View file

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

View file

@ -4,12 +4,11 @@ RUN mkdir /ihatemoney &&\
mkdir -p /etc/ihatemoney &&\
pip install --no-cache-dir gunicorn pymysql
WORKDIR /ihatemoney
COPY . .
COPY . /ihatemoney
ARG INSTALL_FROM_PYPI="False"
RUN if [ "$INSTALL_FROM_PYPI" = True ]; then\
pip install --no-cache-dir ihatemoney ; else\
pip install --no-cache-dir -e . ; \
pip install --no-cache-dir -e /ihatemoney ; \
fi
ENV DEBUG="False" \
@ -26,9 +25,8 @@ ENV DEBUG="False" \
ACTIVATE_DEMO_PROJECT="True" \
ADMIN_PASSWORD="" \
ALLOW_PUBLIC_PROJECT_CREATION="True" \
ACTIVATE_ADMIN_DASHBOARD="False" \
GUNICORN_NUM_WORKERS="3"
ACTIVATE_ADMIN_DASHBOARD="False"
VOLUME /database
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
ACTIVATE_ADMIN_DASHBOARD = $ACTIVATE_ADMIN_DASHBOARD
EOF
gunicorn ihatemoney.wsgi:application \
-b 0.0.0.0:8000 \
--log-syslog \
-w "$GUNICORN_NUM_WORKERS"
# Start gunicorn without forking
exec gunicorn ihatemoney.wsgi:application \
-b 0.0.0.0:8000 \
--log-syslog \
"$@"

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\
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
}
]

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
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
=============

View file

@ -1,62 +1,75 @@
# -*- coding: utf-8 -*-
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 ihatemoney.models import db, Project, Person, Bill
from ihatemoney.forms import (ProjectForm, EditProjectForm, MemberForm,
get_billform_for)
from werkzeug.security import check_password_hash
from functools import wraps
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.
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
"""
auth = request.authorization
@wraps(f)
def wrapper(*args, **kwargs):
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" in kwargs and \
auth.username == kwargs["project_id"]:
project = Project.query.get(auth.username)
if project and check_password_hash(project.password, auth.password):
return project
return False
if auth and project_id and auth.username == project_id:
project = Project.query.get(auth.username)
if project and check_password_hash(project.password, auth.password):
# The whole project object will be passed instead of project_id
kwargs.pop("project_id")
return f(*args, project=project, **kwargs)
abort(401)
return wrapper
class ProjectHandler(object):
def add(self):
class ProjectsHandler(Resource):
def post(self):
form = ProjectForm(meta={'csrf': False})
if form.validate():
project = form.save()
db.session.add(project)
db.session.commit()
return 201, project.id
return 400, form.errors
return project.id, 201
return form.errors, 400
class ProjectHandler(Resource):
method_decorators = [need_auth]
@need_auth(check_project, "project")
def get(self, project):
return 200, project
return project
@need_auth(check_project, "project")
def delete(self, project):
db.session.delete(project)
db.session.commit()
return 200, "DELETED"
return "DELETED"
@need_auth(check_project, "project")
def update(self, project):
def put(self, project):
form = EditProjectForm(meta={'csrf': False})
if form.validate():
form.update(project)
db.session.commit()
return 200, "UPDATED"
return 400, form.errors
return "UPDATED"
return form.errors, 400
class ProjectStatsHandler(Resource):
method_decorators = [need_auth]
def get(self, project):
return project.members_stats
class APIMemberForm(MemberForm):
@ -71,98 +84,93 @@ class APIMemberForm(MemberForm):
return super(APIMemberForm, self).save(project, person)
class MemberHandler(object):
class MembersHandler(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 404, "Not Found"
return 200, member
def get(self, project):
return project.members
def list(self, project):
return 200, project.members
def add(self, project):
def post(self, project):
form = MemberForm(project, meta={'csrf': False})
if form.validate():
member = Person()
form.save(project, member)
db.session.commit()
return 201, member.id
return 400, form.errors
return member.id, 201
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)
if form.validate():
member = Person.query.get(member_id, project)
form.save(project, member)
db.session.commit()
return 200, member
return 400, form.errors
return member
return form.errors, 400
def delete(self, project, member_id):
if project.remove_member(member_id):
return 200, "OK"
return 404, "Not Found"
return "OK"
return "Not Found", 404
class BillHandler(object):
class BillsHandler(Resource):
method_decorators = [need_auth]
def get(self, project, bill_id):
bill = Bill.query.get(project, bill_id)
if not bill:
return 404, "Not Found"
return 200, bill
def list(self, project):
def get(self, project):
return project.get_bills().all()
def add(self, project):
def post(self, project):
form = get_billform_for(project, True, meta={'csrf': False})
if form.validate():
bill = Bill()
form.save(bill, project)
db.session.add(bill)
db.session.commit()
return 201, bill.id
return 400, form.errors
return bill.id, 201
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})
if form.validate():
bill = Bill.query.get(project, bill_id)
form.save(bill, project)
db.session.commit()
return 200, bill.id
return 400, form.errors
return bill.id, 200
return form.errors, 400
def delete(self, project, bill_id):
bill = Bill.query.delete(project, bill_id)
db.session.commit()
if not bill:
return 404, "Not Found"
return 200, "OK"
return "Not Found", 404
return "OK", 200
project_resource = RESTResource(
name="project",
route="/projects",
app=api,
actions=["add", "update", "delete", "get"],
handler=ProjectHandler())
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)
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>")

View file

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

View file

@ -11,7 +11,7 @@ from werkzeug.contrib.fixers import ProxyFix
from ihatemoney.api import api
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 import default_settings
@ -68,6 +68,8 @@ def load_configuration(app, configuration=None):
app.config.from_pyfile(env_var_config)
else:
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):

View file

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

View file

@ -4,7 +4,7 @@
<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>
<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>
{% if project.has_bills() %}
<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>
<tbody>
{% 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.receiver }}</td>
<td>{{ "%0.2f"|format(bill.amount) }}</td>

View file

@ -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 %}
<tr class="{{ loop.cycle("odd", "even") }}">
<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>
{% for stat in members_stats %}
<tr>
<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>

View file

@ -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'))
@ -1053,7 +1053,7 @@ class APITestCase(IhatemoneyTestCase):
})
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'))
# create it
@ -1139,7 +1139,7 @@ class APITestCase(IhatemoneyTestCase):
headers=self.get_auth("raclette"))
self.assertStatus(200, req)
self.assertEqual('[]', req.data.decode('utf-8'))
self.assertEqual('[]\n', req.data.decode('utf-8'))
# add a member
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
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
req = self.client.get("/api/projects/raclette/members",
@ -1223,7 +1223,7 @@ class APITestCase(IhatemoneyTestCase):
headers=self.get_auth("raclette"))
self.assertStatus(200, req)
self.assertEqual('[]', req.data.decode('utf-8'))
self.assertEqual('[]\n', req.data.decode('utf-8'))
def test_bills(self):
# create a project
@ -1239,7 +1239,7 @@ class APITestCase(IhatemoneyTestCase):
headers=self.get_auth("raclette"))
self.assertStatus(200, req)
self.assertEqual("[]", req.data.decode('utf-8'))
self.assertEqual("[]\n", req.data.decode('utf-8'))
# add a bill
req = self.client.post("/api/projects/raclette/bills", data={
@ -1252,7 +1252,7 @@ class APITestCase(IhatemoneyTestCase):
# should return the id
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
req = self.client.get("/api/projects/raclette/bills/1",
@ -1288,7 +1288,7 @@ class APITestCase(IhatemoneyTestCase):
}, headers=self.get_auth("raclette"))
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
req = self.client.put("/api/projects/raclette/bills/1", data={
@ -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")

View file

@ -2,293 +2,289 @@
# Copyright (C) 2011 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# Alexis Métaireau <alexis@notmyidea.org>, 2011.
#
# Adrien CLERC, 2018.
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2013-10-13 21:32+0200\n"
"PO-Revision-Date: 2011-10-14 23:51+0200\n"
"Last-Translator: Quentin Roy <royque@gmail.com>\n"
"POT-Creation-Date: 2018-05-15 21:43+0200\n"
"PO-Revision-Date: 2018-05-15 22:00+0200\n"
"Last-Translator: Adrien CLERC <>\n"
"Language-Team: fr <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\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
msgid "Select all"
msgstr "Tout cocher"
#: forms.py:22
msgid "Select none"
msgstr "Tout décocher"
#: forms.py:61
#: forms.py:46
msgid "Project name"
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"
msgstr "Code d'accès"
msgstr "Code daccès"
#: forms.py:63
#: forms.py:48
msgid "Email"
msgstr "Email"
#: forms.py:85 forms.py:101 forms.py:107
#: forms.py:70 forms.py:87 forms.py:98
msgid "Project identifier"
msgstr "Identifiant du projet"
#: forms.py:87
msgid "Admin password"
msgstr "Mot de passe administrateur"
#: forms.py:87 templates/send_invites.html:5
#: forms.py:72
msgid "Create the project"
msgstr "Créer le projet"
#: forms.py:92
#: forms.py:77
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."
"be able to remember"
msgstr ""
"L'identifiant 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"
"Lidentifiant du projet est utilisé pour se connecter et pour lURL du "
"projet. 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"
#: forms.py:103
#: forms.py:89 forms.py:94
msgid "Get in"
msgstr "Entrer"
#: forms.py:107
msgid "Password mismatch"
msgstr "Les mots de passe fournis ne sont pas les mêmes."
#: forms.py:93
msgid "Admin password"
msgstr "Mot de passe administrateur"
#: forms.py:109
msgid "Password confirmation"
msgstr "Confirmation du mot de passe"
#: forms.py:107
msgid "Password"
msgstr "Mot de passe"
#: forms.py:108
#: forms.py:99
msgid "Send me the code by email"
msgstr "Envoyez moi le code par email"
#: forms.py:112
#: forms.py:103
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"
msgstr "Date"
#: forms.py:117
#: forms.py:116
msgid "What?"
msgstr "Quoi ?"
#: forms.py:118
#: forms.py:117
msgid "Payer"
msgstr "Payeur"
#: forms.py:119
#: forms.py:118
msgid "Amount paid"
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?"
msgstr "Pour qui ?"
#: forms.py:122
#: forms.py:121
msgid "Submit"
msgstr "Valider"
#: forms.py:123
#: forms.py:122
msgid "Submit and add a new one"
msgstr "Valider et ajouter une autre facture"
#: forms.py:149
#: forms.py:146
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"
msgstr "Nom"
#: forms.py:155
#: forms.py:152
msgid "Weight"
msgstr "Parts"
#: forms.py:155 templates/forms.html:95
#: forms.py:153 templates/forms.html:123
msgid "Add"
msgstr "Ajouter"
#: forms.py:163
#: forms.py:162
msgid "User name incorrect"
msgstr "Nom d'utilisateur incorrect"
msgstr "Nom dutilisateur incorrect"
#: forms.py:167
msgid "This project already have this member"
msgstr "Ce membre existe déjà pour ce projet"
#: forms.py:178
#: forms.py:183
msgid "People to notify"
msgstr "Personnes à prévenir"
#: forms.py:179
#: forms.py:184
msgid "Send invites"
msgstr "Envoyer les invitations"
#: forms.py:185
#: forms.py:190
#, python-format
msgid "The email %(email)s is not valid"
msgstr "L'email %(email)s est invalide"
msgstr "Lemail %(email)s est invalide"
#: forms.py:191
msgid "Start date"
msgstr "Date de départ"
#: forms.py:192
msgid "End date"
msgstr "Date de fin"
#: forms.py:202
#: forms.py:196
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"
msgstr "factures"
#: forms.py:205
#: forms.py:199
msgid "transactions"
msgstr "remboursements"
#: forms.py:206
#: forms.py:201
msgid "Export file format"
msgstr "Format du fichier d'export"
msgstr "Format du fichier dexport"
#: web.py:95
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
#: web.py:129
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
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
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."
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"
msgstr "Projet supprimé"
#: web.py:254
#: web.py:401
#, python-format
msgid "You have been invited to share your expenses for %(project)s"
msgstr "Vous avez été invité à partager vos dépenses pour %(project)s"
#: web.py:259
#, 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
#: web.py:408
msgid "Your invitations have been sent"
msgstr "Vos invitations ont bien été envoyées"
#: web.py:290
#: web.py:439
#, python-format
msgid "%(member)s had been added"
msgstr "%(member)s a bien été ajouté"
#: web.py:303
#: web.py:452
#, python-format
msgid "%(name)s is part of this project again"
msgstr "%(name)s a rejoint le projet"
#: web.py:312
#: web.py:461
#, python-format
msgid "User '%(name)s' has been deactivated"
msgstr "Le membre '%(name)s' a été désactivé"
msgid ""
"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
msgid "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 d'apparaître jusqu'à ce que sa balance devienne égale à zéro."
msgid "User '%(name)s' has been removed"
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"
msgstr "La facture a bien été ajoutée"
#: web.py:351
#: web.py:520
msgid "The bill has been deleted"
msgstr "La facture a été supprimée"
#: web.py:369
#: web.py:538
msgid "The bill has been modified"
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"
msgstr "Retourner à la liste"
#: 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 d'acceder n'existe pas. Souhaitez vous"
#: templates/admin.html:10
msgid "Administration tasks are currently disabled."
msgstr "Les tâches dadministration sont actuellement désactivées."
#: 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"
msgstr "le créer"
#: templates/authenticate.html:7
#: templates/authenticate.html:8
msgid "?"
msgstr " ?"
#: templates/authenticate.html:7
msgid "Administration tasks are currently disabled."
msgstr "Les tâches d'administration sont actuellement désactivées."
msgstr " ?"
#: templates/create_project.html:4
msgid "Create a new project"
@ -314,272 +310,316 @@ msgstr "Facture la plus récente"
msgid "Oldest bill"
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
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
msgid "you sure?"
msgstr "c'est sûr ?"
msgstr "cest sûr ?"
#: templates/edit_project.html:11
msgid "Edit this project"
msgstr "Éditer ce projet"
#: templates/forms.html:23
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
#: templates/edit_project.html:15
msgid "Download this project's data"
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"
msgstr "Télécharger"
#: templates/home.html:8
#: templates/home.html:7
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"
#: templates/home.html:9
msgid "Try out the demo"
msgstr "Essayez la démo"
#: templates/home.html:12
#: templates/home.html:13
msgid "You're sharing a house?"
msgstr "Vous êtes en colocation ?"
#: templates/home.html:12
#: templates/home.html:13
msgid "Going on holidays with friends?"
msgstr "Partez en vacances avec des amis ?"
#: templates/home.html:12
#: templates/home.html:13
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!"
msgstr "On peut vous aider !"
#: templates/home.html:24
#: templates/home.html:21
msgid "Log to an existing project"
msgstr "Se connecter à un projet existant"
#: templates/home.html:28
#: templates/home.html:25
msgid "log in"
msgstr "se connecter"
#: templates/home.html:29
#: templates/home.html:26
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"
msgstr "ou créez en un nouveau"
#: templates/home.html:40
#: templates/home.html:38
msgid "let's get started"
msgstr "c'est parti !"
msgstr "cest parti !"
#: templates/home.html:51
msgid ""
"This access code will be sent to your friends. It is stored as-is by the "
"server, so don\\'t reuse a personal password!"
msgstr ""
"Ce code d\\'accès va être envoyé à vos amis et stocké en clair sur le "
"serveur.N\\'utilisez pas un mot de passe personnel !"
"Ce code daccès va être envoyé à vos amis et stocké en clair sur le serveur. "
"Nutilisez pas un mot de passe personnel !"
#: templates/layout.html:5
msgid "Account manager"
msgstr "Gestion de comptes"
#: templates/layout.html:45 templates/settle_bills.html:4
#: templates/layout.html:39
msgid "Bills"
msgstr "Factures"
#: templates/layout.html:46 templates/settle_bills.html:5
#: templates/layout.html:40
msgid "Settle"
msgstr "Remboursements"
#: templates/layout.html:50
#: templates/layout.html:41
msgid "Statistics"
msgstr "Statistiques"
#: templates/layout.html:53
#: templates/layout.html:48
msgid "options"
msgstr "options"
#: templates/layout.html:55
#: templates/layout.html:50
msgid "Project settings"
msgstr "Options du projet"
#: templates/layout.html:59
#: templates/layout.html:54
msgid "switch to"
msgstr "aller à"
#: templates/layout.html:62
#: templates/layout.html:57
msgid "Start a new project"
msgstr "Nouveau projet"
#: templates/layout.html:64
#: templates/layout.html:59
msgid "Logout"
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"
msgstr "Ceci est un logiciel libre"
#: templates/layout.html:92
#: templates/layout.html:89
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"
msgstr "ré-activer"
#: templates/list_bills.html:88
msgid "Invite"
msgstr "Invitez"
#: templates/list_bills.html:88
#: templates/list_bills.html:82
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"
msgstr "Nouvelle facture"
#: templates/list_bills.html:103
#: templates/list_bills.html:101
msgid "When?"
msgstr "Quand ?"
#: templates/list_bills.html:103
#: templates/list_bills.html:101
msgid "Who paid?"
msgstr "Qui a payé ?"
#: templates/list_bills.html:103
#: templates/list_bills.html:101
msgid "For what?"
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?"
msgstr "Combien ?"
#: templates/list_bills.html:103
msgid "Actions"
msgstr "Actions"
#: templates/list_bills.html:111
#: templates/list_bills.html:109
msgid "each"
msgstr "chacun"
#: templates/list_bills.html:113
msgid "edit"
msgstr "éditer"
#: templates/list_bills.html:122
#: templates/list_bills.html:120
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"
msgstr "ajouter une facture"
#: templates/password_reminder.html:4
msgid "Password reminder"
msgstr "Rappel du code d'accès"
msgstr "Rappel du code daccès"
#: templates/recent_projects.html:2
msgid "Your projects"
msgstr "Vos projets"
#: templates/reset_password.html:2
#: templates/reset_password.html:7
msgid "Reset your password"
msgstr "Changez votre mot de passe"
#: templates/send_invites.html:11
#: templates/send_invites.html:4
msgid "Invite people to join this project"
msgstr "Invitez des personnes à rejoindre ce projet"
#: templates/send_invites.html:12
#: templates/send_invites.html:5
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
"creation of this budget management project and we will send them an email"
" for you."
msgstr ""
"Entrez les addresses des personnes que vous souhaitez inviter, séparées "
"par des virgules. On s'occupe de leur envoyer un email."
"Entrez les adresses des personnes que vous souhaitez inviter,\n"
"séparées par des virgules, on soccupe de leur envoyer un email."
#: templates/send_invites.html:14
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 "Si vous préférez vous pouvez partager l'identifiant du projet et son mot "
"de passe par un autre moyen de communication. Ou directement partager le lien "
"suivant :"
#: templates/send_invites.html:7
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 ""
"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?"
msgstr "Qui doit payer ?"
#: templates/settle_bills.html:31
#: templates/settle_bills.html:22
msgid "To whom?"
msgstr "Pour qui ?"
#: templates/statistics.html:22
#: templates/statistics.html:21
msgid "Who?"
msgstr "Qui ?"
#: templates/statistics.html:22
#: templates/statistics.html:21
msgid "Paid"
msgstr "A payé"
#: templates/statistics.html:22
#: templates/statistics.html:21
msgid "Spent"
msgstr "A dépensé"
#: templates/statistics.html:22
#: templates/statistics.html:21
msgid "Balance"
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
import jinja2
from json import dumps
from json import dumps, JSONEncoder
from flask import redirect
from werkzeug.routing import HTTPException, RoutingException
import six
@ -185,3 +185,28 @@ def create_jinja_env(folder, strict_rendering=False):
if strict_rendering:
kwargs['undefined'] = jinja2.StrictUndefined
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")
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',
)

View file

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

View file

@ -2,16 +2,6 @@
import codecs
import os
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__))
@ -23,6 +13,13 @@ def read_file(filename):
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')
CHANGELOG = read_file('CHANGELOG.rst')
@ -37,7 +34,7 @@ ENTRY_POINTS = {
setup(name='ihatemoney',
version='2.1.dev0',
version='2.1.1.dev0',
description='A simple shared budget manager web application.',
long_description="{}\n\n{}".format(README.encode('utf-8'), CHANGELOG.encode('utf-8')),
license='Custom BSD Beerware',
@ -59,5 +56,5 @@ setup(name='ihatemoney',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
install_requires=install_requires,
install_requires=parse_requirements('requirements.txt'),
entry_points=ENTRY_POINTS)