mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-05-01 10:42:24 +02:00
Merge branch 'master' into almet/fix-supervisord-template
This commit is contained in:
commit
1d0880f3cb
21 changed files with 817 additions and 556 deletions
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
10
Dockerfile
10
Dockerfile
|
@ -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"]
|
||||
|
|
|
@ -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 \
|
||||
"$@"
|
||||
|
|
22
docs/api.rst
22
docs/api.rst
|
@ -164,3 +164,25 @@ And you can of course `DELETE` them at `/api/projects/<id>/bills/<bill-id>`::
|
|||
$ curl --basic -u demo:demo -X DELETE\
|
||||
https://ihatemoney.org/api/projects/demo/bills/80\
|
||||
"OK"
|
||||
|
||||
|
||||
Statistics
|
||||
----------
|
||||
|
||||
You can get some project stats with a `GET` on `/api/projects/<id>/statistics`::
|
||||
|
||||
$ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/statistics
|
||||
[
|
||||
{
|
||||
"balance": 12.5,
|
||||
"member": {"activated": True, "id": 1, "name": "alexis", "weight": 1.0},
|
||||
"paid": 25.0,
|
||||
"spent": 12.5
|
||||
},
|
||||
{
|
||||
"balance": -12.5,
|
||||
"member": {"activated": True, "id": 2, "name": "fred", "weight": 1.0},
|
||||
"paid": 0,
|
||||
"spent": 12.5
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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
|
||||
=============
|
||||
|
|
|
@ -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>")
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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")
|
||||
|
|
Binary file not shown.
|
@ -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 d’accè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"
|
||||
"L’identifiant du projet est utilisé pour se connecter et pour l’URL 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 n’existe 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 d’une 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 d’utilisateur 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 "L’email %(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 d’export"
|
||||
|
||||
#: 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 d’authentification 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é n’est pas correct. "
|
||||
"Plus que %(num)d tentatives."
|
||||
|
||||
#: web.py:167
|
||||
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:195
|
||||
msgid "This private code is not the right one"
|
||||
msgstr "Le code que vous avez entré n’est 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)sL’identifiant 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 n’a é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 d’apparaî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 d’administration 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 d’accéder n’existe 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 "c’est 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 d’accè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 l’argent avec d’autres ?"
|
||||
|
||||
#: 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 d’accè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 "c’est 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 d’accès va être envoyé à vos amis et stocké en clair sur le serveur. "
|
||||
"N’utilisez 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 l’amé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 d’autres 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 l’instant. 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 d’accè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 s’occupe 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 l’identifiant 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 ""
|
||||
#~ "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"
|
||||
|
||||
#~ msgid "Start date"
|
||||
#~ msgstr "Date de départ"
|
||||
|
||||
#~ msgid "End date"
|
||||
#~ msgstr "Date de fin"
|
||||
|
||||
#~ msgid "\"No token provided\""
|
||||
#~ msgstr "Aucun token n’a été fourni."
|
||||
|
||||
#~ msgid "User '%(name)s' has been deactivated"
|
||||
#~ msgstr "Le membre '%(name)s' a été désactivé"
|
||||
|
||||
#~ msgid "Invite"
|
||||
#~ msgstr "Invitez"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
21
setup.py
21
setup.py
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue