aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--MANIFEST.in2
-rw-r--r--Makefile4
-rw-r--r--budget/default_settings.py14
-rw-r--r--budget/run.py121
-rw-r--r--budget/wsgi.py1
-rw-r--r--dev-requirements.txt2
-rw-r--r--docs/installation.rst11
-rw-r--r--ihatemoney/__init__.py (renamed from budget/__init__.py)0
-rw-r--r--ihatemoney/api.py (renamed from budget/api.py)6
-rw-r--r--ihatemoney/babel.cfg (renamed from budget/babel.cfg)0
-rw-r--r--ihatemoney/default_settings.py31
-rw-r--r--ihatemoney/forms.py (renamed from budget/forms.py)58
-rwxr-xr-xihatemoney/manage.py (renamed from budget/manage.py)19
-rw-r--r--ihatemoney/messages.pot (renamed from budget/messages.pot)0
-rwxr-xr-xihatemoney/migrations/README (renamed from budget/migrations/README)0
-rw-r--r--ihatemoney/migrations/alembic.ini (renamed from budget/migrations/alembic.ini)0
-rwxr-xr-xihatemoney/migrations/env.py (renamed from budget/migrations/env.py)0
-rwxr-xr-xihatemoney/migrations/script.py.mako (renamed from budget/migrations/script.py.mako)0
-rw-r--r--ihatemoney/migrations/versions/26d6a218c329_.py (renamed from budget/migrations/versions/26d6a218c329_.py)0
-rw-r--r--ihatemoney/migrations/versions/b9a10d5d63ce_.py (renamed from budget/migrations/versions/b9a10d5d63ce_.py)0
-rw-r--r--ihatemoney/migrations/versions/f629c8ef4ab0_initialize_all_members_weights_to_1.py (renamed from budget/migrations/versions/f629c8ef4ab0_initialize_all_members_weights_to_1.py)0
-rw-r--r--ihatemoney/models.py (renamed from budget/models.py)85
-rw-r--r--ihatemoney/run.py144
-rw-r--r--ihatemoney/static/css/bootstrap-datepicker3.standalone.css (renamed from budget/static/css/bootstrap-datepicker3.standalone.css)0
-rw-r--r--ihatemoney/static/css/bootstrap.min.css (renamed from budget/static/css/bootstrap.min.css)0
-rw-r--r--ihatemoney/static/css/main.css (renamed from budget/static/css/main.css)0
-rw-r--r--ihatemoney/static/fonts/OFL.txt (renamed from budget/static/fonts/OFL.txt)0
-rw-r--r--ihatemoney/static/fonts/comfortaa-regular-webfont.eot (renamed from budget/static/fonts/comfortaa-regular-webfont.eot)bin40370 -> 40370 bytes
-rw-r--r--ihatemoney/static/fonts/comfortaa-regular-webfont.svg (renamed from budget/static/fonts/comfortaa-regular-webfont.svg)0
-rw-r--r--ihatemoney/static/fonts/comfortaa-regular-webfont.woff (renamed from budget/static/fonts/comfortaa-regular-webfont.woff)bin20920 -> 20920 bytes
-rw-r--r--ihatemoney/static/fonts/fontfaces.css (renamed from budget/static/fonts/fontfaces.css)0
-rw-r--r--ihatemoney/static/fonts/lobster-webfont.eot (renamed from budget/static/fonts/lobster-webfont.eot)bin63744 -> 63744 bytes
-rw-r--r--ihatemoney/static/fonts/lobster-webfont.svg (renamed from budget/static/fonts/lobster-webfont.svg)0
-rw-r--r--ihatemoney/static/fonts/lobster-webfont.woff (renamed from budget/static/fonts/lobster-webfont.woff)bin33380 -> 33380 bytes
-rw-r--r--ihatemoney/static/images/delete.png (renamed from budget/static/images/delete.png)bin274 -> 274 bytes
-rw-r--r--ihatemoney/static/images/deleter.png (renamed from budget/static/images/deleter.png)bin226 -> 226 bytes
-rw-r--r--ihatemoney/static/images/edit.png (renamed from budget/static/images/edit.png)bin258 -> 258 bytes
-rw-r--r--ihatemoney/static/images/glyphicons-halflings-white.png (renamed from budget/static/images/glyphicons-halflings-white.png)bin4352 -> 4352 bytes
-rw-r--r--ihatemoney/static/images/glyphicons-halflings.png (renamed from budget/static/images/glyphicons-halflings.png)bin4352 -> 4352 bytes
-rw-r--r--ihatemoney/static/images/gradient.png (renamed from budget/static/images/gradient.png)bin24656 -> 24656 bytes
-rw-r--r--ihatemoney/static/images/reactivate.png (renamed from budget/static/images/reactivate.png)bin259 -> 259 bytes
-rw-r--r--ihatemoney/static/js/bootstrap-datepicker.js (renamed from budget/static/js/bootstrap-datepicker.js)0
-rw-r--r--ihatemoney/static/js/bootstrap.min.js (renamed from budget/static/js/bootstrap.min.js)0
-rw-r--r--ihatemoney/static/js/ihatemoney.js (renamed from budget/static/js/ihatemoney.js)0
-rw-r--r--ihatemoney/static/js/jquery-3.1.1.min.js (renamed from budget/static/js/jquery-3.1.1.min.js)0
-rw-r--r--ihatemoney/static/js/locales/bootstrap-datepicker.fr.min.js (renamed from budget/static/js/locales/bootstrap-datepicker.fr.min.js)0
-rw-r--r--ihatemoney/static/js/tether.min.js (renamed from budget/static/js/tether.min.js)0
-rw-r--r--ihatemoney/templates/add_bill.html (renamed from budget/templates/add_bill.html)0
-rw-r--r--ihatemoney/templates/add_member.html (renamed from budget/templates/add_member.html)0
-rw-r--r--ihatemoney/templates/authenticate.html (renamed from budget/templates/authenticate.html)0
-rw-r--r--ihatemoney/templates/create_project.html (renamed from budget/templates/create_project.html)0
-rw-r--r--ihatemoney/templates/dashboard.html (renamed from budget/templates/dashboard.html)0
-rw-r--r--ihatemoney/templates/debug.html (renamed from budget/templates/debug.html)0
-rw-r--r--ihatemoney/templates/display_errors.html (renamed from budget/templates/display_errors.html)0
-rw-r--r--ihatemoney/templates/edit_member.html (renamed from budget/templates/edit_member.html)0
-rw-r--r--ihatemoney/templates/edit_project.html (renamed from budget/templates/edit_project.html)0
-rw-r--r--ihatemoney/templates/forms.html (renamed from budget/templates/forms.html)0
-rw-r--r--ihatemoney/templates/home.html (renamed from budget/templates/home.html)0
-rw-r--r--ihatemoney/templates/invitation_mail.en (renamed from budget/templates/invitation_mail.en)0
-rw-r--r--ihatemoney/templates/invitation_mail.fr (renamed from budget/templates/invitation_mail.fr)0
-rw-r--r--ihatemoney/templates/layout.html (renamed from budget/templates/layout.html)0
-rw-r--r--ihatemoney/templates/list_bills.html (renamed from budget/templates/list_bills.html)0
-rw-r--r--ihatemoney/templates/password_reminder.en (renamed from budget/templates/password_reminder.en)0
-rw-r--r--ihatemoney/templates/password_reminder.fr (renamed from budget/templates/password_reminder.fr)0
-rw-r--r--ihatemoney/templates/password_reminder.html (renamed from budget/templates/password_reminder.html)0
-rw-r--r--ihatemoney/templates/recent_projects.html (renamed from budget/templates/recent_projects.html)0
-rw-r--r--ihatemoney/templates/reminder_mail.en (renamed from budget/templates/reminder_mail.en)0
-rw-r--r--ihatemoney/templates/reminder_mail.fr (renamed from budget/templates/reminder_mail.fr)0
-rw-r--r--ihatemoney/templates/send_invites.html (renamed from budget/templates/send_invites.html)0
-rw-r--r--ihatemoney/templates/settle_bills.html (renamed from budget/templates/settle_bills.html)0
-rw-r--r--ihatemoney/templates/sidebar_table_layout.html (renamed from budget/templates/sidebar_table_layout.html)0
-rw-r--r--ihatemoney/tests/__init__.py (renamed from budget/tests/__init__.py)0
-rw-r--r--ihatemoney/tests/ihatemoney.cfg (renamed from budget/tests/ihatemoney.cfg)0
-rw-r--r--ihatemoney/tests/ihatemoney_envvar.cfg (renamed from budget/tests/ihatemoney_envvar.cfg)0
-rw-r--r--ihatemoney/tests/tests.py (renamed from budget/tests/tests.py)536
-rw-r--r--ihatemoney/translations/fr/LC_MESSAGES/messages.mo (renamed from budget/translations/fr/LC_MESSAGES/messages.mo)bin8425 -> 8425 bytes
-rw-r--r--ihatemoney/translations/fr/LC_MESSAGES/messages.po (renamed from budget/translations/fr/LC_MESSAGES/messages.po)0
-rw-r--r--ihatemoney/utils.py (renamed from budget/utils.py)17
-rw-r--r--ihatemoney/web.py (renamed from budget/web.py)65
-rw-r--r--ihatemoney/wsgi.py3
-rw-r--r--setup.py5
-rw-r--r--tox.ini13
82 files changed, 613 insertions, 524 deletions
diff --git a/MANIFEST.in b/MANIFEST.in
index ba99e2a..9ba34ba 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,3 @@
include *.rst
-recursive-include budget *.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
include LICENSE CONTRIBUTORS CHANGELOG.rst
diff --git a/Makefile b/Makefile
index f8f87d8..6b32e48 100644
--- a/Makefile
+++ b/Makefile
@@ -29,7 +29,7 @@ remove-install-stamp:
update: remove-install-stamp install
serve: install
- $(PYTHON) -m budget.manage runserver
+ $(PYTHON) -m ihatemoney.manage runserver
test: $(DEV_STAMP)
$(VENV)/bin/tox
@@ -38,7 +38,7 @@ release: $(DEV_STAMP)
$(VENV)/bin/fullrelease
build-translations:
- $(VENV)/bin/pybabel compile -d budget/translations
+ $(VENV)/bin/pybabel compile -d ihatemoney/translations
build-requirements:
$(VIRTUALENV) $(TEMPDIR)
diff --git a/budget/default_settings.py b/budget/default_settings.py
deleted file mode 100644
index 15fe9cd..0000000
--- a/budget/default_settings.py
+++ /dev/null
@@ -1,14 +0,0 @@
-DEBUG = False
-SQLALCHEMY_DATABASE_URI = 'sqlite:///budget.db'
-SQLACHEMY_ECHO = DEBUG
-# Will likely become the default value in flask-sqlalchemy >=3 ; could be removed
-# then:
-SQLALCHEMY_TRACK_MODIFICATIONS = False
-
-SECRET_KEY = "tralala"
-
-MAIL_DEFAULT_SENDER = ("Budget manager", "budget@notmyidea.org")
-
-ACTIVATE_DEMO_PROJECT = True
-
-ADMIN_PASSWORD = ""
diff --git a/budget/run.py b/budget/run.py
deleted file mode 100644
index 5e65c90..0000000
--- a/budget/run.py
+++ /dev/null
@@ -1,121 +0,0 @@
-import os
-import os.path
-import warnings
-
-from flask import Flask, g, request, session
-from flask_babel import Babel
-from flask_migrate import Migrate, upgrade, stamp
-from raven.contrib.flask import Sentry
-
-from .web import main, db, mail
-from .api import api
-from .utils import PrefixedWSGI
-from .utils import minimal_round
-
-from . import default_settings
-
-app = Flask(__name__, instance_path='/etc/ihatemoney', instance_relative_config=True)
-
-
-def pre_alembic_db():
- """ Checks if we are migrating from a pre-alembic ihatemoney
- """
- con = db.engine.connect()
- tables_exist = db.engine.dialect.has_table(con, 'project')
- alembic_setup = db.engine.dialect.has_table(con, 'alembic_version')
- return tables_exist and not alembic_setup
-
-
-def configure():
- """ A way to (re)configure the app, specially reset the settings
- """
- default_config_file = os.path.join(app.root_path, 'default_settings.py')
- config_file = os.environ.get('IHATEMONEY_SETTINGS_FILE_PATH')
-
- # Load default settings first
- # Then load the settings from the path set in IHATEMONEY_SETTINGS_FILE_PATH var
- # If not set, default to /etc/ihatemoney/ihatemoney.cfg
- # If the latter doesn't exist no error is raised and the default settings are used
- app.config.from_pyfile(default_config_file)
- if config_file:
- app.config.from_pyfile(config_file)
- else:
- app.config.from_pyfile('ihatemoney.cfg', silent=True)
- app.wsgi_app = PrefixedWSGI(app)
-
- if app.config['SECRET_KEY'] == default_settings.SECRET_KEY:
- warnings.warn(
- "Running a server without changing the SECRET_KEY can lead to"
- + " user impersonation. Please update your configuration file.",
- UserWarning
- )
- # Deprecations
- if 'DEFAULT_MAIL_SENDER' in app.config:
- # Since flask-mail 0.8
- warnings.warn(
- "DEFAULT_MAIL_SENDER is deprecated in favor of MAIL_DEFAULT_SENDER"
- + " and will be removed in further version",
- UserWarning
- )
- if not 'MAIL_DEFAULT_SENDER' in app.config:
- app.config['MAIL_DEFAULT_SENDER'] = DEFAULT_MAIL_SENDER
-
- if "pbkdf2:sha256:" not in app.config['ADMIN_PASSWORD'] and app.config['ADMIN_PASSWORD']:
- # Since 2.0
- warnings.warn(
- "The way Ihatemoney stores your ADMIN_PASSWORD has changed. You are using an unhashed"
- +" ADMIN_PASSWORD, which is not supported anymore and won't let you access your admin"
- +" endpoints. Please use the command './budget/manage.py generate_password_hash'"
- +" to generate a proper password HASH and copy the output to the value of"
- +" ADMIN_PASSWORD in your settings file.",
- UserWarning
- )
-
-configure()
-
-
-app.register_blueprint(main)
-app.register_blueprint(api)
-
-# custom jinja2 filters
-app.jinja_env.filters['minimal_round'] = minimal_round
-
-# db
-db.init_app(app)
-db.app = app
-
-# db migrations
-migrate = Migrate(app, db)
-migrations_path = os.path.join(app.root_path, 'migrations')
-
-if pre_alembic_db():
- with app.app_context():
- # fake the first migration
- stamp(migrations_path, revision='b9a10d5d63ce')
-
-# auto-execute migrations on runtime
-with app.app_context():
- upgrade(migrations_path)
-
-# mail
-mail.init_app(app)
-
-# translations
-babel = Babel(app)
-
-# sentry
-sentry = Sentry(app)
-
-@babel.localeselector
-def get_locale():
- # get the lang from the session if defined, fallback on the browser "accept
- # languages" header.
- lang = session.get('lang', request.accept_languages.best_match(['fr', 'en']))
- setattr(g, 'lang', lang)
- return lang
-
-def main():
- app.run(host="0.0.0.0", debug=True)
-
-if __name__ == '__main__':
- main()
diff --git a/budget/wsgi.py b/budget/wsgi.py
deleted file mode 100644
index 66f7a73..0000000
--- a/budget/wsgi.py
+++ /dev/null
@@ -1 +0,0 @@
-from run import app as application
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 8795457..04358ae 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,3 +1,5 @@
zest.releaser
tox
pytest
+Flask-Testing
+Flake8
diff --git a/docs/installation.rst b/docs/installation.rst
index 3cd143d..d1fd9bc 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -29,7 +29,7 @@ dependencies yourself (that's what the `make serve` does). That would be::
And then run the application::
- cd budget
+ cd ihatemoney
python run.py
In any case, you can point your browser at `http://localhost:5000 <http://localhost:5000>`_.
@@ -76,7 +76,8 @@ properly.
| Setting name | Default | What does it do? |
+============================+===========================+========================================================================================+
| SQLALCHEMY_DATABASE_URI | ``sqlite:///budget.db`` | Specifies the type of backend to use and its location. More information |
-| | | on the format used can be found on `the SQLAlchemy documentation`. |
+| | | on the format used can be found on `the SQLAlchemy documentation |
+| | | <http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls>`_. |
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+
| SECRET_KEY | ``tralala`` | The secret key used to encrypt the cookies. **This needs to be changed**. |
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+
@@ -86,16 +87,14 @@ properly.
| ACTIVATE_DEMO_PROJECT | ``True`` | If set to `True`, a demo project will be available on the frontpage. |
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+
| | ``""`` | If not empty, the specified password must be entered to create new projects. |
-| ADMIN_PASSWORD | | To generate the proper password HASH, use ``./budget/manage.py generate_password_hash``|
+| ADMIN_PASSWORD | | To generate the proper password HASH, use ``ihatemoney generate_password_hash`` |
| | | and copy its output into the value of *ADMIN_PASSWORD*. |
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+
-.. _`the SQLAlechemy documentation`: http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls
-
In a production environment
---------------------------
-Make a copy of ``budget/default_settings.py`` and name it ``ihatemoney.cfg``.
+Make a copy of ``ihatemoney/default_settings.py`` and name it ``ihatemoney.cfg``.
Then adjust the settings to your needs and move this file to
``/etc/ihatemoney/ihatemoney.cfg``.
diff --git a/budget/__init__.py b/ihatemoney/__init__.py
index e69de29..e69de29 100644
--- a/budget/__init__.py
+++ b/ihatemoney/__init__.py
diff --git a/budget/api.py b/ihatemoney/api.py
index 7ce6a34..a34fa12 100644
--- a/budget/api.py
+++ b/ihatemoney/api.py
@@ -2,9 +2,9 @@
from flask import Blueprint, request
from flask_rest import RESTResource, need_auth
-from .models import db, Project, Person, Bill
-from .forms import (ProjectForm, EditProjectForm, MemberForm,
- get_billform_for)
+from ihatemoney.models import db, Project, Person, Bill
+from ihatemoney.forms import (ProjectForm, EditProjectForm, MemberForm,
+ get_billform_for)
api = Blueprint("api", __name__, url_prefix="/api")
diff --git a/budget/babel.cfg b/ihatemoney/babel.cfg
index f0234b3..f0234b3 100644
--- a/budget/babel.cfg
+++ b/ihatemoney/babel.cfg
diff --git a/ihatemoney/default_settings.py b/ihatemoney/default_settings.py
new file mode 100644
index 0000000..4ca11ca
--- /dev/null
+++ b/ihatemoney/default_settings.py
@@ -0,0 +1,31 @@
+# You can find more information about what these settings mean in the
+# documentation, available online at
+# http://ihatemoney.readthedocs.io/en/latest/installation.html#configuration
+
+# Turn this on if you want to have more output on what's happening under the
+# hood.
+DEBUG = False
+
+# The database URI, reprensenting the type of database and how to connect to it.
+# Enter an absolute path here.
+SQLALCHEMY_DATABASE_URI = 'sqlite://'
+SQLACHEMY_ECHO = DEBUG
+
+# Will likely become the default value in flask-sqlalchemy >=3 ; could be removed
+# then:
+SQLALCHEMY_TRACK_MODIFICATIONS = False
+
+# You need to change this secret key, otherwise bad things might happen to your
+# users.
+SECRET_KEY = "tralala"
+
+# A python tuple describing the name and email adress of the sender of the mails.
+MAIL_DEFAULT_SENDER = ("Budget manager", "budget@notmyidea.org")
+
+# If set to True, a demonstration project will be activated.
+ACTIVATE_DEMO_PROJECT = True
+
+# If not empty, the specified password must be entered to create new projects.
+# DO NOT enter the password in cleartext. Generate a password hash with
+# "ihatemoney generate_password_hash" instead.
+ADMIN_PASSWORD = ""
diff --git a/budget/forms.py b/ihatemoney/forms.py
index bd7fc5b..ead5586 100644
--- a/budget/forms.py
+++ b/ihatemoney/forms.py
@@ -6,12 +6,12 @@ from wtforms.validators import Email, Required, ValidationError
from flask_babel import lazy_gettext as _
from flask import request
-from wtforms.widgets import html_params
from datetime import datetime
from jinja2 import Markup
-from .models import Project, Person
-from .utils import slugify
+from ihatemoney.models import Project, Person
+from ihatemoney.utils import slugify
+
def get_billform_for(project, set_default=True, **kwargs):
"""Return an instance of BillForm configured for a particular project.
@@ -21,8 +21,9 @@ def get_billform_for(project, set_default=True, **kwargs):
"""
form = BillForm(**kwargs)
- form.payed_for.choices = form.payer.choices = [(m.id, m.name)
- for m in project.active_members]
+ active_members = [(m.id, m.name) for m in project.active_members]
+
+ form.payed_for.choices = form.payer.choices = active_members
form.payed_for.default = [m.id for m in project.active_members]
if set_default and request.method == "GET":
@@ -31,7 +32,9 @@ def get_billform_for(project, set_default=True, **kwargs):
class CommaDecimalField(DecimalField):
+
"""A class to deal with comma in Decimal Field"""
+
def process_formdata(self, value):
if value:
value[0] = str(value[0]).replace(',', '.')
@@ -49,8 +52,8 @@ class EditProjectForm(FlaskForm):
Returns the created instance
"""
project = Project(name=self.name.data, id=self.id.data,
- password=self.password.data,
- contact_email=self.contact_email.data)
+ password=self.password.data,
+ contact_email=self.contact_email.data)
return project
def update(self, project):
@@ -70,12 +73,13 @@ class ProjectForm(EditProjectForm):
def validate_id(form, field):
form.id.data = slugify(field.data)
if (form.id.data == "dashboard") or Project.query.get(form.id.data):
- raise ValidationError(Markup(_("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.")))
+ message = _("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")
+ raise ValidationError(Markup(message))
class AuthenticationForm(FlaskForm):
@@ -104,7 +108,7 @@ class BillForm(FlaskForm):
payer = SelectField(_("Payer"), validators=[Required()], coerce=int)
amount = CommaDecimalField(_("Amount paid"), validators=[Required()])
payed_for = SelectMultipleField(_("For whom?"),
- validators=[Required()], coerce=int)
+ validators=[Required()], coerce=int)
submit = SubmitField(_("Submit"))
submit2 = SubmitField(_("Submit and add a new one"))
@@ -114,7 +118,7 @@ class BillForm(FlaskForm):
bill.what = self.what.data
bill.date = self.date.data
bill.owers = [Person.query.get(ower, project)
- for ower in self.payed_for.data]
+ for ower in self.payed_for.data]
return bill
@@ -150,7 +154,7 @@ class MemberForm(FlaskForm):
if (not form.edit and Person.query.filter(
Person.name == field.data,
Person.project == form.project,
- Person.activated == True).all()):
+ Person.activated == True).all()): # NOQA
raise ValidationError(_("This project already have this member"))
def save(self, project, person):
@@ -175,17 +179,17 @@ class InviteForm(FlaskForm):
for email in [email.strip() for email in form.emails.data.split(",")]:
if not validator.regex.match(email):
raise ValidationError(_("The email %(email)s is not valid",
- email=email))
+ email=email))
class ExportForm(FlaskForm):
- export_type = SelectField(_("What do you want to download ?"),
- validators=[Required()],
- coerce=str,
- choices=[("bills", _("bills")), ("transactions", _("transactions"))]
- )
- export_format = SelectField(_("Export file format"),
- validators=[Required()],
- coerce=str,
- choices=[("csv", "csv"), ("json", "json")]
- )
+ export_type = SelectField(
+ _("What do you want to download ?"),
+ validators=[Required()],
+ coerce=str,
+ choices=[("bills", _("bills")), ("transactions", _("transactions"))])
+ export_format = SelectField(
+ _("Export file format"),
+ validators=[Required()],
+ coerce=str,
+ choices=[("csv", "csv"), ("json", "json")])
diff --git a/budget/manage.py b/ihatemoney/manage.py
index 0a66284..6f63a98 100755
--- a/budget/manage.py
+++ b/ihatemoney/manage.py
@@ -5,25 +5,26 @@ from flask_script import Manager, Command
from flask_migrate import Migrate, MigrateCommand
from werkzeug.security import generate_password_hash
-from .run import app
-from .models import db
+from ihatemoney.run import create_app
+from ihatemoney.models import db
class GeneratePasswordHash(Command):
- "Get password from user and hash it without printing it in clear text"
+
+ """Get password from user and hash it without printing it in clear text."""
def run(self):
password = getpass(prompt='Password: ')
print(generate_password_hash(password))
-migrate = Migrate(app, db)
-
-manager = Manager(app)
-manager.add_command('db', MigrateCommand)
-manager.add_command('generate_password_hash', GeneratePasswordHash)
-
def main():
+ app = create_app()
+ Migrate(app, db)
+
+ manager = Manager(app)
+ manager.add_command('db', MigrateCommand)
+ manager.add_command('generate_password_hash', GeneratePasswordHash)
manager.run()
diff --git a/budget/messages.pot b/ihatemoney/messages.pot
index 0b1759b..0b1759b 100644
--- a/budget/messages.pot
+++ b/ihatemoney/messages.pot
diff --git a/budget/migrations/README b/ihatemoney/migrations/README
index 98e4f9c..98e4f9c 100755
--- a/budget/migrations/README
+++ b/ihatemoney/migrations/README
diff --git a/budget/migrations/alembic.ini b/ihatemoney/migrations/alembic.ini
index f8ed480..f8ed480 100644
--- a/budget/migrations/alembic.ini
+++ b/ihatemoney/migrations/alembic.ini
diff --git a/budget/migrations/env.py b/ihatemoney/migrations/env.py
index e2f9a28..e2f9a28 100755
--- a/budget/migrations/env.py
+++ b/ihatemoney/migrations/env.py
diff --git a/budget/migrations/script.py.mako b/ihatemoney/migrations/script.py.mako
index 9570201..9570201 100755
--- a/budget/migrations/script.py.mako
+++ b/ihatemoney/migrations/script.py.mako
diff --git a/budget/migrations/versions/26d6a218c329_.py b/ihatemoney/migrations/versions/26d6a218c329_.py
index 859b9af..859b9af 100644
--- a/budget/migrations/versions/26d6a218c329_.py
+++ b/ihatemoney/migrations/versions/26d6a218c329_.py
diff --git a/budget/migrations/versions/b9a10d5d63ce_.py b/ihatemoney/migrations/versions/b9a10d5d63ce_.py
index 92bb446..92bb446 100644
--- a/budget/migrations/versions/b9a10d5d63ce_.py
+++ b/ihatemoney/migrations/versions/b9a10d5d63ce_.py
diff --git a/budget/migrations/versions/f629c8ef4ab0_initialize_all_members_weights_to_1.py b/ihatemoney/migrations/versions/f629c8ef4ab0_initialize_all_members_weights_to_1.py
index 5542146..5542146 100644
--- a/budget/migrations/versions/f629c8ef4ab0_initialize_all_members_weights_to_1.py
+++ b/ihatemoney/migrations/versions/f629c8ef4ab0_initialize_all_members_weights_to_1.py
diff --git a/budget/models.py b/ihatemoney/models.py
index 1da4489..6c71a57 100644
--- a/budget/models.py
+++ b/ihatemoney/models.py
@@ -9,13 +9,12 @@ from sqlalchemy import orm
db = SQLAlchemy()
-# define models
-
-
class Project(db.Model):
- _to_serialize = ("id", "name", "password", "contact_email",
- "members", "active_members", "balance")
+ _to_serialize = (
+ "id", "name", "password", "contact_email", "members", "active_members",
+ "balance"
+ )
id = db.Column(db.String(64), primary_key=True)
@@ -32,12 +31,13 @@ class Project(db.Model):
def balance(self):
balances, should_pay, should_receive = (defaultdict(int)
- for time in (1, 2, 3))
+ for time in (1, 2, 3))
# for each person
for person in self.members:
# get the list of bills he has to pay
- bills = Bill.query.options(orm.subqueryload(Bill.owers)).filter(Bill.owers.contains(person))
+ bills = Bill.query.options(orm.subqueryload(Bill.owers)).filter(
+ Bill.owers.contains(person))
for bill in bills.all():
if person != bill.payer:
share = bill.pay_each() * person.weight
@@ -56,6 +56,7 @@ class Project(db.Model):
def get_transactions_to_settle_bill(self, pretty_output=False):
"""Return a list of transactions that could be made to settle the bill"""
+
def prettify(transactions, pretty_output):
""" Return pretty transactions
"""
@@ -63,36 +64,52 @@ class Project(db.Model):
return transactions
pretty_transactions = []
for transaction in transactions:
- pretty_transactions.append({'ower': transaction['ower'].name,
- 'receiver': transaction['receiver'].name,
- 'amount': round(transaction['amount'], 2)})
+ pretty_transactions.append({
+ 'ower': transaction['ower'].name,
+ 'receiver': transaction['receiver'].name,
+ 'amount': round(transaction['amount'], 2)
+ })
return pretty_transactions
- #cache value for better performance
+ # cache value for better performance
balance = self.balance
- credits, debts, transactions = [],[],[]
+ credits, debts, transactions = [], [], []
# Create lists of credits and debts
for person in self.members:
if round(balance[person.id], 2) > 0:
credits.append({"person": person, "balance": balance[person.id]})
elif round(balance[person.id], 2) < 0:
debts.append({"person": person, "balance": -balance[person.id]})
+
# Try and find exact matches
for credit in credits:
match = self.exactmatch(round(credit["balance"], 2), debts)
if match:
for m in match:
- transactions.append({"ower": m["person"], "receiver": credit["person"], "amount": m["balance"]})
+ transactions.append({
+ "ower": m["person"],
+ "receiver": credit["person"],
+ "amount": m["balance"]
+ })
debts.remove(m)
credits.remove(credit)
# Split any remaining debts & credits
while credits and debts:
+
if credits[0]["balance"] > debts[0]["balance"]:
- transactions.append({"ower": debts[0]["person"], "receiver": credits[0]["person"], "amount": debts[0]["balance"]})
+ transactions.append({
+ "ower": debts[0]["person"],
+ "receiver": credits[0]["person"],
+ "amount": debts[0]["balance"]
+ })
credits[0]["balance"] = credits[0]["balance"] - debts[0]["balance"]
del debts[0]
else:
- transactions.append({"ower": debts[0]["person"], "receiver": credits[0]["person"], "amount": credits[0]["balance"]})
+ transactions.append({
+ "ower": debts[0]["person"],
+ "receiver": credits[0]["person"],
+ "amount": credits[0]["balance"]
+ })
debts[0]["balance"] = debts[0]["balance"] - credits[0]["balance"]
del credits[0]
@@ -107,7 +124,7 @@ class Project(db.Model):
elif debts[0]["balance"] == credit:
return [debts[0]]
else:
- match = self.exactmatch(credit-debts[0]["balance"], debts[1:])
+ match = self.exactmatch(credit - debts[0]["balance"], debts[1:])
if match:
match.append(debts[0])
else:
@@ -136,12 +153,15 @@ class Project(db.Model):
owers = [ower.name for ower in bill.owers]
else:
owers = ', '.join([ower.name for ower in bill.owers])
- pretty_bills.append({"what": bill.what,
- "amount": round(bill.amount, 2),
- "date": str(bill.date),
- "payer_name": Person.query.get(bill.payer_id).name,
- "payer_weight": Person.query.get(bill.payer_id).weight,
- "owers": owers})
+
+ pretty_bills.append({
+ "what": bill.what,
+ "amount": round(bill.amount, 2),
+ "date": str(bill.date),
+ "payer_name": Person.query.get(bill.payer_id).name,
+ "payer_weight": Person.query.get(bill.payer_id).weight,
+ "owers": owers
+ })
return pretty_bills
def remove_member(self, member_id):
@@ -176,6 +196,7 @@ class Project(db.Model):
class Person(db.Model):
class PersonQuery(BaseQuery):
+
def get_by_name(self, name, project):
return Person.query.filter(Person.name == name)\
.filter(Project.id == project.id).one()
@@ -211,8 +232,10 @@ class Person(db.Model):
def __repr__(self):
return "<Person %s for project %s>" % (self.name, self.project.name)
+
# We need to manually define a join table for m2m relations
-billowers = db.Table('billowers',
+billowers = db.Table(
+ 'billowers',
db.Column('bill_id', db.Integer, db.ForeignKey('bill.id')),
db.Column('person_id', db.Integer, db.ForeignKey('person.id')),
)
@@ -224,11 +247,11 @@ class Bill(db.Model):
def get(self, project, id):
try:
- return self.join(Person, Project)\
- .filter(Bill.payer_id == Person.id)\
- .filter(Person.project_id == Project.id)\
- .filter(Project.id == project.id)\
- .filter(Bill.id == id).one()
+ return (self.join(Person, Project)
+ .filter(Bill.payer_id == Person.id)
+ .filter(Person.project_id == Project.id)
+ .filter(Project.id == project.id)
+ .filter(Bill.id == id).one())
except orm.exc.NoResultFound:
return None
@@ -262,8 +285,10 @@ class Bill(db.Model):
return 0
def __repr__(self):
- return "<Bill of %s from %s for %s>" % (self.amount,
- self.payer, ", ".join([o.name for o in self.owers]))
+ return "<Bill of %s from %s for %s>" % (
+ self.amount,
+ self.payer, ", ".join([o.name for o in self.owers])
+ )
class Archive(db.Model):
diff --git a/ihatemoney/run.py b/ihatemoney/run.py
new file mode 100644
index 0000000..22cf235
--- /dev/null
+++ b/ihatemoney/run.py
@@ -0,0 +1,144 @@
+import os
+import os.path
+import warnings
+
+from flask import Flask, g, request, session
+from flask_babel import Babel
+from flask_mail import Mail
+from flask_migrate import Migrate, upgrade, stamp
+from raven.contrib.flask import Sentry
+
+from ihatemoney.api import api
+from ihatemoney.models import db
+from ihatemoney.utils import PrefixedWSGI, minimal_round
+from ihatemoney.web import main as web_interface
+
+from ihatemoney import default_settings
+
+
+def setup_database(app):
+ """Prepare the database. Create tables, run migrations etc."""
+
+ def _pre_alembic_db():
+ """ Checks if we are migrating from a pre-alembic ihatemoney
+ """
+ con = db.engine.connect()
+ tables_exist = db.engine.dialect.has_table(con, 'project')
+ alembic_setup = db.engine.dialect.has_table(con, 'alembic_version')
+ return tables_exist and not alembic_setup
+
+ db.init_app(app)
+ db.app = app
+
+ Migrate(app, db)
+ migrations_path = os.path.join(app.root_path, 'migrations')
+
+ if _pre_alembic_db():
+ with app.app_context():
+ # fake the first migration
+ stamp(migrations_path, revision='b9a10d5d63ce')
+
+ # auto-execute migrations on runtime
+ with app.app_context():
+ upgrade(migrations_path)
+
+
+def load_configuration(app, configuration=None):
+ """ Find the right configuration file for the application and load it.
+
+ By order of preference:
+ - Use the IHATEMONEY_SETTINGS_FILE_PATH env var if defined ;
+ - If not, use /etc/ihatemoney/ihatemoney.cfg ;
+ - Otherwise, load the default settings.
+ """
+
+ env_var_config = os.environ.get('IHATEMONEY_SETTINGS_FILE_PATH')
+ app.config.from_object('ihatemoney.default_settings')
+ if configuration:
+ app.config.from_object(configuration)
+ elif env_var_config:
+ app.config.from_pyfile(env_var_config)
+ else:
+ app.config.from_pyfile('ihatemoney.cfg', silent=True)
+
+
+def validate_configuration(app):
+
+ if app.config['SECRET_KEY'] == default_settings.SECRET_KEY:
+ warnings.warn(
+ "Running a server without changing the SECRET_KEY can lead to"
+ + " user impersonation. Please update your configuration file.",
+ UserWarning
+ )
+ # Deprecations
+ if 'DEFAULT_MAIL_SENDER' in app.config:
+ # Since flask-mail 0.8
+ warnings.warn(
+ "DEFAULT_MAIL_SENDER is deprecated in favor of MAIL_DEFAULT_SENDER"
+ + " and will be removed in further version",
+ UserWarning
+ )
+ if 'MAIL_DEFAULT_SENDER' not in app.config:
+ app.config['MAIL_DEFAULT_SENDER'] = default_settings.DEFAULT_MAIL_SENDER
+
+ if "pbkdf2:sha256:" not in app.config['ADMIN_PASSWORD'] and app.config['ADMIN_PASSWORD']:
+ # Since 2.0
+ warnings.warn(
+ "The way Ihatemoney stores your ADMIN_PASSWORD has changed. You are using an unhashed"
+ + " ADMIN_PASSWORD, which is not supported anymore and won't let you access your admin"
+ + " endpoints. Please use the command 'ihatemoney generate_password_hash'"
+ + " to generate a proper password HASH and copy the output to the value of"
+ + " ADMIN_PASSWORD in your settings file.",
+ UserWarning
+ )
+
+
+def create_app(configuration=None, instance_path='/etc/ihatemoney',
+ instance_relative_config=True):
+ app = Flask(
+ __name__,
+ instance_path=instance_path,
+ instance_relative_config=instance_relative_config)
+
+ # If a configuration object is passed, use it. Otherwise try to find one.
+ load_configuration(app, configuration)
+ app.wsgi_app = PrefixedWSGI(app)
+
+ validate_configuration(app)
+ app.register_blueprint(web_interface)
+ app.register_blueprint(api)
+
+ # Configure the application
+ setup_database(app)
+
+ mail = Mail()
+ mail.init_app(app)
+ app.mail = mail
+
+ # Error reporting
+ Sentry(app)
+
+ # Jinja filters
+ app.jinja_env.filters['minimal_round'] = minimal_round
+
+ # Translations
+ babel = Babel(app)
+
+ @babel.localeselector
+ def get_locale():
+ # get the lang from the session if defined, fallback on the browser "accept
+ # languages" header.
+ lang = session.get('lang', request.accept_languages.best_match(['fr', 'en']))
+ setattr(g, 'lang', lang)
+ return lang
+
+ return app
+
+
+def main():
+ app = create_app()
+ app.run(host="0.0.0.0", debug=True)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/budget/static/css/bootstrap-datepicker3.standalone.css b/ihatemoney/static/css/bootstrap-datepicker3.standalone.css
index b61742e..b61742e 100644
--- a/budget/static/css/bootstrap-datepicker3.standalone.css
+++ b/ihatemoney/static/css/bootstrap-datepicker3.standalone.css
diff --git a/budget/static/css/bootstrap.min.css b/ihatemoney/static/css/bootstrap.min.css
index a8da074..a8da074 100644
--- a/budget/static/css/bootstrap.min.css
+++ b/ihatemoney/static/css/bootstrap.min.css
diff --git a/budget/static/css/main.css b/ihatemoney/static/css/main.css
index 54a0008..54a0008 100644
--- a/budget/static/css/main.css
+++ b/ihatemoney/static/css/main.css
diff --git a/budget/static/fonts/OFL.txt b/ihatemoney/static/fonts/OFL.txt
index 6e1e20d..6e1e20d 100644
--- a/budget/static/fonts/OFL.txt
+++ b/ihatemoney/static/fonts/OFL.txt
diff --git a/budget/static/fonts/comfortaa-regular-webfont.eot b/ihatemoney/static/fonts/comfortaa-regular-webfont.eot
index 41f9d83..41f9d83 100644
--- a/budget/static/fonts/comfortaa-regular-webfont.eot
+++ b/ihatemoney/static/fonts/comfortaa-regular-webfont.eot
Binary files differ
diff --git a/budget/static/fonts/comfortaa-regular-webfont.svg b/ihatemoney/static/fonts/comfortaa-regular-webfont.svg
index 518873c..518873c 100644
--- a/budget/static/fonts/comfortaa-regular-webfont.svg
+++ b/ihatemoney/static/fonts/comfortaa-regular-webfont.svg
diff --git a/budget/static/fonts/comfortaa-regular-webfont.woff b/ihatemoney/static/fonts/comfortaa-regular-webfont.woff
index 10f74d0..10f74d0 100644
--- a/budget/static/fonts/comfortaa-regular-webfont.woff
+++ b/ihatemoney/static/fonts/comfortaa-regular-webfont.woff
Binary files differ
diff --git a/budget/static/fonts/fontfaces.css b/ihatemoney/static/fonts/fontfaces.css
index c872f38..c872f38 100644
--- a/budget/static/fonts/fontfaces.css
+++ b/ihatemoney/static/fonts/fontfaces.css
diff --git a/budget/static/fonts/lobster-webfont.eot b/ihatemoney/static/fonts/lobster-webfont.eot
index d2257df..d2257df 100644
--- a/budget/static/fonts/lobster-webfont.eot
+++ b/ihatemoney/static/fonts/lobster-webfont.eot
Binary files differ
diff --git a/budget/static/fonts/lobster-webfont.svg b/ihatemoney/static/fonts/lobster-webfont.svg
index a9f510f..a9f510f 100644
--- a/budget/static/fonts/lobster-webfont.svg
+++ b/ihatemoney/static/fonts/lobster-webfont.svg
diff --git a/budget/static/fonts/lobster-webfont.woff b/ihatemoney/static/fonts/lobster-webfont.woff
index bf39d59..bf39d59 100644
--- a/budget/static/fonts/lobster-webfont.woff
+++ b/ihatemoney/static/fonts/lobster-webfont.woff
Binary files differ
diff --git a/budget/static/images/delete.png b/ihatemoney/static/images/delete.png
index aa786a3..aa786a3 100644
--- a/budget/static/images/delete.png
+++ b/ihatemoney/static/images/delete.png
Binary files differ
diff --git a/budget/static/images/deleter.png b/ihatemoney/static/images/deleter.png
index 04a23f3..04a23f3 100644
--- a/budget/static/images/deleter.png
+++ b/ihatemoney/static/images/deleter.png
Binary files differ
diff --git a/budget/static/images/edit.png b/ihatemoney/static/images/edit.png
index 02662fc..02662fc 100644
--- a/budget/static/images/edit.png
+++ b/ihatemoney/static/images/edit.png
Binary files differ
diff --git a/budget/static/images/glyphicons-halflings-white.png b/ihatemoney/static/images/glyphicons-halflings-white.png
index a20760b..a20760b 100644
--- a/budget/static/images/glyphicons-halflings-white.png
+++ b/ihatemoney/static/images/glyphicons-halflings-white.png
Binary files differ
diff --git a/budget/static/images/glyphicons-halflings.png b/ihatemoney/static/images/glyphicons-halflings.png
index 92d4445..92d4445 100644
--- a/budget/static/images/glyphicons-halflings.png
+++ b/ihatemoney/static/images/glyphicons-halflings.png
Binary files differ
diff --git a/budget/static/images/gradient.png b/ihatemoney/static/images/gradient.png
index ad148bf..ad148bf 100644
--- a/budget/static/images/gradient.png
+++ b/ihatemoney/static/images/gradient.png
Binary files differ
diff --git a/budget/static/images/reactivate.png b/ihatemoney/static/images/reactivate.png
index 54c60c0..54c60c0 100644
--- a/budget/static/images/reactivate.png
+++ b/ihatemoney/static/images/reactivate.png
Binary files differ
diff --git a/budget/static/js/bootstrap-datepicker.js b/ihatemoney/static/js/bootstrap-datepicker.js
index 76a99fc..76a99fc 100644
--- a/budget/static/js/bootstrap-datepicker.js
+++ b/ihatemoney/static/js/bootstrap-datepicker.js
diff --git a/budget/static/js/bootstrap.min.js b/ihatemoney/static/js/bootstrap.min.js
index d9c72df..d9c72df 100644
--- a/budget/static/js/bootstrap.min.js
+++ b/ihatemoney/static/js/bootstrap.min.js
diff --git a/budget/static/js/ihatemoney.js b/ihatemoney/static/js/ihatemoney.js
index 24e82b7..24e82b7 100644
--- a/budget/static/js/ihatemoney.js
+++ b/ihatemoney/static/js/ihatemoney.js
diff --git a/budget/static/js/jquery-3.1.1.min.js b/ihatemoney/static/js/jquery-3.1.1.min.js
index 4c5be4c..4c5be4c 100644
--- a/budget/static/js/jquery-3.1.1.min.js
+++ b/ihatemoney/static/js/jquery-3.1.1.min.js
diff --git a/budget/static/js/locales/bootstrap-datepicker.fr.min.js b/ihatemoney/static/js/locales/bootstrap-datepicker.fr.min.js
index 88379c3..88379c3 100644
--- a/budget/static/js/locales/bootstrap-datepicker.fr.min.js
+++ b/ihatemoney/static/js/locales/bootstrap-datepicker.fr.min.js
diff --git a/budget/static/js/tether.min.js b/ihatemoney/static/js/tether.min.js
index d16b9b1..d16b9b1 100644
--- a/budget/static/js/tether.min.js
+++ b/ihatemoney/static/js/tether.min.js
diff --git a/budget/templates/add_bill.html b/ihatemoney/templates/add_bill.html
index 595f363..595f363 100644
--- a/budget/templates/add_bill.html
+++ b/ihatemoney/templates/add_bill.html
diff --git a/budget/templates/add_member.html b/ihatemoney/templates/add_member.html
index 8ddfd52..8ddfd52 100644
--- a/budget/templates/add_member.html
+++ b/ihatemoney/templates/add_member.html
diff --git a/budget/templates/authenticate.html b/ihatemoney/templates/authenticate.html
index f241c48..f241c48 100644
--- a/budget/templates/authenticate.html
+++ b/ihatemoney/templates/authenticate.html
diff --git a/budget/templates/create_project.html b/ihatemoney/templates/create_project.html
index 9d4fde9..9d4fde9 100644
--- a/budget/templates/create_project.html
+++ b/ihatemoney/templates/create_project.html
diff --git a/budget/templates/dashboard.html b/ihatemoney/templates/dashboard.html
index 3f50915..3f50915 100644
--- a/budget/templates/dashboard.html
+++ b/ihatemoney/templates/dashboard.html
diff --git a/budget/templates/debug.html b/ihatemoney/templates/debug.html
index 6f97667..6f97667 100644
--- a/budget/templates/debug.html
+++ b/ihatemoney/templates/debug.html
diff --git a/budget/templates/display_errors.html b/ihatemoney/templates/display_errors.html
index 9e19605..9e19605 100644
--- a/budget/templates/display_errors.html
+++ b/ihatemoney/templates/display_errors.html
diff --git a/budget/templates/edit_member.html b/ihatemoney/templates/edit_member.html
index 5f097f9..5f097f9 100644
--- a/budget/templates/edit_member.html
+++ b/ihatemoney/templates/edit_member.html
diff --git a/budget/templates/edit_project.html b/ihatemoney/templates/edit_project.html
index a5e85c3..a5e85c3 100644
--- a/budget/templates/edit_project.html
+++ b/ihatemoney/templates/edit_project.html
diff --git a/budget/templates/forms.html b/ihatemoney/templates/forms.html
index ffdd165..ffdd165 100644
--- a/budget/templates/forms.html
+++ b/ihatemoney/templates/forms.html
diff --git a/budget/templates/home.html b/ihatemoney/templates/home.html
index 9bfe467..9bfe467 100644
--- a/budget/templates/home.html
+++ b/ihatemoney/templates/home.html
diff --git a/budget/templates/invitation_mail.en b/ihatemoney/templates/invitation_mail.en
index 03f5141..03f5141 100644
--- a/budget/templates/invitation_mail.en
+++ b/ihatemoney/templates/invitation_mail.en
diff --git a/budget/templates/invitation_mail.fr b/ihatemoney/templates/invitation_mail.fr
index 53698dd..53698dd 100644
--- a/budget/templates/invitation_mail.fr
+++ b/ihatemoney/templates/invitation_mail.fr
diff --git a/budget/templates/layout.html b/ihatemoney/templates/layout.html
index 6ecff41..6ecff41 100644
--- a/budget/templates/layout.html
+++ b/ihatemoney/templates/layout.html
diff --git a/budget/templates/list_bills.html b/ihatemoney/templates/list_bills.html
index 4029bc9..4029bc9 100644
--- a/budget/templates/list_bills.html
+++ b/ihatemoney/templates/list_bills.html
diff --git a/budget/templates/password_reminder.en b/ihatemoney/templates/password_reminder.en
index 31210aa..31210aa 100644
--- a/budget/templates/password_reminder.en
+++ b/ihatemoney/templates/password_reminder.en
diff --git a/budget/templates/password_reminder.fr b/ihatemoney/templates/password_reminder.fr
index 58f04e3..58f04e3 100644
--- a/budget/templates/password_reminder.fr
+++ b/ihatemoney/templates/password_reminder.fr
diff --git a/budget/templates/password_reminder.html b/ihatemoney/templates/password_reminder.html
index 8f46289..8f46289 100644
--- a/budget/templates/password_reminder.html
+++ b/ihatemoney/templates/password_reminder.html
diff --git a/budget/templates/recent_projects.html b/ihatemoney/templates/recent_projects.html
index df4972d..df4972d 100644
--- a/budget/templates/recent_projects.html
+++ b/ihatemoney/templates/recent_projects.html
diff --git a/budget/templates/reminder_mail.en b/ihatemoney/templates/reminder_mail.en
index fe57be2..fe57be2 100644
--- a/budget/templates/reminder_mail.en
+++ b/ihatemoney/templates/reminder_mail.en
diff --git a/budget/templates/reminder_mail.fr b/ihatemoney/templates/reminder_mail.fr
index 8130218..8130218 100644
--- a/budget/templates/reminder_mail.fr
+++ b/ihatemoney/templates/reminder_mail.fr
diff --git a/budget/templates/send_invites.html b/ihatemoney/templates/send_invites.html
index 7b3bdc5..7b3bdc5 100644
--- a/budget/templates/send_invites.html
+++ b/ihatemoney/templates/send_invites.html
diff --git a/budget/templates/settle_bills.html b/ihatemoney/templates/settle_bills.html
index b67a9b8..b67a9b8 100644
--- a/budget/templates/settle_bills.html
+++ b/ihatemoney/templates/settle_bills.html
diff --git a/budget/templates/sidebar_table_layout.html b/ihatemoney/templates/sidebar_table_layout.html
index 239acb3..239acb3 100644
--- a/budget/templates/sidebar_table_layout.html
+++ b/ihatemoney/templates/sidebar_table_layout.html
diff --git a/budget/tests/__init__.py b/ihatemoney/tests/__init__.py
index e69de29..e69de29 100644
--- a/budget/tests/__init__.py
+++ b/ihatemoney/tests/__init__.py
diff --git a/budget/tests/ihatemoney.cfg b/ihatemoney/tests/ihatemoney.cfg
index 6345fcf..6345fcf 100644
--- a/budget/tests/ihatemoney.cfg
+++ b/ihatemoney/tests/ihatemoney.cfg
diff --git a/budget/tests/ihatemoney_envvar.cfg b/ihatemoney/tests/ihatemoney_envvar.cfg
index dbc078e..dbc078e 100644
--- a/budget/tests/ihatemoney_envvar.cfg
+++ b/ihatemoney/tests/ihatemoney_envvar.cfg
diff --git a/budget/tests/tests.py b/ihatemoney/tests/tests.py
index 386920f..271477a 100644
--- a/budget/tests/tests.py
+++ b/ihatemoney/tests/tests.py
@@ -1,4 +1,4 @@
- # -*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
from __future__ import unicode_literals
try:
import unittest2 as unittest
@@ -12,139 +12,119 @@ import six
from werkzeug.security import generate_password_hash
from flask import session
+from flask_testing import TestCase
+
+from ihatemoney.run import create_app, db
+from ihatemoney import models
+from ihatemoney import utils
# Unset configuration file env var if previously set
if 'IHATEMONEY_SETTINGS_FILE_PATH' in os.environ:
del os.environ['IHATEMONEY_SETTINGS_FILE_PATH']
-from .. import run
-from .. import models
-from .. import utils
-
__HERE__ = os.path.dirname(os.path.abspath(__file__))
-class TestCase(unittest.TestCase):
+class BaseTestCase(TestCase):
- def setUp(self):
- run.app.config['TESTING'] = True
+ SECRET_KEY = "TEST SESSION"
- run.app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///memory"
- run.app.config['WTF_CSRF_ENABLED'] = False # simplify the tests
- self.app = run.app.test_client()
- try:
- models.db.init_app(run.app)
- run.mail.init_app(run.app)
- except:
- pass
+ def create_app(self):
+ # Pass the test object as a configuration.
+ return create_app(self)
- models.db.app = run.app
- models.db.create_all()
+ def setUp(self):
+ db.create_all()
def tearDown(self):
# clean after testing
- models.db.session.remove()
- models.db.drop_all()
- # reconfigure app with default settings
- run.configure()
+ db.session.remove()
+ db.drop_all()
def login(self, project, password=None, test_client=None):
password = password or project
- return self.app.post('/authenticate', data=dict(
+ return self.client.post('/authenticate', data=dict(
id=project, password=password), follow_redirects=True)
def post_project(self, name):
"""Create a fake project"""
# create the project
- self.app.post("/create", data={
- 'name': name,
- 'id': name,
- 'password': name,
- 'contact_email': '%s@notmyidea.org' % name
+ self.client.post("/create", data={
+ 'name': name,
+ 'id': name,
+ 'password': name,
+ 'contact_email': '%s@notmyidea.org' % name
})
def create_project(self, name):
- models.db.session.add(models.Project(id=name, name=six.text_type(name),
- password=name, contact_email="%s@notmyidea.org" % name))
+ project = models.Project(
+ id=name,
+ name=six.text_type(name),
+ password=name,
+ contact_email="%s@notmyidea.org" % name)
+ models.db.session.add(project)
models.db.session.commit()
-class BudgetTestCase(TestCase):
+class IhatemoneyTestCase(BaseTestCase):
+ SQLALCHEMY_DATABASE_URI = "sqlite://"
+ TESTING = True
+ WTF_CSRF_ENABLED = False # Simplifies the tests.
+
+
+class DefaultConfigurationTestCase(BaseTestCase):
def test_default_configuration(self):
"""Test that default settings are loaded when no other configuration file is specified"""
- run.configure()
- self.assertFalse(run.app.config['DEBUG'])
- self.assertEqual(run.app.config['SQLALCHEMY_DATABASE_URI'], 'sqlite:///budget.db')
- self.assertFalse(run.app.config['SQLALCHEMY_TRACK_MODIFICATIONS'])
- self.assertEqual(run.app.config['SECRET_KEY'], 'tralala')
- self.assertEqual(run.app.config['MAIL_DEFAULT_SENDER'],
+ self.assertFalse(self.app.config['DEBUG'])
+ self.assertEqual(self.app.config['SQLALCHEMY_DATABASE_URI'], 'sqlite://')
+ self.assertFalse(self.app.config['SQLALCHEMY_TRACK_MODIFICATIONS'])
+ self.assertEqual(self.app.config['MAIL_DEFAULT_SENDER'],
("Budget manager", "budget@notmyidea.org"))
- def test_env_var_configuration_file(self):
- """Test that settings are loaded from the specified configuration file"""
- os.environ['IHATEMONEY_SETTINGS_FILE_PATH'] = os.path.join(__HERE__,
- "ihatemoney_envvar.cfg")
- run.configure()
- self.assertEqual(run.app.config['SECRET_KEY'], 'lalatra')
-
- # Test that the specified configuration file is loaded
- # even if the default configuration file ihatemoney.cfg exists
- os.environ['IHATEMONEY_SETTINGS_FILE_PATH'] = os.path.join(__HERE__,
- "ihatemoney_envvar.cfg")
- run.app.config.root_path = __HERE__
- run.configure()
- self.assertEqual(run.app.config['SECRET_KEY'], 'lalatra')
-
- if 'IHATEMONEY_SETTINGS_FILE_PATH' in os.environ:
- del os.environ['IHATEMONEY_SETTINGS_FILE_PATH']
-
- def test_default_configuration_file(self):
- """Test that settings are loaded from the default configuration file"""
- run.app.config.root_path = __HERE__
- run.configure()
- self.assertEqual(run.app.config['SECRET_KEY'], 'supersecret')
+
+class BudgetTestCase(IhatemoneyTestCase):
def test_notifications(self):
"""Test that the notifications are sent, and that email adresses
are checked properly.
"""
# sending a message to one person
- with run.mail.record_messages() as outbox:
+ with self.app.mail.record_messages() as outbox:
# create a project
self.login("raclette")
self.post_project("raclette")
- self.app.post("/raclette/invite",
- data={"emails": 'alexis@notmyidea.org'})
+ self.client.post("/raclette/invite",
+ data={"emails": 'alexis@notmyidea.org'})
self.assertEqual(len(outbox), 2)
self.assertEqual(outbox[0].recipients, ["raclette@notmyidea.org"])
self.assertEqual(outbox[1].recipients, ["alexis@notmyidea.org"])
# sending a message to multiple persons
- with run.mail.record_messages() as outbox:
- self.app.post("/raclette/invite",
- data={"emails": 'alexis@notmyidea.org, toto@notmyidea.org'})
+ with self.app.mail.record_messages() as outbox:
+ self.client.post("/raclette/invite",
+ data={"emails": 'alexis@notmyidea.org, toto@notmyidea.org'})
# only one message is sent to multiple persons
self.assertEqual(len(outbox), 1)
self.assertEqual(outbox[0].recipients,
- ["alexis@notmyidea.org", "toto@notmyidea.org"])
+ ["alexis@notmyidea.org", "toto@notmyidea.org"])
# mail address checking
- with run.mail.record_messages() as outbox:
- response = self.app.post("/raclette/invite",
- data={"emails": "toto"})
+ with self.app.mail.record_messages() as outbox:
+ response = self.client.post("/raclette/invite",
+ data={"emails": "toto"})
self.assertEqual(len(outbox), 0) # no message sent
self.assertIn("The email toto is not valid", response.data.decode('utf-8'))
# mixing good and wrong adresses shouldn't send any messages
- with run.mail.record_messages() as outbox:
- self.app.post("/raclette/invite",
- data={"emails": 'alexis@notmyidea.org, alexis'}) # not valid
+ with self.app.mail.record_messages() as outbox:
+ self.client.post("/raclette/invite",
+ data={"emails": 'alexis@notmyidea.org, alexis'}) # not valid
# only one message is sent to multiple persons
self.assertEqual(len(outbox), 0)
@@ -155,19 +135,19 @@ class BudgetTestCase(TestCase):
self.create_project("raclette")
- with run.mail.record_messages() as outbox:
+ with self.app.mail.record_messages() as outbox:
# a nonexisting project should not send an email
- self.app.post("/password-reminder", data={"id": "unexisting"})
+ self.client.post("/password-reminder", data={"id": "unexisting"})
self.assertEqual(len(outbox), 0)
# a mail should be sent when a project exists
- self.app.post("/password-reminder", data={"id": "raclette"})
+ self.client.post("/password-reminder", data={"id": "raclette"})
self.assertEqual(len(outbox), 1)
self.assertIn("raclette", outbox[0].body)
self.assertIn("raclette@notmyidea.org", outbox[0].recipients)
def test_project_creation(self):
- with run.app.test_client() as c:
+ with self.app.test_client() as c:
# add a valid project
c.post("/create", data={
@@ -198,7 +178,7 @@ class BudgetTestCase(TestCase):
def test_project_deletion(self):
- with run.app.test_client() as c:
+ with self.app.test_client() as c:
c.post("/create", data={
'name': 'raclette party',
'id': 'raclette',
@@ -219,37 +199,37 @@ class BudgetTestCase(TestCase):
self.login("raclette")
# adds a member to this project
- self.app.post("/raclette/members/add", data={'name': 'alexis'})
+ self.client.post("/raclette/members/add", data={'name': 'alexis'})
self.assertEqual(len(models.Project.query.get("raclette").members), 1)
# adds him twice
- result = self.app.post("/raclette/members/add",
- data={'name': 'alexis'})
+ result = self.client.post("/raclette/members/add",
+ data={'name': 'alexis'})
# should not accept him
self.assertEqual(len(models.Project.query.get("raclette").members), 1)
# add fred
- self.app.post("/raclette/members/add", data={'name': 'fred'})
+ self.client.post("/raclette/members/add", data={'name': 'fred'})
self.assertEqual(len(models.Project.query.get("raclette").members), 2)
# check fred is present in the bills page
- result = self.app.get("/raclette/")
+ result = self.client.get("/raclette/")
self.assertIn("fred", result.data.decode('utf-8'))
# remove fred
- self.app.post("/raclette/members/%s/delete" %
- models.Project.query.get("raclette").members[-1].id)
+ self.client.post("/raclette/members/%s/delete" %
+ models.Project.query.get("raclette").members[-1].id)
# as fred is not bound to any bill, he is removed
self.assertEqual(len(models.Project.query.get("raclette").members), 1)
# add fred again
- self.app.post("/raclette/members/add", data={'name': 'fred'})
+ self.client.post("/raclette/members/add", data={'name': 'fred'})
fred_id = models.Project.query.get("raclette").members[-1].id
# bound him to a bill
- result = self.app.post("/raclette/add", data={
+ result = self.client.post("/raclette/add", data={
'date': '2011-08-10',
'what': 'fromage à raclette',
'payer': fred_id,
@@ -258,47 +238,47 @@ class BudgetTestCase(TestCase):
})
# remove fred
- self.app.post("/raclette/members/%s/delete" % fred_id)
+ self.client.post("/raclette/members/%s/delete" % fred_id)
# he is still in the database, but is deactivated
self.assertEqual(len(models.Project.query.get("raclette").members), 2)
self.assertEqual(
- len(models.Project.query.get("raclette").active_members), 1)
+ len(models.Project.query.get("raclette").active_members), 1)
# as fred is now deactivated, check that he is not listed when adding
# a bill or displaying the balance
- result = self.app.get("/raclette/")
+ result = self.client.get("/raclette/")
self.assertNotIn(("/raclette/members/%s/delete" % fred_id), result.data.decode('utf-8'))
- result = self.app.get("/raclette/add")
+ result = self.client.get("/raclette/add")
self.assertNotIn("fred", result.data.decode('utf-8'))
# adding him again should reactivate him
- self.app.post("/raclette/members/add", data={'name': 'fred'})
+ self.client.post("/raclette/members/add", data={'name': 'fred'})
self.assertEqual(
- len(models.Project.query.get("raclette").active_members), 2)
+ len(models.Project.query.get("raclette").active_members), 2)
# adding an user with the same name as another user from a different
# project should not cause any troubles
self.post_project("randomid")
self.login("randomid")
- self.app.post("/randomid/members/add", data={'name': 'fred'})
+ self.client.post("/randomid/members/add", data={'name': 'fred'})
self.assertEqual(
- len(models.Project.query.get("randomid").active_members), 1)
+ len(models.Project.query.get("randomid").active_members), 1)
def test_person_model(self):
self.post_project("raclette")
self.login("raclette")
# adds a member to this project
- self.app.post("/raclette/members/add", data={'name': 'alexis'})
+ self.client.post("/raclette/members/add", data={'name': 'alexis'})
alexis = models.Project.query.get("raclette").members[-1]
# should not have any bills
self.assertFalse(alexis.has_bills())
# bound him to a bill
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2011-08-10',
'what': 'fromage à raclette',
'payer': alexis.id,
@@ -315,36 +295,36 @@ class BudgetTestCase(TestCase):
self.login("raclette")
# adds a member to this project
- self.app.post("/raclette/members/add", data={'name': 'alexis'})
+ self.client.post("/raclette/members/add", data={'name': 'alexis'})
# try to remove the member using GET method
- response = self.app.get("/raclette/members/1/delete")
+ response = self.client.get("/raclette/members/1/delete")
self.assertEqual(response.status_code, 405)
- #delete user using POST method
- self.app.post("/raclette/members/1/delete")
+ # delete user using POST method
+ self.client.post("/raclette/members/1/delete")
self.assertEqual(
- len(models.Project.query.get("raclette").active_members), 0)
- #try to delete an user already deleted
- self.app.post("/raclette/members/1/delete")
+ len(models.Project.query.get("raclette").active_members), 0)
+ # try to delete an user already deleted
+ self.client.post("/raclette/members/1/delete")
def test_demo(self):
# test that a demo project is created if none is defined
self.assertEqual([], models.Project.query.all())
- self.app.get("/demo")
+ self.client.get("/demo")
self.assertTrue(models.Project.query.get("demo") is not None)
def test_deactivated_demo(self):
- run.app.config['ACTIVATE_DEMO_PROJECT'] = False
+ self.app.config['ACTIVATE_DEMO_PROJECT'] = False
# test redirection to the create project form when demo is deactivated
- resp = self.app.get("/demo")
+ resp = self.client.get("/demo")
self.assertIn('<a href="/create?project_id=demo">', resp.data.decode('utf-8'))
def test_authentication(self):
# try to authenticate without credentials should redirect
# to the authentication page
- resp = self.app.post("/authenticate")
+ resp = self.client.post("/authenticate")
self.assertIn("Authentication", resp.data.decode('utf-8'))
# raclette that the login / logout process works
@@ -352,21 +332,21 @@ class BudgetTestCase(TestCase):
# try to see the project while not being authenticated should redirect
# to the authentication page
- resp = self.app.get("/raclette", follow_redirects=True)
+ resp = self.client.get("/raclette", follow_redirects=True)
self.assertIn("Authentication", resp.data.decode('utf-8'))
# try to connect with wrong credentials should not work
- with run.app.test_client() as c:
+ with self.app.test_client() as c:
resp = c.post("/authenticate",
- data={'id': 'raclette', 'password': 'nope'})
+ data={'id': 'raclette', 'password': 'nope'})
self.assertIn("Authentication", resp.data.decode('utf-8'))
self.assertNotIn('raclette', session)
# try to connect with the right credentials should work
- with run.app.test_client() as c:
+ with self.app.test_client() as c:
resp = c.post("/authenticate",
- data={'id': 'raclette', 'password': 'raclette'})
+ data={'id': 'raclette', 'password': 'raclette'})
self.assertNotIn("Authentication", resp.data.decode('utf-8'))
self.assertIn('raclette', session)
@@ -377,36 +357,36 @@ class BudgetTestCase(TestCase):
self.assertNotIn('raclette', session)
def test_admin_authentication(self):
- run.app.config['ADMIN_PASSWORD'] = generate_password_hash("pass")
+ self.app.config['ADMIN_PASSWORD'] = generate_password_hash("pass")
# test the redirection to the authentication page when trying to access admin endpoints
- resp = self.app.get("/create")
+ resp = self.client.get("/create")
self.assertIn('<a href="/admin?goto=%2Fcreate">', resp.data.decode('utf-8'))
# test right password
- resp = self.app.post("/admin?goto=%2Fcreate", data={'admin_password': 'pass'})
+ resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'pass'})
self.assertIn('<a href="/create">/create</a>', resp.data.decode('utf-8'))
# test wrong password
- resp = self.app.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'})
+ resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'})
self.assertNotIn('<a href="/create">/create</a>', resp.data.decode('utf-8'))
# test empty password
- resp = self.app.post("/admin?goto=%2Fcreate", data={'admin_password': ''})
+ resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': ''})
self.assertNotIn('<a href="/create">/create</a>', resp.data.decode('utf-8'))
def test_manage_bills(self):
self.post_project("raclette")
# add two persons
- self.app.post("/raclette/members/add", data={'name': 'alexis'})
- self.app.post("/raclette/members/add", data={'name': 'fred'})
+ self.client.post("/raclette/members/add", data={'name': 'alexis'})
+ self.client.post("/raclette/members/add", data={'name': 'fred'})
members_ids = [m.id for m in
models.Project.query.get("raclette").members]
# create a bill
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2011-08-10',
'what': 'fromage à raclette',
'payer': members_ids[0],
@@ -418,7 +398,7 @@ class BudgetTestCase(TestCase):
self.assertEqual(bill.amount, 25)
# edit the bill
- self.app.post("/raclette/edit/%s" % bill.id, data={
+ self.client.post("/raclette/edit/%s" % bill.id, data={
'date': '2011-08-10',
'what': 'fromage à raclette',
'payer': members_ids[0],
@@ -430,11 +410,11 @@ class BudgetTestCase(TestCase):
self.assertEqual(bill.amount, 10, "bill edition")
# delete the bill
- self.app.get("/raclette/delete/%s" % bill.id)
+ self.client.get("/raclette/delete/%s" % bill.id)
self.assertEqual(0, len(models.Bill.query.all()), "bill deletion")
# test balance
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2011-08-10',
'what': 'fromage à raclette',
'payer': members_ids[0],
@@ -442,7 +422,7 @@ class BudgetTestCase(TestCase):
'amount': '19',
})
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2011-08-10',
'what': 'fromage à raclette',
'payer': members_ids[1],
@@ -450,7 +430,7 @@ class BudgetTestCase(TestCase):
'amount': '20',
})
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2011-08-10',
'what': 'fromage à raclette',
'payer': members_ids[1],
@@ -461,8 +441,8 @@ class BudgetTestCase(TestCase):
balance = models.Project.query.get("raclette").balance
self.assertEqual(set(balance.values()), set([19.0, -19.0]))
- #Bill with negative amount
- self.app.post("/raclette/add", data={
+ # Bill with negative amount
+ self.client.post("/raclette/add", data={
'date': '2011-08-12',
'what': 'fromage à raclette',
'payer': members_ids[0],
@@ -472,8 +452,8 @@ class BudgetTestCase(TestCase):
bill = models.Bill.query.filter(models.Bill.date == '2011-08-12')[0]
self.assertEqual(bill.amount, -25)
- #add a bill with a comma
- self.app.post("/raclette/add", data={
+ # add a bill with a comma
+ self.client.post("/raclette/add", data={
'date': '2011-08-01',
'what': 'fromage à raclette',
'payer': members_ids[0],
@@ -487,14 +467,14 @@ class BudgetTestCase(TestCase):
self.post_project("raclette")
# add two persons
- self.app.post("/raclette/members/add", data={'name': 'alexis'})
- self.app.post("/raclette/members/add", data={'name': 'freddy familly', 'weight': 4})
+ self.client.post("/raclette/members/add", data={'name': 'alexis'})
+ self.client.post("/raclette/members/add", data={'name': 'freddy familly', 'weight': 4})
members_ids = [m.id for m in
models.Project.query.get("raclette").members]
# test balance
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2011-08-10',
'what': 'fromage à raclette',
'payer': members_ids[0],
@@ -502,7 +482,7 @@ class BudgetTestCase(TestCase):
'amount': '10',
})
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2011-08-10',
'what': 'pommes de terre',
'payer': members_ids[1],
@@ -517,28 +497,27 @@ class BudgetTestCase(TestCase):
self.post_project("raclette")
# add two persons
- self.app.post("/raclette/members/add", data={'name': 'alexis'})
- self.app.post("/raclette/members/add", data={'name': 'tata', 'weight': 1})
+ self.client.post("/raclette/members/add", data={'name': 'alexis'})
+ self.client.post("/raclette/members/add", data={'name': 'tata', 'weight': 1})
- resp = self.app.get("/raclette/")
+ resp = self.client.get("/raclette/")
self.assertIn('extra-info', resp.data.decode('utf-8'))
- self.app.post("/raclette/members/add", data={'name': 'freddy familly', 'weight': 4})
+ self.client.post("/raclette/members/add", data={'name': 'freddy familly', 'weight': 4})
- resp = self.app.get("/raclette/")
+ resp = self.client.get("/raclette/")
self.assertNotIn('extra-info', resp.data.decode('utf-8'))
-
def test_rounding(self):
self.post_project("raclette")
# add members
- self.app.post("/raclette/members/add", data={'name': 'alexis'})
- self.app.post("/raclette/members/add", data={'name': 'fred'})
- self.app.post("/raclette/members/add", data={'name': 'tata'})
+ self.client.post("/raclette/members/add", data={'name': 'alexis'})
+ self.client.post("/raclette/members/add", data={'name': 'fred'})
+ self.client.post("/raclette/members/add", data={'name': 'tata'})
# create bills
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2011-08-10',
'what': 'fromage à raclette',
'payer': 1,
@@ -546,7 +525,7 @@ class BudgetTestCase(TestCase):
'amount': '24.36',
})
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2011-08-10',
'what': 'red wine',
'payer': 2,
@@ -554,7 +533,7 @@ class BudgetTestCase(TestCase):
'amount': '19.12',
})
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2011-08-10',
'what': 'delicatessen',
'payer': 1,
@@ -567,8 +546,10 @@ class BudgetTestCase(TestCase):
result[models.Project.query.get("raclette").members[0].id] = 8.12
result[models.Project.query.get("raclette").members[1].id] = 0.0
result[models.Project.query.get("raclette").members[2].id] = -8.12
- # Since we're using floating point to store currency, we can have some rounding issues that prevent test from working.
- # However, we should obtain the same values as the theorical ones if we round to 2 decimals, like in the UI.
+ # Since we're using floating point to store currency, we can have some
+ # rounding issues that prevent test from working.
+ # However, we should obtain the same values as the theorical ones if we
+ # round to 2 decimals, like in the UI.
for key, value in six.iteritems(balance):
self.assertEqual(round(value, 2), result[key])
@@ -582,8 +563,8 @@ class BudgetTestCase(TestCase):
'password': 'didoudida'
}
- resp = self.app.post("/raclette/edit", data=new_data,
- follow_redirects=True)
+ resp = self.client.post("/raclette/edit", data=new_data,
+ follow_redirects=True)
self.assertEqual(resp.status_code, 200)
project = models.Project.query.get("raclette")
@@ -593,31 +574,31 @@ class BudgetTestCase(TestCase):
# Editing a project with a wrong email address should fail
new_data['contact_email'] = 'wrong_email'
- resp = self.app.post("/raclette/edit", data=new_data,
- follow_redirects=True)
+ resp = self.client.post("/raclette/edit", data=new_data,
+ follow_redirects=True)
self.assertIn("Invalid email address", resp.data.decode('utf-8'))
def test_dashboard(self):
- response = self.app.get("/dashboard")
+ response = self.client.get("/dashboard")
self.assertEqual(response.status_code, 200)
def test_settle_page(self):
self.post_project("raclette")
- response = self.app.get("/raclette/settle_bills")
+ response = self.client.get("/raclette/settle_bills")
self.assertEqual(response.status_code, 200)
def test_settle(self):
self.post_project("raclette")
# add members
- self.app.post("/raclette/members/add", data={'name': 'alexis'})
- self.app.post("/raclette/members/add", data={'name': 'fred'})
- self.app.post("/raclette/members/add", data={'name': 'tata'})
- #Add a member with a balance=0 :
- self.app.post("/raclette/members/add", data={'name': 'toto'})
+ self.client.post("/raclette/members/add", data={'name': 'alexis'})
+ self.client.post("/raclette/members/add", data={'name': 'fred'})
+ self.client.post("/raclette/members/add", data={'name': 'tata'})
+ # Add a member with a balance=0 :
+ self.client.post("/raclette/members/add", data={'name': 'toto'})
# create bills
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2011-08-10',
'what': 'fromage à raclette',
'payer': 1,
@@ -625,7 +606,7 @@ class BudgetTestCase(TestCase):
'amount': '10.0',
})
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2011-08-10',
'what': 'red wine',
'payer': 2,
@@ -633,20 +614,20 @@ class BudgetTestCase(TestCase):
'amount': '20',
})
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2011-08-10',
'what': 'delicatessen',
'payer': 1,
'payed_for': [1, 2],
'amount': '10',
})
- project = models.Project.query.get('raclette')
+ project = models.Project.query.get('raclette')
transactions = project.get_transactions_to_settle_bill()
members = defaultdict(int)
- #We should have the same values between transactions and project balances
+ # We should have the same values between transactions and project balances
for t in transactions:
- members[t['ower']]-=t['amount']
- members[t['receiver']]+=t['amount']
+ members[t['ower']] -= t['amount']
+ members[t['receiver']] += t['amount']
balance = models.Project.query.get("raclette").balance
for m, a in members.items():
self.assertEqual(a, balance[m.id])
@@ -656,12 +637,12 @@ class BudgetTestCase(TestCase):
self.post_project("raclette")
# add members
- self.app.post("/raclette/members/add", data={'name': 'alexis'})
- self.app.post("/raclette/members/add", data={'name': 'fred'})
- self.app.post("/raclette/members/add", data={'name': 'tata'})
+ self.client.post("/raclette/members/add", data={'name': 'alexis'})
+ self.client.post("/raclette/members/add", data={'name': 'fred'})
+ self.client.post("/raclette/members/add", data={'name': 'tata'})
# create bills
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2016-12-31',
'what': 'fromage à raclette',
'payer': 1,
@@ -669,7 +650,7 @@ class BudgetTestCase(TestCase):
'amount': '10.0',
})
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2016-12-31',
'what': 'red wine',
'payer': 2,
@@ -677,16 +658,16 @@ class BudgetTestCase(TestCase):
'amount': '20',
})
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2017-01-01',
'what': 'refund',
'payer': 3,
'payed_for': [2],
'amount': '13.33',
})
- project = models.Project.query.get('raclette')
+ project = models.Project.query.get('raclette')
transactions = project.get_transactions_to_settle_bill()
- members = defaultdict(int)
+
# There should not be any zero-amount transfer after rounding
for t in transactions:
rounded_amount = round(t['amount'], 2)
@@ -697,13 +678,13 @@ class BudgetTestCase(TestCase):
self.post_project("raclette")
# add members
- self.app.post("/raclette/members/add", data={'name': 'alexis', 'weight': 2})
- self.app.post("/raclette/members/add", data={'name': 'fred'})
- self.app.post("/raclette/members/add", data={'name': 'tata'})
- self.app.post("/raclette/members/add", data={'name': 'pépé'})
+ self.client.post("/raclette/members/add", data={'name': 'alexis', 'weight': 2})
+ self.client.post("/raclette/members/add", data={'name': 'fred'})
+ self.client.post("/raclette/members/add", data={'name': 'tata'})
+ self.client.post("/raclette/members/add", data={'name': 'pépé'})
# create bills
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2016-12-31',
'what': 'fromage à raclette',
'payer': 1,
@@ -711,7 +692,7 @@ class BudgetTestCase(TestCase):
'amount': '10.0',
})
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2016-12-31',
'what': 'red wine',
'payer': 2,
@@ -719,7 +700,7 @@ class BudgetTestCase(TestCase):
'amount': '200',
})
- self.app.post("/raclette/add", data={
+ self.client.post("/raclette/add", data={
'date': '2017-01-01',
'what': 'refund',
'payer': 3,
@@ -728,27 +709,44 @@ class BudgetTestCase(TestCase):
})
# generate json export of bills
- resp = self.app.post("/raclette/edit", data={
+ resp = self.client.post("/raclette/edit", data={
'export_format': 'json',
'export_type': 'bills'
})
- expected = [{'date': '2017-01-01', 'what': 'refund',
- 'amount': 13.33, 'payer_name': 'tata', 'payer_weight': 1.0, 'owers': ['fred']},
- {'date': '2016-12-31', 'what': 'red wine',
- 'amount': 200.0, 'payer_name': 'fred', 'payer_weight': 1.0, 'owers': ['alexis', 'tata']},
- {'date': '2016-12-31', 'what': 'fromage \xe0 raclette',
- 'amount': 10.0, 'payer_name': 'alexis', 'payer_weight': 2.0, 'owers': ['alexis', 'fred', 'tata', 'p\xe9p\xe9']}]
+ expected = [{
+ 'date': '2017-01-01',
+ 'what': 'refund',
+ 'amount': 13.33,
+ 'payer_name': 'tata',
+ 'payer_weight': 1.0,
+ 'owers': ['fred']
+ }, {
+ 'date': '2016-12-31',
+ 'what': 'red wine',
+ 'amount': 200.0,
+ 'payer_name': 'fred',
+ 'payer_weight': 1.0,
+ 'owers': ['alexis', 'tata']
+ }, {
+ 'date': '2016-12-31',
+ 'what': 'fromage \xe0 raclette',
+ 'amount': 10.0,
+ 'payer_name': 'alexis',
+ 'payer_weight': 2.0,
+ 'owers': ['alexis', 'fred', 'tata', 'p\xe9p\xe9']
+ }]
self.assertEqual(json.loads(resp.data.decode('utf-8')), expected)
# generate csv export of bills
- resp = self.app.post("/raclette/edit", data={
+ resp = self.client.post("/raclette/edit", data={
'export_format': 'csv',
'export_type': 'bills'
})
- expected = ["date,what,amount,payer_name,payer_weight,owers",
- "2017-01-01,refund,13.33,tata,1.0,fred",
- "2016-12-31,red wine,200.0,fred,1.0,\"alexis, tata\"",
- "2016-12-31,fromage à raclette,10.0,alexis,2.0,\"alexis, fred, tata, pépé\""]
+ expected = [
+ "date,what,amount,payer_name,payer_weight,owers",
+ "2017-01-01,refund,13.33,tata,1.0,fred",
+ "2016-12-31,red wine,200.0,fred,1.0,\"alexis, tata\"",
+ "2016-12-31,fromage à raclette,10.0,alexis,2.0,\"alexis, fred, tata, pépé\""]
received_lines = resp.data.decode('utf-8').split("\n")
for i, line in enumerate(expected):
@@ -758,7 +756,7 @@ class BudgetTestCase(TestCase):
)
# generate json export of transactions
- resp = self.app.post("/raclette/edit", data={
+ resp = self.client.post("/raclette/edit", data={
'export_format': 'json',
'export_type': 'transactions'
})
@@ -768,7 +766,7 @@ class BudgetTestCase(TestCase):
self.assertEqual(json.loads(resp.data.decode('utf-8')), expected)
# generate csv export of transactions
- resp = self.app.post("/raclette/edit", data={
+ resp = self.client.post("/raclette/edit", data={
'export_format': 'csv',
'export_type': 'transactions'
})
@@ -786,7 +784,7 @@ class BudgetTestCase(TestCase):
)
# wrong export_format should return a 200 and export form
- resp = self.app.post("/raclette/edit", data={
+ resp = self.client.post("/raclette/edit", data={
'export_format': 'wrong_export_format',
'export_type': 'transactions'
})
@@ -795,7 +793,7 @@ class BudgetTestCase(TestCase):
self.assertIn('id="export_format" name="export_format"', resp.data.decode('utf-8'))
# wrong export_type should return a 200 and export form
- resp = self.app.post("/raclette/edit", data={
+ resp = self.client.post("/raclette/edit", data={
'export_format': 'json',
'export_type': 'wrong_export_type'
})
@@ -804,7 +802,8 @@ class BudgetTestCase(TestCase):
self.assertIn('id="export_format" name="export_format"', resp.data.decode('utf-8'))
-class APITestCase(TestCase):
+class APITestCase(IhatemoneyTestCase):
+
"""Tests the API"""
def api_create(self, name, id=None, password=None, contact=None):
@@ -812,7 +811,7 @@ class APITestCase(TestCase):
password = password or name
contact = contact or "%s@notmyidea.org" % name
- return self.app.post("/api/projects", data={
+ return self.client.post("/api/projects", data={
'name': name,
'id': id,
'password': password,
@@ -820,9 +819,9 @@ class APITestCase(TestCase):
})
def api_add_member(self, project, name, weight=1):
- self.app.post("/api/projects/%s/members" % project,
- data={"name": name, "weight": weight},
- headers=self.get_auth(project))
+ self.client.post("/api/projects/%s/members" % project,
+ data={"name": name, "weight": weight},
+ headers=self.get_auth(project))
def get_auth(self, username, password=None):
password = password or username
@@ -833,7 +832,7 @@ class APITestCase(TestCase):
def assertStatus(self, expected, resp, url=""):
return self.assertEqual(expected, resp.status_code,
- "%s expected %s, got %s" % (url, expected, resp.status_code))
+ "%s expected %s, got %s" % (url, expected, resp.status_code))
def test_basic_auth(self):
# create a project
@@ -841,7 +840,7 @@ class APITestCase(TestCase):
self.assertStatus(201, resp)
# try to do something on it being unauth should return a 401
- resp = self.app.get("/api/projects/raclette")
+ resp = self.client.get("/api/projects/raclette")
self.assertStatus(401, resp)
# PUT / POST / DELETE / GET on the different resources
@@ -849,20 +848,20 @@ class APITestCase(TestCase):
for verb in ('post',):
for resource in ("/raclette/members", "/raclette/bills"):
url = "/api/projects" + resource
- self.assertStatus(401, getattr(self.app, verb)(url),
- verb + resource)
+ self.assertStatus(401, getattr(self.client, verb)(url),
+ verb + resource)
for verb in ('get', 'delete', 'put'):
for resource in ("/raclette", "/raclette/members/1",
- "/raclette/bills/1"):
+ "/raclette/bills/1"):
url = "/api/projects" + resource
- self.assertStatus(401, getattr(self.app, verb)(url),
- verb + resource)
+ self.assertStatus(401, getattr(self.client, verb)(url),
+ verb + resource)
def test_project(self):
# wrong email should return an error
- resp = self.app.post("/api/projects", data={
+ resp = self.client.post("/api/projects", data={
'name': "raclette",
'id': "raclette",
'password': "raclette",
@@ -884,8 +883,8 @@ class APITestCase(TestCase):
self.assertIn('id', json.loads(resp.data.decode('utf-8')))
# get information about it
- resp = self.app.get("/api/projects/raclette",
- headers=self.get_auth("raclette"))
+ resp = self.client.get("/api/projects/raclette",
+ headers=self.get_auth("raclette"))
self.assertTrue(200, resp.status_code)
expected = {
@@ -900,16 +899,16 @@ class APITestCase(TestCase):
self.assertDictEqual(json.loads(resp.data.decode('utf-8')), expected)
# edit should work
- resp = self.app.put("/api/projects/raclette", data={
+ resp = self.client.put("/api/projects/raclette", data={
"contact_email": "yeah@notmyidea.org",
"password": "raclette",
"name": "The raclette party",
- }, headers=self.get_auth("raclette"))
+ }, headers=self.get_auth("raclette"))
self.assertEqual(200, resp.status_code)
- resp = self.app.get("/api/projects/raclette",
- headers=self.get_auth("raclette"))
+ resp = self.client.get("/api/projects/raclette",
+ headers=self.get_auth("raclette"))
self.assertEqual(200, resp.status_code)
expected = {
@@ -924,14 +923,14 @@ class APITestCase(TestCase):
self.assertDictEqual(json.loads(resp.data.decode('utf-8')), expected)
# delete should work
- resp = self.app.delete("/api/projects/raclette",
- headers=self.get_auth("raclette"))
+ resp = self.client.delete("/api/projects/raclette",
+ headers=self.get_auth("raclette"))
self.assertEqual(200, resp.status_code)
# get should return a 401 on an unknown resource
- resp = self.app.get("/api/projects/raclette",
- headers=self.get_auth("raclette"))
+ resp = self.client.get("/api/projects/raclette",
+ headers=self.get_auth("raclette"))
self.assertEqual(401, resp.status_code)
def test_member(self):
@@ -939,53 +938,53 @@ class APITestCase(TestCase):
self.api_create("raclette")
# get the list of members (should be empty)
- req = self.app.get("/api/projects/raclette/members",
- headers=self.get_auth("raclette"))
+ req = self.client.get("/api/projects/raclette/members",
+ headers=self.get_auth("raclette"))
self.assertStatus(200, req)
self.assertEqual('[]', req.data.decode('utf-8'))
# add a member
- req = self.app.post("/api/projects/raclette/members", data={
- "name": "Alexis"
- }, headers=self.get_auth("raclette"))
+ req = self.client.post("/api/projects/raclette/members", data={
+ "name": "Alexis"
+ }, headers=self.get_auth("raclette"))
# the id of the new member should be returned
self.assertStatus(201, req)
self.assertEqual("1", req.data.decode('utf-8'))
# the list of members should contain one member
- req = self.app.get("/api/projects/raclette/members",
- headers=self.get_auth("raclette"))
+ req = self.client.get("/api/projects/raclette/members",
+ headers=self.get_auth("raclette"))
self.assertStatus(200, req)
self.assertEqual(len(json.loads(req.data.decode('utf-8'))), 1)
# edit this member
- req = self.app.put("/api/projects/raclette/members/1", data={
- "name": "Fred"
- }, headers=self.get_auth("raclette"))
+ req = self.client.put("/api/projects/raclette/members/1", data={
+ "name": "Fred"
+ }, headers=self.get_auth("raclette"))
self.assertStatus(200, req)
# get should return the new name
- req = self.app.get("/api/projects/raclette/members/1",
- headers=self.get_auth("raclette"))
+ req = self.client.get("/api/projects/raclette/members/1",
+ headers=self.get_auth("raclette"))
self.assertStatus(200, req)
self.assertEqual("Fred", json.loads(req.data.decode('utf-8'))["name"])
# delete a member
- req = self.app.delete("/api/projects/raclette/members/1",
- headers=self.get_auth("raclette"))
+ req = self.client.delete("/api/projects/raclette/members/1",
+ headers=self.get_auth("raclette"))
self.assertStatus(200, req)
# the list of members should be empty
# get the list of members (should be empty)
- req = self.app.get("/api/projects/raclette/members",
- headers=self.get_auth("raclette"))
+ req = self.client.get("/api/projects/raclette/members",
+ headers=self.get_auth("raclette"))
self.assertStatus(200, req)
self.assertEqual('[]', req.data.decode('utf-8'))
@@ -1000,28 +999,28 @@ class APITestCase(TestCase):
self.api_add_member("raclette", "arnaud")
# get the list of bills (should be empty)
- req = self.app.get("/api/projects/raclette/bills",
- headers=self.get_auth("raclette"))
+ req = self.client.get("/api/projects/raclette/bills",
+ headers=self.get_auth("raclette"))
self.assertStatus(200, req)
self.assertEqual("[]", req.data.decode('utf-8'))
# add a bill
- req = self.app.post("/api/projects/raclette/bills", data={
+ 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"))
+ }, headers=self.get_auth("raclette"))
# should return the id
self.assertStatus(201, req)
self.assertEqual(req.data.decode('utf-8'), "1")
# get this bill details
- req = self.app.get("/api/projects/raclette/bills/1",
- headers=self.get_auth("raclette"))
+ req = self.client.get("/api/projects/raclette/bills/1",
+ headers=self.get_auth("raclette"))
# compare with the added info
self.assertStatus(200, req)
@@ -1038,35 +1037,35 @@ class APITestCase(TestCase):
self.assertDictEqual(expected, json.loads(req.data.decode('utf-8')))
# the list of bills should lenght 1
- req = self.app.get("/api/projects/raclette/bills",
- headers=self.get_auth("raclette"))
+ req = self.client.get("/api/projects/raclette/bills",
+ headers=self.get_auth("raclette"))
self.assertStatus(200, req)
self.assertEqual(1, len(json.loads(req.data.decode('utf-8'))))
# edit with errors should return an error
- req = self.app.put("/api/projects/raclette/bills/1", data={
+ req = self.client.put("/api/projects/raclette/bills/1", data={
'date': '201111111-08-10', # not a date
'what': 'fromage',
'payer': "1",
'payed_for': ["1", "2"],
'amount': '25',
- }, headers=self.get_auth("raclette"))
+ }, headers=self.get_auth("raclette"))
self.assertStatus(400, req)
self.assertEqual('{"date": ["This field is required."]}', req.data.decode('utf-8'))
# edit a bill
- req = self.app.put("/api/projects/raclette/bills/1", data={
+ req = self.client.put("/api/projects/raclette/bills/1", data={
'date': '2011-09-10',
'what': 'beer',
'payer': "2",
'payed_for': ["1", "2"],
'amount': '25',
- }, headers=self.get_auth("raclette"))
+ }, headers=self.get_auth("raclette"))
# check its fields
- req = self.app.get("/api/projects/raclette/bills/1",
- headers=self.get_auth("raclette"))
+ req = self.client.get("/api/projects/raclette/bills/1",
+ headers=self.get_auth("raclette"))
expected = {
"what": "beer",
@@ -1081,25 +1080,25 @@ class APITestCase(TestCase):
self.assertDictEqual(expected, json.loads(req.data.decode('utf-8')))
# delete a bill
- req = self.app.delete("/api/projects/raclette/bills/1",
- headers=self.get_auth("raclette"))
+ req = self.client.delete("/api/projects/raclette/bills/1",
+ headers=self.get_auth("raclette"))
self.assertStatus(200, req)
# getting it should return a 404
- req = self.app.get("/api/projects/raclette/bills/1",
- headers=self.get_auth("raclette"))
+ req = self.client.get("/api/projects/raclette/bills/1",
+ headers=self.get_auth("raclette"))
self.assertStatus(404, req)
def test_username_xss(self):
# create a project
- #self.api_create("raclette")
+ # self.api_create("raclette")
self.post_project("raclette")
self.login("raclette")
# add members
self.api_add_member("raclette", "<script>")
- result = self.app.get('/raclette/')
+ result = self.client.get('/raclette/')
self.assertNotIn("<script>", result.data.decode('utf-8'))
def test_weighted_bills(self):
@@ -1112,17 +1111,17 @@ class APITestCase(TestCase):
self.api_add_member("raclette", "arnaud")
# add a bill
- req = self.app.post("/api/projects/raclette/bills", data={
+ 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"))
+ }, headers=self.get_auth("raclette"))
# get this bill details
- req = self.app.get("/api/projects/raclette/bills/1",
- headers=self.get_auth("raclette"))
+ req = self.client.get("/api/projects/raclette/bills/1",
+ headers=self.get_auth("raclette"))
# compare with the added info
self.assertStatus(200, req)
@@ -1138,8 +1137,8 @@ class APITestCase(TestCase):
self.assertDictEqual(expected, json.loads(req.data.decode('utf-8')))
# getting it should return a 404
- req = self.app.get("/api/projects/raclette",
- headers=self.get_auth("raclette"))
+ req = self.client.get("/api/projects/raclette",
+ headers=self.get_auth("raclette"))
expected = {
"active_members": [
@@ -1162,20 +1161,19 @@ class APITestCase(TestCase):
self.assertStatus(200, req)
self.assertEqual(expected, json.loads(req.data.decode('utf-8')))
+
class ServerTestCase(APITestCase):
- def setUp(self):
- run.configure()
- super(ServerTestCase, self).setUp()
def test_unprefixed(self):
- run.app.config['APPLICATION_ROOT'] = '/'
- req = self.app.get("/foo/")
+ self.app.config['APPLICATION_ROOT'] = '/'
+ req = self.client.get("/foo/")
self.assertStatus(303, req)
def test_prefixed(self):
- run.app.config['APPLICATION_ROOT'] = '/foo'
- req = self.app.get("/foo/")
+ self.app.config['APPLICATION_ROOT'] = '/foo'
+ req = self.client.get("/foo/")
self.assertStatus(200, req)
+
if __name__ == "__main__":
unittest.main()
diff --git a/budget/translations/fr/LC_MESSAGES/messages.mo b/ihatemoney/translations/fr/LC_MESSAGES/messages.mo
index 210852b..210852b 100644
--- a/budget/translations/fr/LC_MESSAGES/messages.mo
+++ b/ihatemoney/translations/fr/LC_MESSAGES/messages.mo
Binary files differ
diff --git a/budget/translations/fr/LC_MESSAGES/messages.po b/ihatemoney/translations/fr/LC_MESSAGES/messages.po
index 0f3339e..0f3339e 100644
--- a/budget/translations/fr/LC_MESSAGES/messages.po
+++ b/ihatemoney/translations/fr/LC_MESSAGES/messages.po
diff --git a/budget/utils.py b/ihatemoney/utils.py
index 0e6251b..4e79a37 100644
--- a/budget/utils.py
+++ b/ihatemoney/utils.py
@@ -1,6 +1,5 @@
import base64
import re
-import inspect
from io import BytesIO, StringIO
from jinja2 import filters
@@ -26,7 +25,9 @@ def slugify(value):
value = six.text_type(re.sub('[^\w\s-]', '', value).strip().lower())
return re.sub('[-\s]+', '-', value)
+
class Redirect303(HTTPException, RoutingException):
+
"""Raise if the map requests a redirect. This is for example the case if
`strict_slashes` are activated and an url that requires a trailing slash.
@@ -43,6 +44,7 @@ class Redirect303(HTTPException, RoutingException):
class PrefixedWSGI(object):
+
'''
Wrap the application in this middleware and configure the
front-end server to add these headers, to let you quietly bind
@@ -55,6 +57,7 @@ class PrefixedWSGI(object):
:param app: the WSGI application
'''
+
def __init__(self, app):
self.app = app
self.wsgi_app = app.wsgi_app
@@ -85,12 +88,14 @@ def minimal_round(*args, **kw):
ires = int(res)
return (res if res != ires else ires)
+
def list_of_dicts2json(dict_to_convert):
"""Take a list of dictionnaries and turns it into
a json in-memory file
"""
return BytesIO(dumps(dict_to_convert).encode('utf-8'))
+
def list_of_dicts2csv(dict_to_convert):
"""Take a list of dictionnaries and turns it into
a csv in-memory file, assume all dict have the same keys
@@ -110,9 +115,10 @@ def list_of_dicts2csv(dict_to_convert):
csv_data = []
csv_data.append([key.encode('utf-8') for key in dict_to_convert[0].keys()])
for dic in dict_to_convert:
- csv_data.append([dic[h].encode('utf8')
- if isinstance(dic[h], unicode) else str(dic[h]).encode('utf8')
- for h in dict_to_convert[0].keys()])
+ csv_data.append(
+ [dic[h].encode('utf8')
+ if isinstance(dic[h], unicode) else str(dic[h]).encode('utf8') # NOQA
+ for h in dict_to_convert[0].keys()])
except (KeyError, IndexError):
csv_data = []
writer = csv.writer(csv_file)
@@ -122,5 +128,6 @@ def list_of_dicts2csv(dict_to_convert):
csv_file = BytesIO(csv_file.getvalue().encode('utf-8'))
return csv_file
+
# base64 encoding that works with both py2 and py3 and yield no warning
-base64_encode = base64.encodestring if six.PY2 else base64.encodebytes
+base64_encode = base64.encodestring if six.PY2 else base64.encodebytes
diff --git a/budget/web.py b/ihatemoney/web.py
index ba77124..65c0ed6 100644
--- a/budget/web.py
+++ b/ihatemoney/web.py
@@ -13,25 +13,23 @@ from flask import (
Blueprint, current_app, flash, g, redirect, render_template, request,
session, url_for, send_file
)
-from flask_mail import Mail, Message
+from flask_mail import Message
from flask_babel import get_locale, gettext as _
-from werkzeug.security import generate_password_hash, \
- check_password_hash
+from werkzeug.security import check_password_hash
from smtplib import SMTPRecipientsRefused
import werkzeug
from sqlalchemy import orm
from functools import wraps
-from .models import db, Project, Person, Bill
-from .forms import (
+from ihatemoney.models import db, Project, Person, Bill
+from ihatemoney.forms import (
AdminAuthenticationForm, AuthenticationForm, EditProjectForm,
InviteForm, MemberForm, PasswordReminder, ProjectForm, get_billform_for,
ExportForm
)
-from .utils import Redirect303, list_of_dicts2json, list_of_dicts2csv
+from ihatemoney.utils import Redirect303, list_of_dicts2json, list_of_dicts2csv
main = Blueprint("main", __name__)
-mail = Mail()
def requires_admin(f):
@@ -75,14 +73,14 @@ def pull_project(endpoint, values):
project = Project.query.get(project_id)
if not project:
raise Redirect303(url_for(".create_project",
- project_id=project_id))
+ project_id=project_id))
if project.id in session and session[project.id] == project.password:
# add project into kwargs and call the original function
g.project = project
else:
# redirect to authentication page
raise Redirect303(
- url_for(".authenticate", project_id=project_id))
+ url_for(".authenticate", project_id=project_id))
@main.route("/admin", methods=["GET", "POST"])
@@ -110,7 +108,7 @@ def authenticate(project_id=None):
form.id.data = request.args['project_id']
project_id = form.id.data
if project_id is None:
- #User doesn't provide project identifier, return to authenticate form
+ # User doesn't provide project identifier, return to authenticate form
msg = _("You need to enter a project identifier")
form.errors["id"] = [msg]
return render_template("authenticate.html", form=form)
@@ -150,7 +148,7 @@ def authenticate(project_id=None):
return redirect(url_for(".list_bills"))
return render_template("authenticate.html", form=form,
- create_project=create_project)
+ create_project=create_project)
@main.route("/")
@@ -196,16 +194,16 @@ def create_project():
g.project = project
message_title = _("You have just created '%(project)s' "
- "to share your expenses", project=g.project.name)
+ "to share your expenses", project=g.project.name)
message_body = render_template("reminder_mail.%s" %
- get_locale().language)
+ get_locale().language)
msg = Message(message_title,
- body=message_body,
- recipients=[project.contact_email])
+ body=message_body,
+ recipients=[project.contact_email])
try:
- mail.send(msg)
+ current_app.mail.send(msg)
except SMTPRecipientsRefused:
msg_compl = 'Problem sending mail. '
# TODO: destroy the project and cancel instead?
@@ -214,7 +212,7 @@ def create_project():
# redirect the user to the next step (invite)
flash(_("%(msg_compl)sThe project identifier is %(project)s",
- msg_compl=msg_compl, project=project.id))
+ msg_compl=msg_compl, project=project.id))
return redirect(url_for(".invite", project_id=project.id))
return render_template("create_project.html", form=form)
@@ -230,7 +228,8 @@ def remind_password():
# send the password reminder
password_reminder = "password_reminder.%s" % get_locale().language
- mail.send(Message("password recovery",
+ current_app.mail.send(Message(
+ "password recovery",
body=render_template(password_reminder, project=project),
recipients=[project.contact_email]))
flash(_("a mail has been sent to you with the password"))
@@ -270,7 +269,7 @@ def edit_project():
attachment_filename="%s-%s.%s" %
(g.project.id, export_type, export_format),
as_attachment=True
- )
+ )
else:
edit_form.name.data = g.project.name
edit_form.password.data = g.project.password
@@ -311,7 +310,7 @@ def demo():
project_id='demo'))
if not project and is_demo_project_activated:
project = Project(id="demo", name=u"demonstration", password="demo",
- contact_email="demo@notmyidea.org")
+ contact_email="demo@notmyidea.org")
db.session.add(project)
db.session.commit()
session[project.id] = project.password
@@ -329,15 +328,15 @@ def invite():
# send the email
message_body = render_template("invitation_mail.%s" %
- get_locale().language)
+ get_locale().language)
message_title = _("You have been invited to share your "
- "expenses for %(project)s", project=g.project.name)
+ "expenses for %(project)s", project=g.project.name)
msg = Message(message_title,
- body=message_body,
- recipients=[email.strip()
- for email in form.emails.data.split(",")])
- mail.send(msg)
+ body=message_body,
+ recipients=[email.strip()
+ for email in form.emails.data.split(",")])
+ current_app.mail.send(msg)
flash(_("Your invitations have been sent"))
return redirect(url_for(".list_bills"))
@@ -354,11 +353,11 @@ def list_bills():
bills = g.project.get_bills().options(orm.subqueryload(Bill.owers))
return render_template("list_bills.html",
- bills=bills, member_form=MemberForm(g.project),
- bill_form=bill_form,
- add_bill=request.values.get('add_bill', False),
- current_view="list_bills",
- )
+ bills=bills, member_form=MemberForm(g.project),
+ bill_form=bill_form,
+ add_bill=request.values.get('add_bill', False),
+ current_view="list_bills",
+ )
@main.route("/<project_id>/members/add", methods=["GET", "POST"])
@@ -378,7 +377,7 @@ def add_member():
@main.route("/<project_id>/members/<member_id>/reactivate", methods=["POST"])
def reactivate(member_id):
person = Person.query.filter(Person.id == member_id)\
- .filter(Project.id == g.project.id).all()
+ .filter(Project.id == g.project.id).all()
if person:
person[0].activated = True
db.session.commit()
@@ -390,7 +389,7 @@ def reactivate(member_id):
def remove_member(member_id):
member = g.project.remove_member(member_id)
if member:
- if member.activated == False:
+ if not member.activated:
flash(_("User '%(name)s' has been deactivated. It will still "
"appear in the users list until its balance "
"becomes zero.", name=member.name))
diff --git a/ihatemoney/wsgi.py b/ihatemoney/wsgi.py
new file mode 100644
index 0000000..d537568
--- /dev/null
+++ b/ihatemoney/wsgi.py
@@ -0,0 +1,3 @@
+from ihatemoney.run import create_app
+
+application = create_app()
diff --git a/setup.py b/setup.py
index c70a31d..f346248 100644
--- a/setup.py
+++ b/setup.py
@@ -35,7 +35,10 @@ DEPENDENCY_LINKS = [
ENTRY_POINTS = {
'paste.app_factory': [
- 'main = budget.run:main',
+ 'main = ihatemoney.run:main',
+ ],
+ 'console_scripts': [
+ 'ihatemoney = ihatemoney.manage:main'
],
}
diff --git a/tox.ini b/tox.ini
index b918bdb..63af830 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,12 +1,12 @@
[tox]
-envlist = py35,py34,py27,docs
+envlist = py35,py34,py27,docs,lint
skip_missing_interpreters = True
[testenv]
commands =
python --version
- py.test budget/tests/tests.py
+ py.test ihatemoney/tests/tests.py
deps =
-rdev-requirements.txt
-rrequirements.txt
@@ -17,3 +17,12 @@ install_command = pip install --pre {opts} {packages}
commands = sphinx-build -a -n -b html -d docs/_build/doctrees docs docs/_build/html
deps =
-rdocs/requirements.txt
+
+[testenv:lint]
+commands = flake8 ihatemoney
+deps =
+ -rdev-requirements.txt
+
+[flake8]
+exclude = migrations
+max_line_length = 100