Absolute imports & some other improvements (#243)

* Use absolute imports and rename package to ihatemoney
* Add a ihatemoney command
* Factorize application creation logic
* Refactor the tests
* Update the wsgi.py module with the new create_app() function
* Fix some styling thanks to Flake8.
* Automate Flake8 check in the CI.
This commit is contained in:
Alexis Metaireau 2017-07-07 00:06:56 +02:00 committed by GitHub
parent 0e374cd5e0
commit 3a4282fd75
82 changed files with 613 additions and 524 deletions

View file

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

View file

@ -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)

View file

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

View file

@ -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()

View file

@ -1 +0,0 @@
from run import app as application

View file

@ -1,3 +1,5 @@
zest.releaser
tox
pytest
Flask-Testing
Flake8

View file

@ -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``.

View file

@ -2,8 +2,8 @@
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,
from ihatemoney.models import db, Project, Person, Bill
from ihatemoney.forms import (ProjectForm, EditProjectForm, MemberForm,
get_billform_for)

View file

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

View file

@ -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(',', '.')
@ -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):
@ -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):
@ -179,13 +183,13 @@ class InviteForm(FlaskForm):
class ExportForm(FlaskForm):
export_type = SelectField(_("What do you want to download ?"),
export_type = SelectField(
_("What do you want to download ?"),
validators=[Required()],
coerce=str,
choices=[("bills", _("bills")), ("transactions", _("transactions"))]
)
export_format = SelectField(_("Export file format"),
choices=[("bills", _("bills")), ("transactions", _("transactions"))])
export_format = SelectField(
_("Export file format"),
validators=[Required()],
coerce=str,
choices=[("csv", "csv"), ("json", "json")]
)
choices=[("csv", "csv"), ("json", "json")])

View file

@ -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)
def main():
app = create_app()
Migrate(app, db)
manager = Manager(app)
manager.add_command('db', MigrateCommand)
manager.add_command('generate_password_hash', GeneratePasswordHash)
def main():
manager.run()

View file

@ -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)
@ -37,7 +36,8 @@ class Project(db.Model):
# 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,9 +64,11 @@ class Project(db.Model):
return transactions
pretty_transactions = []
for transaction in transactions:
pretty_transactions.append({'ower': transaction['ower'].name,
pretty_transactions.append({
'ower': transaction['ower'].name,
'receiver': transaction['receiver'].name,
'amount': round(transaction['amount'], 2)})
'amount': round(transaction['amount'], 2)
})
return pretty_transactions
# cache value for better performance
@ -77,22 +80,36 @@ class Project(db.Model):
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]
@ -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,
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})
"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):

144
ihatemoney/run.py Normal file
View file

@ -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()

View file

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View file

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View file

Before

Width:  |  Height:  |  Size: 274 B

After

Width:  |  Height:  |  Size: 274 B

View file

Before

Width:  |  Height:  |  Size: 226 B

After

Width:  |  Height:  |  Size: 226 B

View file

Before

Width:  |  Height:  |  Size: 258 B

After

Width:  |  Height:  |  Size: 258 B

View file

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 259 B

After

Width:  |  Height:  |  Size: 259 B

File diff suppressed because it is too large Load diff

View file

@ -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,8 +115,9 @@ 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')
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 = []
@ -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

View file

@ -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):
@ -205,7 +203,7 @@ def create_project():
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?
@ -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"))
@ -337,7 +336,7 @@ def invite():
body=message_body,
recipients=[email.strip()
for email in form.emails.data.split(",")])
mail.send(msg)
current_app.mail.send(msg)
flash(_("Your invitations have been sent"))
return redirect(url_for(".list_bills"))
@ -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))

3
ihatemoney/wsgi.py Normal file
View file

@ -0,0 +1,3 @@
from ihatemoney.run import create_app
application = create_app()

View file

@ -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'
],
}

13
tox.ini
View file

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