aboutsummaryrefslogtreecommitdiff
path: root/ihatemoney
diff options
context:
space:
mode:
Diffstat (limited to 'ihatemoney')
-rw-r--r--ihatemoney/api.py176
-rw-r--r--ihatemoney/conf-templates/apache-vhost.conf.j23
-rwxr-xr-xihatemoney/manage.py25
-rw-r--r--ihatemoney/messages.pot374
-rw-r--r--ihatemoney/models.py20
-rw-r--r--ihatemoney/run.py4
-rw-r--r--ihatemoney/static/css/main.css2
-rw-r--r--ihatemoney/templates/dashboard.html2
-rw-r--r--ihatemoney/templates/invitation_mail.en.j2 (renamed from ihatemoney/templates/invitation_mail.en)4
-rw-r--r--ihatemoney/templates/invitation_mail.fr.j2 (renamed from ihatemoney/templates/invitation_mail.fr)6
-rw-r--r--ihatemoney/templates/password_reminder.en.j2 (renamed from ihatemoney/templates/password_reminder.en)4
-rw-r--r--ihatemoney/templates/password_reminder.fr.j2 (renamed from ihatemoney/templates/password_reminder.fr)0
-rw-r--r--ihatemoney/templates/reminder_mail.en.j2 (renamed from ihatemoney/templates/reminder_mail.en)0
-rw-r--r--ihatemoney/templates/reminder_mail.fr.j2 (renamed from ihatemoney/templates/reminder_mail.fr)0
-rw-r--r--ihatemoney/templates/settle_bills.html2
-rw-r--r--ihatemoney/templates/statistics.html23
-rw-r--r--ihatemoney/tests/tests.py99
-rw-r--r--ihatemoney/translations/fr/LC_MESSAGES/messages.mobin9762 -> 9909 bytes
-rw-r--r--ihatemoney/translations/fr/LC_MESSAGES/messages.po502
-rw-r--r--ihatemoney/utils.py46
-rw-r--r--ihatemoney/web.py20
21 files changed, 786 insertions, 526 deletions
diff --git a/ihatemoney/api.py b/ihatemoney/api.py
index 827202c..6068cf7 100644
--- a/ihatemoney/api.py
+++ b/ihatemoney/api.py
@@ -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
-
- # 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
-
-
-class ProjectHandler(object):
-
- def add(self):
+ @wraps(f)
+ def wrapper(*args, **kwargs):
+ auth = request.authorization
+ project_id = kwargs.get("project_id")
+
+ 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 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):
-
- 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
+class MembersHandler(Resource):
+ method_decorators = [need_auth]
- def list(self, project):
- return 200, project.members
+ def get(self, project):
+ return 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
+
+
+class MemberHandler(Resource):
+ method_decorators = [need_auth]
- def update(self, project, member_id):
+ 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"
-
-
-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)
+ return "Not Found", 404
+ return "OK", 200
+
+
+restful_api.add_resource(ProjectsHandler, '/projects')
+restful_api.add_resource(ProjectHandler, '/projects/<string:project_id>')
+restful_api.add_resource(MembersHandler, "/projects/<string:project_id>/members")
+restful_api.add_resource(ProjectStatsHandler, "/projects/<string:project_id>/statistics")
+restful_api.add_resource(MemberHandler, "/projects/<string:project_id>/members/<int:member_id>")
+restful_api.add_resource(BillsHandler, "/projects/<string:project_id>/bills")
+restful_api.add_resource(BillHandler, "/projects/<string:project_id>/bills/<int:bill_id>")
diff --git a/ihatemoney/conf-templates/apache-vhost.conf.j2 b/ihatemoney/conf-templates/apache-vhost.conf.j2
index 3246d27..e169589 100644
--- a/ihatemoney/conf-templates/apache-vhost.conf.j2
+++ b/ihatemoney/conf-templates/apache-vhost.conf.j2
@@ -1,8 +1,7 @@
<VirtualHost *:80>
ServerAdmin admin@example.com # CUSTOMIZE
ServerName ihatemoney.example.com # CUSTOMIZE
-
- WSGIDaemonProcess ihatemoney user=www-data group=www-data threads=5 python-path={{ pkg_path }} {% if venv_path %}python-home={{ venv_path }}{% endif %}
+ WSGIDaemonProcess ihatemoney user=www-data group=www-data threads=5 python-home={{ sys_prefix }}
WSGIScriptAlias / {{ pkg_path }}/wsgi.py
WSGIPassAuthorization On
diff --git a/ihatemoney/manage.py b/ihatemoney/manage.py
index 9982890..3207b55 100755
--- a/ihatemoney/manage.py
+++ b/ihatemoney/manage.py
@@ -1,18 +1,17 @@
#!/usr/bin/env python
import os
-import pkgutil
import random
import sys
-from getpass import getpass
+import getpass
from flask_script import Manager, Command, Option
from flask_migrate import Migrate, MigrateCommand
-from jinja2 import Template
from werkzeug.security import generate_password_hash
from ihatemoney.run import create_app
from ihatemoney.models import db
+from ihatemoney.utils import create_jinja_env
class GeneratePasswordHash(Command):
@@ -20,11 +19,11 @@ class GeneratePasswordHash(Command):
"""Get password from user and hash it without printing it in clear text."""
def run(self):
- password = getpass(prompt='Password: ')
+ password = getpass.getpass(prompt='Password: ')
print(generate_password_hash(password))
-class ConfigTemplate(Command):
+class GenerateConfig(Command):
def get_options(self):
return [
Option('config_file', choices=[
@@ -44,16 +43,16 @@ class ConfigTemplate(Command):
for i in range(50)])
def run(self, config_file):
- template_content = pkgutil.get_data(
- 'ihatemoney',
- os.path.join('conf-templates/', config_file) + '.j2'
- ).decode('utf-8')
+ env = create_jinja_env('conf-templates', strict_rendering=True)
+ template = env.get_template('%s.j2' % config_file)
- bin_path = os.path.join(os.path.dirname(sys.executable))
+ bin_path = os.path.dirname(sys.executable)
+ pkg_path = os.path.abspath(os.path.dirname(__file__))
- print(Template(template_content).render(
- pkg_path=os.path.abspath(os.path.dirname(__file__)),
+ print(template.render(
+ pkg_path=pkg_path,
bin_path=bin_path,
+ sys_prefix=sys.prefix,
secret_key=self.gen_secret_key(),
))
@@ -76,7 +75,7 @@ def main():
manager = Manager(app)
manager.add_command('db', MigrateCommand)
manager.add_command('generate_password_hash', GeneratePasswordHash)
- manager.add_command('generate-config', ConfigTemplate)
+ manager.add_command('generate-config', GenerateConfig)
manager.run()
diff --git a/ihatemoney/messages.pot b/ihatemoney/messages.pot
index 0b1759b..4a15259 100644
--- a/ihatemoney/messages.pot
+++ b/ihatemoney/messages.pot
@@ -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:291
+msgid "No token provided"
+msgstr ""
+
+#: web.py:294
+msgid "Invalid token"
msgstr ""
-#: web.py:211
+#: 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/admin.html:10
+msgid "Administration tasks are currently disabled."
+msgstr ""
+
#: templates/authenticate.html:6
-msgid ""
-"The project you are trying to access do not exist, do you want \n"
-"to"
+msgid "The project you are trying to access do not exist, do you want to"
msgstr ""
-#: templates/authenticate.html:7
+#: 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
+#: 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:102
+#: templates/forms.html:129
+msgid "Edit this member"
+msgstr ""
+
+#: templates/forms.html:145
msgid "Send the invitations"
msgstr ""
-#: templates/forms.html:103
+#: 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
-msgid "reactivate"
+#: templates/list_bills.html:63
+msgid "deactivate"
msgstr ""
-#: templates/list_bills.html:88
-msgid "The project identifier is"
+#: templates/list_bills.html:70
+msgid "reactivate"
msgstr ""
-#: templates/list_bills.html:88
-msgid "remember it!"
+#: templates/list_bills.html:82
+msgid "Invite people to join this project!"
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"
-msgstr ""
-
-#: templates/send_invites.html:7
-msgid "Use it!"
+#: templates/reset_password.html:7
+msgid "Reset your password"
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/settle_bills.html:22
+msgid "Who pays?"
msgstr ""
-#: templates/send_invites.html:14
-msgid "skip this step"
+#: templates/settle_bills.html:22
+msgid "To whom?"
msgstr ""
-#: templates/send_invites.html:14
-msgid "and notify them yourself"
+#: templates/statistics.html:21
+msgid "Who?"
msgstr ""
-#: templates/settle_bills.html:31
-msgid "Who pays?"
+#: templates/statistics.html:21
+msgid "Paid"
msgstr ""
-#: templates/settle_bills.html:31
-msgid "To whom?"
+#: templates/statistics.html:21
+msgid "Spent"
+msgstr ""
+
+#: templates/statistics.html:21
+msgid "Balance"
msgstr ""
diff --git a/ihatemoney/models.py b/ihatemoney/models.py
index aa3083d..c6ce23f 100644
--- a/ihatemoney/models.py
+++ b/ihatemoney/models.py
@@ -53,6 +53,26 @@ class Project(db.Model):
return balances
@property
+ def members_stats(self):
+ """Compute what each member has paid
+
+ :return: one stat dict per member
+ :rtype list:
+ """
+ return [{
+ 'member': member,
+ 'paid': sum([
+ bill.amount
+ for bill in self.get_member_bills(member.id).all()
+ ]),
+ 'spent': sum([
+ bill.pay_each() * member.weight
+ for bill in self.get_bills().all() if member in bill.owers
+ ]),
+ 'balance': self.balance[member.id]
+ } for member in self.active_members]
+
+ @property
def uses_weights(self):
return len([i for i in self.members if i.weight != 1]) > 0
diff --git a/ihatemoney/run.py b/ihatemoney/run.py
index b431cb4..6dac233 100644
--- a/ihatemoney/run.py
+++ b/ihatemoney/run.py
@@ -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):
diff --git a/ihatemoney/static/css/main.css b/ihatemoney/static/css/main.css
index b0120ca..94ca4bd 100644
--- a/ihatemoney/static/css/main.css
+++ b/ihatemoney/static/css/main.css
@@ -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;
}
}
diff --git a/ihatemoney/templates/dashboard.html b/ihatemoney/templates/dashboard.html
index b1220bd..807e3e2 100644
--- a/ihatemoney/templates/dashboard.html
+++ b/ihatemoney/templates/dashboard.html
@@ -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>
diff --git a/ihatemoney/templates/invitation_mail.en b/ihatemoney/templates/invitation_mail.en.j2
index eeaafdb..42be0d2 100644
--- a/ihatemoney/templates/invitation_mail.en
+++ b/ihatemoney/templates/invitation_mail.en.j2
@@ -2,10 +2,10 @@ Hi,
Someone using the email address {{ g.project.contact_email }} invited you to share your expenses for "{{ g.project.name }}".
-It's as simple as saying what did you paid for, for who, and how much did it cost you, we are caring about the rest.
+It's as simple as saying what did you pay for, for whom, and how much did it cost you, we are caring about the rest.
You can log in using this link: {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}.
-Once logged in you can use the following link which is easier to remember: {{ url_for(".list_bills", _external=True) }}
+Once logged-in, you can use the following link which is easier to remember: {{ url_for(".list_bills", _external=True) }}
If your cookie gets deleted or if you log out, you will need to log back in using the first link.
Enjoy,
diff --git a/ihatemoney/templates/invitation_mail.fr b/ihatemoney/templates/invitation_mail.fr.j2
index a95f9e9..197edcc 100644
--- a/ihatemoney/templates/invitation_mail.fr
+++ b/ihatemoney/templates/invitation_mail.fr.j2
@@ -1,11 +1,11 @@
Salut,
-Quelqu'un avec l'addresse email "{{ g.project.contact_email }}" vous à invité à partager vos dépenses pour "{{ g.project.name }}".
+Quelqu'un avec l'adresse "{{ g.project.contact_email }}" vous à invité à partager vos dépenses pour "{{ g.project.name }}".
-C'est aussi simple que de dire qui à payé pour quoi, pour qui, et combien celà à coûté, on s'occuppe du reste.
+C'est aussi simple que de dire qui à payé pour quoi, pour qui, et combien celà à coûté, on s’occupe du reste.
Vous pouvez vous authentifier avec le lien suivant: {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}.
Une fois authentifié, vous pouvez utiliser le lien suivant qui est plus facile à mémoriser: {{ url_for(".list_bills", _external=True) }}
-Si votre cookie est supprimé ou si vous vous déconnectez, voous devrez vous réauthentifier en utilisant le premier lien.
+Si votre cookie est supprimé ou si vous vous déconnectez, vous devrez vous authentifier à nouveau en utilisant le premier lien.
Have fun,
diff --git a/ihatemoney/templates/password_reminder.en b/ihatemoney/templates/password_reminder.en.j2
index bc7e609..c654354 100644
--- a/ihatemoney/templates/password_reminder.en
+++ b/ihatemoney/templates/password_reminder.en.j2
@@ -1,8 +1,8 @@
Hi,
-You requested to reset the password of the following project: "{{ project.name }}".
+You requested to reset the password of the following project: "{{ project.name }}".
You can reset it here: {{ url_for(".reset_password", _external=True, token=project.generate_token(expiration=3600)) }}.
-This link is only valid for 1 hour.
+This link is only valid for one hour.
Hope this helps,
Some weird guys (with beards)
diff --git a/ihatemoney/templates/password_reminder.fr b/ihatemoney/templates/password_reminder.fr.j2
index d4fbc2d..d4fbc2d 100644
--- a/ihatemoney/templates/password_reminder.fr
+++ b/ihatemoney/templates/password_reminder.fr.j2
diff --git a/ihatemoney/templates/reminder_mail.en b/ihatemoney/templates/reminder_mail.en.j2
index 8784d2a..8784d2a 100644
--- a/ihatemoney/templates/reminder_mail.en
+++ b/ihatemoney/templates/reminder_mail.en.j2
diff --git a/ihatemoney/templates/reminder_mail.fr b/ihatemoney/templates/reminder_mail.fr.j2
index e73938a..e73938a 100644
--- a/ihatemoney/templates/reminder_mail.fr
+++ b/ihatemoney/templates/reminder_mail.fr.j2
diff --git a/ihatemoney/templates/settle_bills.html b/ihatemoney/templates/settle_bills.html
index b67a9b8..7ec5e29 100644
--- a/ihatemoney/templates/settle_bills.html
+++ b/ihatemoney/templates/settle_bills.html
@@ -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>
diff --git a/ihatemoney/templates/statistics.html b/ihatemoney/templates/statistics.html
index 061c629..1b07a33 100644
--- a/ihatemoney/templates/statistics.html
+++ b/ihatemoney/templates/statistics.html
@@ -3,12 +3,11 @@
{% block sidebar %}
<div id="table_overflow">
<table class="balance table">
- {% set balance = g.project.balance %}
- {% for member in g.project.members | sort(attribute='name') if member.activated or balance[member.id]|round(2) != 0 %}
- <tr id="bal-member-{{ member.id }}" action={% if member.activated %}delete{% else %}reactivate{% endif %}>
- <td class="balance-name">{{ member.name }}</td>
- <td class="balance-value {% if balance[member.id]|round(2) > 0 %}positive{% elif balance[member.id]|round(2) < 0 %}negative{% endif %}">
- {% if balance[member.id]|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }}
+ {% for stat in members_stats| sort(attribute='member.name') %}
+ <tr>
+ <td class="balance-name">{{ stat.member.name }}</td>
+ <td class="balance-value {% if stat.balance|round(2) > 0 %}positive{% elif stat.balance|round(2) < 0 %}negative{% endif %}">
+ {% if stat.balance|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(stat.balance) }}
</td>
</tr>
{% endfor %}
@@ -21,12 +20,12 @@
<table id="bill_table" class="split_bills table table-striped">
<thead><tr><th>{{ _("Who?") }}</th><th>{{ _("Paid") }}</th><th>{{ _("Spent") }}</th><th>{{ _("Balance") }}</th></tr></thead>
<tbody>
- {% for member in members %}
- <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>
diff --git a/ihatemoney/tests/tests.py b/ihatemoney/tests/tests.py
index de53c58..3797f09 100644
--- a/ihatemoney/tests/tests.py
+++ b/ihatemoney/tests/tests.py
@@ -4,6 +4,10 @@ try:
import unittest2 as unittest
except ImportError:
import unittest # NOQA
+try:
+ from unittest.mock import patch
+except ImportError:
+ from mock import patch
import os
import json
@@ -16,6 +20,7 @@ from flask import session
from flask_testing import TestCase
from ihatemoney.run import create_app, db, load_configuration
+from ihatemoney.manage import GenerateConfig, GeneratePasswordHash
from ihatemoney import models
from ihatemoney import utils
@@ -745,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'))
@@ -1048,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
@@ -1134,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={
@@ -1143,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",
@@ -1218,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
@@ -1234,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={
@@ -1247,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",
@@ -1283,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={
@@ -1320,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")
@@ -1406,5 +1445,27 @@ class ServerTestCase(IhatemoneyTestCase):
self.assertStatus(200, req)
+class CommandTestCase(BaseTestCase):
+ def test_generate_config(self):
+ """ Simply checks that all config file generation
+ - raise no exception
+ - produce something non-empty
+ """
+ cmd = GenerateConfig()
+ for config_file in cmd.get_options()[0].kwargs['choices']:
+ with patch('sys.stdout', new=six.StringIO()) as stdout:
+ cmd.run(config_file)
+ print(stdout.getvalue())
+ self.assertNotEqual(len(stdout.getvalue().strip()), 0)
+
+ def test_generate_password_hash(self):
+ cmd = GeneratePasswordHash()
+ with patch('sys.stdout', new=six.StringIO()) as stdout, \
+ patch('getpass.getpass', new=lambda prompt: 'secret'): # NOQA
+ cmd.run()
+ print(stdout.getvalue())
+ self.assertEqual(len(stdout.getvalue().strip()), 187)
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/ihatemoney/translations/fr/LC_MESSAGES/messages.mo b/ihatemoney/translations/fr/LC_MESSAGES/messages.mo
index 47b801d..3fa8d8f 100644
--- a/ihatemoney/translations/fr/LC_MESSAGES/messages.mo
+++ b/ihatemoney/translations/fr/LC_MESSAGES/messages.mo
Binary files differ
diff --git a/ihatemoney/translations/fr/LC_MESSAGES/messages.po b/ihatemoney/translations/fr/LC_MESSAGES/messages.po
index b344098..a4a3e1b 100644
--- a/ihatemoney/translations/fr/LC_MESSAGES/messages.po
+++ b/ihatemoney/translations/fr/LC_MESSAGES/messages.po
@@ -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
+#: forms.py:93
+msgid "Admin password"
+msgstr "Mot de passe administrateur"
+
+#: forms.py:99
+msgid "Send me the code by email"
+msgstr "Envoyez moi le code par email"
+
+#: forms.py:103
+msgid "This project does not exists"
+msgstr "Ce projet n’existe pas"
+
+#: forms.py:108
msgid "Password mismatch"
-msgstr "Les mots de passe fournis ne sont pas les mêmes."
+msgstr "Les mots de passe sont différents"
#: forms.py:109
-msgid "Password confirmation"
-msgstr "Confirmation du mot de passe"
-
-#: forms.py:107
msgid "Password"
msgstr "Mot de passe"
-#: forms.py:108
-msgid "Send me the code by email"
-msgstr "Envoyez moi le code par email"
+#: forms.py:110
+msgid "Password confirmation"
+msgstr "Confirmation du mot de passe"
-#: forms.py:112
-msgid "This project does not exists"
-msgstr "Ce projet n'existe pas"
+#: forms.py:111
+msgid "Reset password"
+msgstr "Réinitialiser le mot de passe"
-#: forms.py:116
+#: 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"
-
-#: forms.py:191
-msgid "Start date"
-msgstr "Date de départ"
-
-#: forms.py:192
-msgid "End date"
-msgstr "Date de fin"
+msgstr "L’email %(email)s est invalide"
-#: 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:129
+msgid "Too many failed login attempts, please retry later."
+msgstr "Trop d'échecs d’authentification successifs, veuillez réessayer plus tard."
-#: web.py:106
+#: 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."
+msgstr ""
+"Le mot de passe administrateur que vous avez entré n’est pas correct. "
+"Plus que %(num)d tentatives."
-#: web.py:106
-msgid "Too many failed login attempts, please retry later."
-msgstr "Trop d'échecs d'authentification successifs, veuillez réessayer plus tard."
+#: 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:147
+#: 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
-msgid "Project successfully deleted"
-msgstr "Projet supprimé"
-
-#: web.py:254
-#, 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:291
+msgid "No token provided"
+msgstr "Aucun token n’a été fourni"
-#: web.py:259
-#, python-format
-msgid ""No token provided""
-msgstr "Aucun token n'a été fourni."
+#: web.py:294
+msgid "Invalid token"
+msgstr "Token invalide"
-#: web.py:259
-#, python-format
+#: web.py:297
msgid "Unknown project"
msgstr "Project inconnu"
-#: web.py:261
-#, python-format
-msgid "Invalid token"
-msgstr "Token invalide"
-
-#: web.py:267
-#, python-format
+#: web.py:303
msgid "Password successfully reset."
msgstr "Le mot de passe a été changé avec succès."
-#: web.py:261
+#: web.py:351
+msgid "Project successfully deleted"
+msgstr "Projet supprimé"
+
+#: 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: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/admin.html:10
+msgid "Administration tasks are currently disabled."
+msgstr "Les tâches d’administration sont actuellement désactivées."
+
#: 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"
+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:7
+#: 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
+#: templates/edit_project.html:15
+msgid "Download this project's data"
+msgstr "Télécharger les données de ce projet"
+
+#: templates/forms.html:27
msgid "Can't remember the password?"
-msgstr "Vous ne vous souvenez plus du code d'accès ?"
+msgstr "Vous ne vous souvenez plus du code d’accès ?"
-#: templates/forms.html:26
+#: templates/forms.html:30
msgid "Cancel"
msgstr "Annuler"
-#: templates/forms.html:68
+#: templates/forms.html:82
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
+#: templates/forms.html:91
msgid "Edit this bill"
msgstr "Éditer cette facture"
-#: templates/forms.html:77 templates/list_bills.html:94
+#: templates/forms.html:91 templates/list_bills.html:89
msgid "Add a bill"
msgstr "Ajouter une facture"
-#: templates/forms.html:95
+#: 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:100
+#: templates/forms.html:129
msgid "Edit this member"
msgstr "Éditer ce participant"
-#: templates/forms.html:102
+#: templates/forms.html:145
msgid "Send the invitations"
msgstr "Envoyer les invitations"
-#: templates/forms.html:103
+#: templates/forms.html:146
msgid "No, thanks"
msgstr "Non merci"
-#: templates/forms.html:136
-msgid "Download this project's data"
-msgstr "Télécharger les données de ce projet"
-
-#: templates/forms.html:136
+#: 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:63
+msgid "deactivate"
+msgstr "désactiver"
-#: templates/list_bills.html:74
+#: 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"
diff --git a/ihatemoney/utils.py b/ihatemoney/utils.py
index 6af0112..5dd1e7b 100644
--- a/ihatemoney/utils.py
+++ b/ihatemoney/utils.py
@@ -2,8 +2,8 @@ import base64
import re
from io import BytesIO, StringIO
-from jinja2 import filters
-from json import dumps
+import jinja2
+from json import dumps, JSONEncoder
from flask import redirect
from werkzeug.routing import HTTPException, RoutingException
import six
@@ -83,7 +83,7 @@ def minimal_round(*args, **kw):
from http://stackoverflow.com/questions/28458524/
"""
# Use the original round filter, to deal with the extra arguments
- res = filters.do_round(*args, **kw)
+ res = jinja2.filters.do_round(*args, **kw)
# Test if the result is equivalent to an integer and
# return depending on it
ires = int(res)
@@ -170,3 +170,43 @@ class LoginThrottler():
def reset(self, ip):
self._attempts.pop(ip, None)
+
+
+def create_jinja_env(folder, strict_rendering=False):
+ """Creates and return a Jinja2 Environment object, used, to load the
+ templates.
+
+ :param strict_rendering:
+ if set to `True`, all templates which use an undefined variable will
+ throw an exception (default to `False`).
+ """
+ loader = jinja2.PackageLoader('ihatemoney', folder)
+ kwargs = {'loader': loader}
+ 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)
diff --git a/ihatemoney/web.py b/ihatemoney/web.py
index e6df385..1e16202 100644
--- a/ihatemoney/web.py
+++ b/ihatemoney/web.py
@@ -242,7 +242,7 @@ def create_project():
message_title = _("You have just created '%(project)s' "
"to share your expenses", project=g.project.name)
- message_body = render_template("reminder_mail.%s" %
+ message_body = render_template("reminder_mail.%s.j2" %
get_locale().language)
msg = Message(message_title,
@@ -273,7 +273,7 @@ def remind_password():
project = Project.query.get(form.id.data)
# send a link to reset the password
- password_reminder = "password_reminder.%s" % get_locale().language
+ password_reminder = "password_reminder.%s.j2" % get_locale().language
current_app.mail.send(Message(
"password recovery",
body=render_template(password_reminder, project=project),
@@ -395,7 +395,7 @@ def invite():
if form.validate():
# send the email
- message_body = render_template("invitation_mail.%s" %
+ message_body = render_template("invitation_mail.%s.j2" %
get_locale().language)
message_title = _("You have been invited to share your "
@@ -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',
)