diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d256bec4..81993f3d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,15 +3,46 @@ Changelog This document describes changes between each past release. -2.1 (unreleased) ----------------- +2.1.1 (unreleased) +------------------ Fixed ===== -- Fix the generation of the supervisord template (#306) +- Regenerate translations (#338) +- Fix broken install with pip ≥ 10 (#340) +- Fix the generation of the supervisord template (#309) - Fix the validation of the hashed password (#310) +Added +===== + +- Document MySQL setup (#357) + +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) ---------------- diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 9a817980..008ae90c 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -9,6 +9,7 @@ Alexis Metaireau Arnaud Bos Baptiste Jonglez Berteh +donkers Feth AREZKI Frédéric Sureau Jocelyn Delalande diff --git a/Dockerfile b/Dockerfile index ba2752a1..cbe4af6e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,15 @@ FROM python:3.6-alpine -RUN mkdir /ihatemoney &&\ +RUN apk add gcc libc-dev libffi-dev openssl-dev &&\ + 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 +26,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"] diff --git a/MANIFEST.in b/MANIFEST.in index 91d0edb2..74ea23b1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include *.rst -recursive-include ihatemoney *.rst *.py *.yaml *.po *.mo *.html *.css *.js *.eot *.svg *.woff *.txt *.png *.ini *.cfg +recursive-include ihatemoney *.rst *.py *.yaml *.po *.mo *.html *.css *.js *.eot *.svg *.woff *.txt *.png *.ini *.cfg *.j2 include LICENSE CONTRIBUTORS CHANGELOG.rst requirements.txt diff --git a/conf/confandrun.sh b/conf/confandrun.sh index e76a8e8a..e37a5739 100755 --- a/conf/confandrun.sh +++ b/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 \ + "$@" diff --git a/dev-requirements.txt b/dev-requirements.txt index 04358ae8..28116fac 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,3 +3,4 @@ tox pytest Flask-Testing Flake8 +mock; python_version < '3.3' diff --git a/docs/api.rst b/docs/api.rst index b82c6f3e..0ae42144 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -164,3 +164,25 @@ And you can of course `DELETE` them at `/api/projects//bills/`:: $ 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//statistics`:: + + $ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/statistics + [ + { + "balance": 12.5, + "member": {"activated": True, "id": 1, "name": "alexis", "weight": 1.0}, + "paid": 25.0, + "spent": 12.5 + }, + { + "balance": -12.5, + "member": {"activated": True, "id": 2, "name": "fred", "weight": 1.0}, + "paid": 0, + "spent": 12.5 + } + ] diff --git a/docs/installation.rst b/docs/installation.rst index 0c3cfac1..4829c5de 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -63,6 +63,22 @@ Once installed, you can start a test server:: And point your browser at `http://localhost:5000 `_. +Configure database with MySQL/MariaDB (optional) +================================================ + +Only required if you prefer MySQL/MariaDB over SQLite. + +1. Install PyMySQL dependencies. On Debian or Ubuntu, that would be:: + + apt install python3-dev libssl-dev + +2. Install PyMySQL (within your virtualenv):: + + pip install 'PyMySQL>=0.9,<0.10' + +3. Create an empty database and a database user +4. Configure :ref:`SQLALCHEMY_DATABASE_URI ` accordingly + Deploy it ========= @@ -154,9 +170,12 @@ 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: Configuration ============= @@ -186,7 +205,7 @@ Production values are recommended values for use in production. | | | **Production value:** `ihatemoney conf-example ihatemoney.cfg` sets it to something | | | | random, which is good. | +-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+ -| MAIL_DEFAULT_SENDER | ``("Budget manager", | A python tuple describing the name and email address to use when sending | +| MAIL_DEFAULT_SENDER | ``("Budget manager", | A python tuple describing the name and email address to use when sending | | | "budget@notmyidea.org")`` | emails. | | | | | | | | **Production value:** Any tuple you want. | diff --git a/ihatemoney/api.py b/ihatemoney/api.py index 827202c8..6068cf72 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 + @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//members", - app=api, - handler=MemberHandler(), - authentifier=check_project) - -bill_resource = RESTResource( - name="bill", - inject_name="project", - route="/projects//bills", - app=api, - handler=BillHandler(), - authentifier=check_project) +restful_api.add_resource(ProjectsHandler, '/projects') +restful_api.add_resource(ProjectHandler, '/projects/') +restful_api.add_resource(MembersHandler, "/projects//members") +restful_api.add_resource(ProjectStatsHandler, "/projects//statistics") +restful_api.add_resource(MemberHandler, "/projects//members/") +restful_api.add_resource(BillsHandler, "/projects//bills") +restful_api.add_resource(BillHandler, "/projects//bills/") diff --git a/ihatemoney/conf-templates/apache-vhost.conf.j2 b/ihatemoney/conf-templates/apache-vhost.conf.j2 index 3246d27f..e169589a 100644 --- a/ihatemoney/conf-templates/apache-vhost.conf.j2 +++ b/ihatemoney/conf-templates/apache-vhost.conf.j2 @@ -1,8 +1,7 @@ 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 99828900..3207b558 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 0b1759b3..4a152593 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 , 2013. +# FIRST AUTHOR , 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 \n" "Language-Team: LANGUAGE \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
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 "" + diff --git a/ihatemoney/models.py b/ihatemoney/models.py index aa3083d6..c6ce23fb 100644 --- a/ihatemoney/models.py +++ b/ihatemoney/models.py @@ -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 diff --git a/ihatemoney/run.py b/ihatemoney/run.py index b431cb48..6dac2330 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 b0120ca9..94ca4bd8 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 b1220bd4..807e3e2f 100644 --- a/ihatemoney/templates/dashboard.html +++ b/ihatemoney/templates/dashboard.html @@ -4,7 +4,7 @@ {% for project in projects|sort(attribute='name') %} - + {% if project.has_bills() %} diff --git a/ihatemoney/templates/invitation_mail.en b/ihatemoney/templates/invitation_mail.en.j2 similarity index 70% rename from ihatemoney/templates/invitation_mail.en rename to ihatemoney/templates/invitation_mail.en.j2 index eeaafdb9..42be0d25 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 similarity index 53% rename from ihatemoney/templates/invitation_mail.fr rename to ihatemoney/templates/invitation_mail.fr.j2 index a95f9e9e..197edcca 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 similarity index 80% rename from ihatemoney/templates/password_reminder.en rename to ihatemoney/templates/password_reminder.en.j2 index bc7e609c..c6543546 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 similarity index 100% rename from ihatemoney/templates/password_reminder.fr rename to ihatemoney/templates/password_reminder.fr.j2 diff --git a/ihatemoney/templates/reminder_mail.en b/ihatemoney/templates/reminder_mail.en.j2 similarity index 100% rename from ihatemoney/templates/reminder_mail.en rename to ihatemoney/templates/reminder_mail.en.j2 diff --git a/ihatemoney/templates/reminder_mail.fr b/ihatemoney/templates/reminder_mail.fr.j2 similarity index 100% rename from ihatemoney/templates/reminder_mail.fr rename to ihatemoney/templates/reminder_mail.fr.j2 diff --git a/ihatemoney/templates/settle_bills.html b/ihatemoney/templates/settle_bills.html index b67a9b85..7ec5e290 100644 --- a/ihatemoney/templates/settle_bills.html +++ b/ihatemoney/templates/settle_bills.html @@ -22,7 +22,7 @@ {% for bill in bills %} - + diff --git a/ihatemoney/templates/statistics.html b/ihatemoney/templates/statistics.html index 061c6299..1b07a33f 100644 --- a/ihatemoney/templates/statistics.html +++ b/ihatemoney/templates/statistics.html @@ -3,12 +3,11 @@ {% block sidebar %}
{{ _("Project") }}{{ _("Number of members") }}{{ _("Number of bills") }}{{_("Newest bill")}}{{_("Oldest bill")}}{{_("Actions")}}
{{ project.name }}{{ project.members | count }}{{ project.get_bills().count() }}{{ project.get_bills().all()[0].date }}
{{ _("Who pays?") }}{{ _("To whom?") }}{{ _("How much?") }}
{{ bill.ower }} {{ bill.receiver }} {{ "%0.2f"|format(bill.amount) }}
- {% set balance = g.project.balance %} - {% for member in g.project.members | sort(attribute='name') if member.activated or balance[member.id]|round(2) != 0 %} - - - + + {% endfor %} @@ -21,12 +20,12 @@
{{ member.name }} - {% if balance[member.id]|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }} + {% for stat in members_stats| sort(attribute='member.name') %} +
{{ stat.member.name }} + {% if stat.balance|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(stat.balance) }}
- {% for member in members %} - - - - - + {% for stat in members_stats %} + + + + + {% endfor %} diff --git a/ihatemoney/tests/tests.py b/ihatemoney/tests/tests.py index de53c584..3797f09d 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("\n " - + "\n " - + "\n " + self.assertIn("\n " + + "\n " + + "\n " + "\n", response.data.decode('utf-8')) - self.assertIn("\n " - + "\n " - + "\n " + self.assertIn("\n " + + "\n " + + "\n " + "\n", response.data.decode('utf-8')) - self.assertIn("\n " - + "\n " - + "\n " + self.assertIn("\n " + + "\n " + + "\n " + "\n", response.data.decode('utf-8')) - self.assertIn("\n " - + "\n " - + "\n " + self.assertIn("\n " + + "\n " + + "\n " + "\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 47b801d4..3fa8d8f4 100644 Binary files a/ihatemoney/translations/fr/LC_MESSAGES/messages.mo and b/ihatemoney/translations/fr/LC_MESSAGES/messages.mo differ diff --git a/ihatemoney/translations/fr/LC_MESSAGES/messages.po b/ihatemoney/translations/fr/LC_MESSAGES/messages.po index b3440987..a4a3e1b8 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 , 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 \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 \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
expenses, easily" -msgstr "Gérez vos dépenses
partagées, facilement" +msgstr "Gérez vos dépenses
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" diff --git a/ihatemoney/utils.py b/ihatemoney/utils.py index 6af0112c..5dd1e7ba 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 e6df385a..1e162024 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("//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', ) diff --git a/requirements.txt b/requirements.txt index 64610abd..c2fe5348 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.py b/setup.py index 6531286f..974a70f7 100644 --- a/setup.py +++ b/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) diff --git a/tox.ini b/tox.ini index ddef0a1a..15c2b40f 100644 --- a/tox.ini +++ b/tox.ini @@ -6,22 +6,31 @@ skip_missing_interpreters = True commands = python --version - py.test ihatemoney/tests/tests.py + py.test --pyargs ihatemoney.tests.tests + deps = -rdev-requirements.txt -rrequirements.txt -install_command = pip install --pre {opts} {packages} +# To be sure we are importing ihatemoney pkg from pip-installed version +changedir = /tmp + +install_command = + pip install --pre {opts} {packages} + pip install . + [testenv:docs] commands = sphinx-build -a -n -b html -d docs/_build/doctrees docs docs/_build/html deps = -rdocs/requirements.txt +changedir = {toxinidir} [testenv:lint] commands = flake8 ihatemoney deps = -rdev-requirements.txt +changedir = {toxinidir} [flake8] exclude = migrations
{{ _("Who?") }}{{ _("Paid") }}{{ _("Spent") }}{{ _("Balance") }}
{{ member.name }}{{ "%0.2f"|format(paid[member.id]) }}{{ "%0.2f"|format(spent[member.id]) }}{{ "%0.2f"|format(balance[member.id]) }}
{{ stat.member.name }}{{ "%0.2f"|format(stat.paid) }}{{ "%0.2f"|format(stat.spent) }}{{ "%0.2f"|format(stat.balance) }}
alexis20.0031.67alexis20.0031.67-11.67fred20.005.83fred20.005.8314.17tata0.002.50tata0.002.50-2.50toto0.000.00toto0.000.000.00