aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/conf.py5
-rw-r--r--ihatemoney/api.py42
-rw-r--r--ihatemoney/default_settings.py4
-rw-r--r--ihatemoney/forms.py72
-rwxr-xr-xihatemoney/manage.py55
-rwxr-xr-xihatemoney/migrations/env.py33
-rw-r--r--ihatemoney/migrations/versions/26d6a218c329_.py8
-rw-r--r--ihatemoney/migrations/versions/6c6fb2b7f229_.py8
-rw-r--r--ihatemoney/migrations/versions/a67119aa3ee5_migrate_negative_weights.py26
-rw-r--r--ihatemoney/migrations/versions/afbf27e6ef20_add_bill_import_date_field.py8
-rw-r--r--ihatemoney/migrations/versions/b78f8a8bdb16_hash_project_passwords.py25
-rw-r--r--ihatemoney/migrations/versions/b9a10d5d63ce_.py85
-rw-r--r--ihatemoney/migrations/versions/f629c8ef4ab0_initialize_all_members_weights_to_1.py25
-rw-r--r--ihatemoney/models.py186
-rw-r--r--ihatemoney/run.py73
-rw-r--r--ihatemoney/tests/tests.py1600
-rw-r--r--ihatemoney/web.py329
17 files changed, 1494 insertions, 1090 deletions
diff --git a/docs/conf.py b/docs/conf.py
index 4f26391..82c0e03 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -5,11 +5,10 @@ templates_path = ["_templates"]
source_suffix = ".rst"
master_doc = "index"
-project = u"I hate money"
-copyright = u"2011, The 'I hate money' team"
+project = "I hate money"
+copyright = "2011, The 'I hate money' team"
version = "1.0"
release = "1.0"
exclude_patterns = ["_build"]
pygments_style = "sphinx"
-
diff --git a/ihatemoney/api.py b/ihatemoney/api.py
index 00ebe21..dc2f598 100644
--- a/ihatemoney/api.py
+++ b/ihatemoney/api.py
@@ -5,8 +5,7 @@ from flask_cors import CORS
from wtforms.fields.core import BooleanField
from ihatemoney.models import db, Project, Person, Bill
-from ihatemoney.forms import (ProjectForm, EditProjectForm, MemberForm,
- get_billform_for)
+from ihatemoney.forms import ProjectForm, EditProjectForm, MemberForm, get_billform_for
from werkzeug.security import check_password_hash
from functools import wraps
@@ -21,6 +20,7 @@ def need_auth(f):
Return the project if the authorization is good, abort the request with a 401 otherwise
"""
+
@wraps(f)
def wrapper(*args, **kwargs):
auth = request.authorization
@@ -35,25 +35,26 @@ def need_auth(f):
return f(*args, project=project, **kwargs)
else:
# Use Bearer token Auth
- auth_header = request.headers.get('Authorization', '')
- auth_token = ''
+ auth_header = request.headers.get("Authorization", "")
+ auth_token = ""
try:
auth_token = auth_header.split(" ")[1]
except IndexError:
abort(401)
- project_id = Project.verify_token(auth_token, token_type='non_timed_token')
+ project_id = Project.verify_token(auth_token, token_type="non_timed_token")
if auth_token and project_id:
project = Project.query.get(project_id)
if project:
kwargs.pop("project_id")
return f(*args, project=project, **kwargs)
abort(401)
+
return wrapper
class ProjectsHandler(Resource):
def post(self):
- form = ProjectForm(meta={'csrf': False})
+ form = ProjectForm(meta={"csrf": False})
if form.validate():
project = form.save()
db.session.add(project)
@@ -74,7 +75,7 @@ class ProjectHandler(Resource):
return "DELETED"
def put(self, project):
- form = EditProjectForm(meta={'csrf': False})
+ form = EditProjectForm(meta={"csrf": False})
if form.validate():
form.update(project)
db.session.commit()
@@ -94,7 +95,8 @@ class APIMemberForm(MemberForm):
But we want Member.enabled to be togglable via the API.
"""
- activated = BooleanField(false_values=('false', '', 'False'))
+
+ activated = BooleanField(false_values=("false", "", "False"))
def save(self, project, person):
person.activated = self.activated.data
@@ -108,7 +110,7 @@ class MembersHandler(Resource):
return project.members
def post(self, project):
- form = MemberForm(project, meta={'csrf': False})
+ form = MemberForm(project, meta={"csrf": False})
if form.validate():
member = Person()
form.save(project, member)
@@ -127,7 +129,7 @@ class MemberHandler(Resource):
return member
def put(self, project, member_id):
- form = APIMemberForm(project, meta={'csrf': False}, edit=True)
+ form = APIMemberForm(project, meta={"csrf": False}, edit=True)
if form.validate():
member = Person.query.get(member_id, project)
form.save(project, member)
@@ -148,7 +150,7 @@ class BillsHandler(Resource):
return project.get_bills().all()
def post(self, project):
- form = get_billform_for(project, True, meta={'csrf': False})
+ form = get_billform_for(project, True, meta={"csrf": False})
if form.validate():
bill = Bill()
form.save(bill, project)
@@ -168,7 +170,7 @@ class BillHandler(Resource):
return bill, 200
def put(self, project, bill_id):
- form = get_billform_for(project, True, meta={'csrf': False})
+ form = get_billform_for(project, True, meta={"csrf": False})
if form.validate():
bill = Bill.query.get(project, bill_id)
form.save(bill, project)
@@ -184,10 +186,16 @@ class BillHandler(Resource):
return "OK", 200
-restful_api.add_resource(ProjectsHandler, '/projects')
-restful_api.add_resource(ProjectHandler, '/projects/<string:project_id>')
+restful_api.add_resource(ProjectsHandler, "/projects")
+restful_api.add_resource(ProjectHandler, "/projects/<string:project_id>")
restful_api.add_resource(MembersHandler, "/projects/<string:project_id>/members")
-restful_api.add_resource(ProjectStatsHandler, "/projects/<string:project_id>/statistics")
-restful_api.add_resource(MemberHandler, "/projects/<string:project_id>/members/<int:member_id>")
+restful_api.add_resource(
+ ProjectStatsHandler, "/projects/<string:project_id>/statistics"
+)
+restful_api.add_resource(
+ MemberHandler, "/projects/<string:project_id>/members/<int:member_id>"
+)
restful_api.add_resource(BillsHandler, "/projects/<string:project_id>/bills")
-restful_api.add_resource(BillHandler, "/projects/<string:project_id>/bills/<int:bill_id>")
+restful_api.add_resource(
+ BillHandler, "/projects/<string:project_id>/bills/<int:bill_id>"
+)
diff --git a/ihatemoney/default_settings.py b/ihatemoney/default_settings.py
index f1aaa34..7ce4422 100644
--- a/ihatemoney/default_settings.py
+++ b/ihatemoney/default_settings.py
@@ -1,6 +1,6 @@
# Verbose and documented settings are in conf-templates/ihatemoney.cfg.j2
DEBUG = SQLACHEMY_ECHO = False
-SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/ihatemoney.db'
+SQLALCHEMY_DATABASE_URI = "sqlite:////tmp/ihatemoney.db"
SQLALCHEMY_TRACK_MODIFICATIONS = False
SECRET_KEY = "tralala"
MAIL_DEFAULT_SENDER = ("Budget manager", "budget@notmyidea.org")
@@ -8,4 +8,4 @@ ACTIVATE_DEMO_PROJECT = True
ADMIN_PASSWORD = ""
ALLOW_PUBLIC_PROJECT_CREATION = True
ACTIVATE_ADMIN_DASHBOARD = False
-SUPPORTED_LANGUAGES = ['en', 'fr', 'de', 'nl', 'es_419']
+SUPPORTED_LANGUAGES = ["en", "fr", "de", "nl", "es_419"]
diff --git a/ihatemoney/forms.py b/ihatemoney/forms.py
index 1e0ba00..c9b0547 100644
--- a/ihatemoney/forms.py
+++ b/ihatemoney/forms.py
@@ -2,7 +2,14 @@ from flask_wtf.form import FlaskForm
from wtforms.fields.core import SelectField, SelectMultipleField
from wtforms.fields.html5 import DateField, DecimalField, URLField
from wtforms.fields.simple import PasswordField, SubmitField, TextAreaField, StringField
-from wtforms.validators import Email, DataRequired, ValidationError, EqualTo, NumberRange, Optional
+from wtforms.validators import (
+ Email,
+ DataRequired,
+ ValidationError,
+ EqualTo,
+ NumberRange,
+ Optional,
+)
from flask_babel import lazy_gettext as _
from flask import request
from werkzeug.security import generate_password_hash
@@ -48,7 +55,7 @@ class CommaDecimalField(DecimalField):
def process_formdata(self, value):
if value:
- value[0] = str(value[0]).replace(',', '.')
+ value[0] = str(value[0]).replace(",", ".")
return super(CommaDecimalField, self).process_formdata(value)
@@ -68,7 +75,7 @@ class CalculatorStringField(StringField):
value = str(valuelist[0]).replace(",", ".")
# avoid exponents to prevent expensive calculations i.e 2**9999999999**9999999
- if not match(r'^[ 0-9\.\+\-\*/\(\)]{0,200}$', value) or "**" in value:
+ if not match(r"^[ 0-9\.\+\-\*/\(\)]{0,200}$", value) or "**" in value:
raise ValueError(Markup(message))
valuelist[0] = str(eval_arithmetic_expression(value))
@@ -86,9 +93,12 @@ class EditProjectForm(FlaskForm):
Returns the created instance
"""
- project = Project(name=self.name.data, id=self.id.data,
- password=generate_password_hash(self.password.data),
- contact_email=self.contact_email.data)
+ project = Project(
+ name=self.name.data,
+ id=self.id.data,
+ password=generate_password_hash(self.password.data),
+ contact_email=self.contact_email.data,
+ )
return project
def update(self, project):
@@ -108,8 +118,11 @@ 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):
- message = _("A project with this identifier (\"%(project)s\") already exists. "
- "Please choose a new identifier", project=form.id.data)
+ message = _(
+ 'A project with this identifier ("%(project)s") already exists. '
+ "Please choose a new identifier",
+ project=form.id.data,
+ )
raise ValidationError(Markup(message))
@@ -134,10 +147,14 @@ class PasswordReminder(FlaskForm):
class ResetPasswordForm(FlaskForm):
- password_validators = [DataRequired(),
- EqualTo('password_confirmation', message=_("Password mismatch"))]
+ password_validators = [
+ DataRequired(),
+ EqualTo("password_confirmation", message=_("Password mismatch")),
+ ]
password = PasswordField(_("Password"), validators=password_validators)
- password_confirmation = PasswordField(_("Password confirmation"), validators=[DataRequired()])
+ password_confirmation = PasswordField(
+ _("Password confirmation"), validators=[DataRequired()]
+ )
submit = SubmitField(_("Reset password"))
@@ -146,10 +163,14 @@ class BillForm(FlaskForm):
what = StringField(_("What?"), validators=[DataRequired()])
payer = SelectField(_("Payer"), validators=[DataRequired()], coerce=int)
amount = CalculatorStringField(_("Amount paid"), validators=[DataRequired()])
- external_link = URLField(_("External link"), validators=[Optional(
- )], description=_("A link to an external document, related to this bill"))
- payed_for = SelectMultipleField(_("For whom?"),
- validators=[DataRequired()], coerce=int)
+ external_link = URLField(
+ _("External link"),
+ validators=[Optional()],
+ description=_("A link to an external document, related to this bill"),
+ )
+ payed_for = SelectMultipleField(
+ _("For whom?"), validators=[DataRequired()], coerce=int
+ )
submit = SubmitField(_("Submit"))
submit2 = SubmitField(_("Submit and add a new one"))
@@ -159,8 +180,7 @@ class BillForm(FlaskForm):
bill.what = self.what.data
bill.external_link = self.external_link.data
bill.date = self.date.data
- bill.owers = [Person.query.get(ower, project)
- for ower in self.payed_for.data]
+ bill.owers = [Person.query.get(ower, project) for ower in self.payed_for.data]
return bill
@@ -181,11 +201,10 @@ class BillForm(FlaskForm):
class MemberForm(FlaskForm):
- name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter, ])
+ name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter])
weight_validators = [NumberRange(min=0.1, message=_("Weights should be positive"))]
- weight = CommaDecimalField(_("Weight"), default=1,
- validators=weight_validators)
+ weight = CommaDecimalField(_("Weight"), default=1, validators=weight_validators)
submit = SubmitField(_("Add"))
def __init__(self, project, edit=False, *args, **kwargs):
@@ -196,10 +215,14 @@ class MemberForm(FlaskForm):
def validate_name(form, field):
if field.data == form.name.default:
raise ValidationError(_("User name incorrect"))
- if (not form.edit and Person.query.filter(
+ if (
+ not form.edit
+ and Person.query.filter(
Person.name == field.data,
Person.project == form.project,
- Person.activated == True).all()): # NOQA
+ Person.activated == True,
+ ).all()
+ ): # NOQA
raise ValidationError(_("This project already have this member"))
def save(self, project, person):
@@ -224,5 +247,6 @@ class InviteForm(FlaskForm):
try:
email_validator.validate_email(email)
except email_validator.EmailNotValidError:
- raise ValidationError(_("The email %(email)s is not valid",
- email=email))
+ raise ValidationError(
+ _("The email %(email)s is not valid", email=email)
+ )
diff --git a/ihatemoney/manage.py b/ihatemoney/manage.py
index 8e73bc9..6343ee7 100755
--- a/ihatemoney/manage.py
+++ b/ihatemoney/manage.py
@@ -19,42 +19,51 @@ class GeneratePasswordHash(Command):
"""Get password from user and hash it without printing it in clear text."""
def run(self):
- password = getpass.getpass(prompt='Password: ')
+ password = getpass.getpass(prompt="Password: ")
print(generate_password_hash(password))
class GenerateConfig(Command):
def get_options(self):
return [
- Option('config_file', choices=[
- 'ihatemoney.cfg',
- 'apache-vhost.conf',
- 'gunicorn.conf.py',
- 'supervisord.conf',
- 'nginx.conf',
- ]),
+ Option(
+ "config_file",
+ choices=[
+ "ihatemoney.cfg",
+ "apache-vhost.conf",
+ "gunicorn.conf.py",
+ "supervisord.conf",
+ "nginx.conf",
+ ],
+ )
]
@staticmethod
def gen_secret_key():
- return ''.join([
- random.SystemRandom().choice(
- 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)')
- for i in range(50)])
+ return "".join(
+ [
+ random.SystemRandom().choice(
+ "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)"
+ )
+ for i in range(50)
+ ]
+ )
def run(self, config_file):
- env = create_jinja_env('conf-templates', strict_rendering=True)
- template = env.get_template('%s.j2' % config_file)
+ env = create_jinja_env("conf-templates", strict_rendering=True)
+ template = env.get_template("%s.j2" % config_file)
bin_path = os.path.dirname(sys.executable)
pkg_path = os.path.abspath(os.path.dirname(__file__))
- print(template.render(
+ print(
+ template.render(
pkg_path=pkg_path,
bin_path=bin_path,
sys_prefix=sys.prefix,
secret_key=self.gen_secret_key(),
- ))
+ )
+ )
class DeleteProject(Command):
@@ -65,14 +74,14 @@ class DeleteProject(Command):
def main():
- QUIET_COMMANDS = ('generate_password_hash', 'generate-config')
+ QUIET_COMMANDS = ("generate_password_hash", "generate-config")
exception = None
backup_stderr = sys.stderr
# Hack to divert stderr for commands generating content to stdout
# to avoid confusing the user
if len(sys.argv) > 1 and sys.argv[1] in QUIET_COMMANDS:
- sys.stderr = open(os.devnull, 'w')
+ sys.stderr = open(os.devnull, "w")
try:
app = create_app()
@@ -87,12 +96,12 @@ def main():
raise exception
manager = Manager(app)
- manager.add_command('db', MigrateCommand)
- manager.add_command('generate_password_hash', GeneratePasswordHash)
- manager.add_command('generate-config', GenerateConfig)
- manager.add_command('delete-project', DeleteProject)
+ manager.add_command("db", MigrateCommand)
+ manager.add_command("generate_password_hash", GeneratePasswordHash)
+ manager.add_command("generate-config", GenerateConfig)
+ manager.add_command("delete-project", DeleteProject)
manager.run()
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
diff --git a/ihatemoney/migrations/env.py b/ihatemoney/migrations/env.py
index e2f9a28..4d4729c 100755
--- a/ihatemoney/migrations/env.py
+++ b/ihatemoney/migrations/env.py
@@ -11,14 +11,16 @@ config = context.config
# Interpret the config file for Python logging. This line sets up loggers
# basically.
fileConfig(config.config_file_name)
-logger = logging.getLogger('alembic.env')
+logger = logging.getLogger("alembic.env")
# Add your model's MetaData object here for 'autogenerate' support from myapp
# import mymodel target_metadata = mymodel.Base.metadata.
from flask import current_app
-config.set_main_option('sqlalchemy.url',
- current_app.config.get('SQLALCHEMY_DATABASE_URI'))
-target_metadata = current_app.extensions['migrate'].db.metadata
+
+config.set_main_option(
+ "sqlalchemy.url", current_app.config.get("SQLALCHEMY_DATABASE_URI")
+)
+target_metadata = current_app.extensions["migrate"].db.metadata
# Other values from the config, defined by the needs of env.py,
# can be acquired:
@@ -57,21 +59,25 @@ def run_migrations_online():
# when there are no changes to the schema.
# reference: https://alembic.readthedocs.io/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
- if getattr(config.cmd_opts, 'autogenerate', False):
+ if getattr(config.cmd_opts, "autogenerate", False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
- logger.info('No changes in schema detected.')
+ logger.info("No changes in schema detected.")
- engine = engine_from_config(config.get_section(config.config_ini_section),
- prefix='sqlalchemy.',
- poolclass=pool.NullPool)
+ engine = engine_from_config(
+ config.get_section(config.config_ini_section),
+ prefix="sqlalchemy.",
+ poolclass=pool.NullPool,
+ )
connection = engine.connect()
- context.configure(connection=connection,
- target_metadata=target_metadata,
- process_revision_directives=process_revision_directives,
- **current_app.extensions['migrate'].configure_args)
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata,
+ process_revision_directives=process_revision_directives,
+ **current_app.extensions["migrate"].configure_args
+ )
try:
with context.begin_transaction():
@@ -79,6 +85,7 @@ def run_migrations_online():
finally:
connection.close()
+
if context.is_offline_mode():
run_migrations_offline()
else:
diff --git a/ihatemoney/migrations/versions/26d6a218c329_.py b/ihatemoney/migrations/versions/26d6a218c329_.py
index 859b9af..6d5e237 100644
--- a/ihatemoney/migrations/versions/26d6a218c329_.py
+++ b/ihatemoney/migrations/versions/26d6a218c329_.py
@@ -7,8 +7,8 @@ Create Date: 2016-06-15 09:22:04.069447
"""
# revision identifiers, used by Alembic.
-revision = '26d6a218c329'
-down_revision = 'b9a10d5d63ce'
+revision = "26d6a218c329"
+down_revision = "b9a10d5d63ce"
from alembic import op
import sqlalchemy as sa
@@ -16,11 +16,11 @@ import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
- op.add_column('person', sa.Column('weight', sa.Float(), nullable=True))
+ op.add_column("person", sa.Column("weight", sa.Float(), nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
- op.drop_column('person', 'weight')
+ op.drop_column("person", "weight")
### end Alembic commands ###
diff --git a/ihatemoney/migrations/versions/6c6fb2b7f229_.py b/ihatemoney/migrations/versions/6c6fb2b7f229_.py
index 0336f6c..da31578 100644
--- a/ihatemoney/migrations/versions/6c6fb2b7f229_.py
+++ b/ihatemoney/migrations/versions/6c6fb2b7f229_.py
@@ -7,8 +7,8 @@ Create Date: 2019-09-28 13:38:09.550747
"""
# revision identifiers, used by Alembic.
-revision = '6c6fb2b7f229'
-down_revision = 'a67119aa3ee5'
+revision = "6c6fb2b7f229"
+down_revision = "a67119aa3ee5"
from alembic import op
import sqlalchemy as sa
@@ -16,11 +16,11 @@ import sqlalchemy as sa
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
- op.add_column('bill', sa.Column('external_link', sa.UnicodeText(), nullable=True))
+ op.add_column("bill", sa.Column("external_link", sa.UnicodeText(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
- op.drop_column('bill', 'external_link')
+ op.drop_column("bill", "external_link")
# ### end Alembic commands ###
diff --git a/ihatemoney/migrations/versions/a67119aa3ee5_migrate_negative_weights.py b/ihatemoney/migrations/versions/a67119aa3ee5_migrate_negative_weights.py
index ec23470..8061896 100644
--- a/ihatemoney/migrations/versions/a67119aa3ee5_migrate_negative_weights.py
+++ b/ihatemoney/migrations/versions/a67119aa3ee5_migrate_negative_weights.py
@@ -7,29 +7,29 @@ Create Date: 2018-12-25 18:34:20.220844
"""
# revision identifiers, used by Alembic.
-revision = 'a67119aa3ee5'
-down_revision = 'afbf27e6ef20'
+revision = "a67119aa3ee5"
+down_revision = "afbf27e6ef20"
from alembic import op
import sqlalchemy as sa
+
# Snapshot of the person table
person_helper = sa.Table(
- 'person', sa.MetaData(),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('project_id', sa.String(length=64), nullable=True),
- sa.Column('name', sa.UnicodeText(), nullable=True),
- sa.Column('activated', sa.Boolean(), nullable=True),
- sa.Column('weight', sa.Float(), nullable=True),
- sa.ForeignKeyConstraint(['project_id'], ['project.id'], ),
- sa.PrimaryKeyConstraint('id')
+ "person",
+ sa.MetaData(),
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("project_id", sa.String(length=64), nullable=True),
+ sa.Column("name", sa.UnicodeText(), nullable=True),
+ sa.Column("activated", sa.Boolean(), nullable=True),
+ sa.Column("weight", sa.Float(), nullable=True),
+ sa.ForeignKeyConstraint(["project_id"], ["project.id"]),
+ sa.PrimaryKeyConstraint("id"),
)
def upgrade():
op.execute(
- person_helper.update()
- .where(person_helper.c.weight <= 0)
- .values(weight=1)
+ person_helper.update().where(person_helper.c.weight <= 0).values(weight=1)
)
diff --git a/ihatemoney/migrations/versions/afbf27e6ef20_add_bill_import_date_field.py b/ihatemoney/migrations/versions/afbf27e6ef20_add_bill_import_date_field.py
index 4179155..0ccfac2 100644
--- a/ihatemoney/migrations/versions/afbf27e6ef20_add_bill_import_date_field.py
+++ b/ihatemoney/migrations/versions/afbf27e6ef20_add_bill_import_date_field.py
@@ -7,8 +7,8 @@ Create Date: 2018-02-19 20:29:26.286136
"""
# revision identifiers, used by Alembic.
-revision = 'afbf27e6ef20'
-down_revision = 'b78f8a8bdb16'
+revision = "afbf27e6ef20"
+down_revision = "b78f8a8bdb16"
from alembic import op
import sqlalchemy as sa
@@ -16,11 +16,11 @@ import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
- op.add_column('bill', sa.Column('creation_date', sa.Date(), nullable=True))
+ op.add_column("bill", sa.Column("creation_date", sa.Date(), nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
- op.drop_column('bill', 'creation_date')
+ op.drop_column("bill", "creation_date")
### end Alembic commands ###
diff --git a/ihatemoney/migrations/versions/b78f8a8bdb16_hash_project_passwords.py b/ihatemoney/migrations/versions/b78f8a8bdb16_hash_project_passwords.py
index e32983d..e730b8d 100644
--- a/ihatemoney/migrations/versions/b78f8a8bdb16_hash_project_passwords.py
+++ b/ihatemoney/migrations/versions/b78f8a8bdb16_hash_project_passwords.py
@@ -7,20 +7,21 @@ Create Date: 2017-12-17 11:45:44.783238
"""
# revision identifiers, used by Alembic.
-revision = 'b78f8a8bdb16'
-down_revision = 'f629c8ef4ab0'
+revision = "b78f8a8bdb16"
+down_revision = "f629c8ef4ab0"
from alembic import op
import sqlalchemy as sa
from werkzeug.security import generate_password_hash
project_helper = sa.Table(
- 'project', sa.MetaData(),
- sa.Column('id', sa.String(length=64), nullable=False),
- sa.Column('name', sa.UnicodeText(), nullable=True),
- sa.Column('password', sa.String(length=128), nullable=True),
- sa.Column('contact_email', sa.String(length=128), nullable=True),
- sa.PrimaryKeyConstraint('id')
+ "project",
+ sa.MetaData(),
+ sa.Column("id", sa.String(length=64), nullable=False),
+ sa.Column("name", sa.UnicodeText(), nullable=True),
+ sa.Column("password", sa.String(length=128), nullable=True),
+ sa.Column("contact_email", sa.String(length=128), nullable=True),
+ sa.PrimaryKeyConstraint("id"),
)
@@ -28,11 +29,9 @@ def upgrade():
connection = op.get_bind()
for project in connection.execute(project_helper.select()):
connection.execute(
- project_helper.update().where(
- project_helper.c.name == project.name
- ).values(
- password=generate_password_hash(project.password)
- )
+ project_helper.update()
+ .where(project_helper.c.name == project.name)
+ .values(password=generate_password_hash(project.password))
)
diff --git a/ihatemoney/migrations/versions/b9a10d5d63ce_.py b/ihatemoney/migrations/versions/b9a10d5d63ce_.py
index 92bb446..3c92780 100644
--- a/ihatemoney/migrations/versions/b9a10d5d63ce_.py
+++ b/ihatemoney/migrations/versions/b9a10d5d63ce_.py
@@ -7,7 +7,7 @@ Create Date: 2016-05-21 23:21:21.605076
"""
# revision identifiers, used by Alembic.
-revision = 'b9a10d5d63ce'
+revision = "b9a10d5d63ce"
down_revision = None
from alembic import op
@@ -16,53 +16,58 @@ import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
- op.create_table('project',
- sa.Column('id', sa.String(length=64), nullable=False),
- sa.Column('name', sa.UnicodeText(), nullable=True),
- sa.Column('password', sa.String(length=128), nullable=True),
- sa.Column('contact_email', sa.String(length=128), nullable=True),
- sa.PrimaryKeyConstraint('id')
+ op.create_table(
+ "project",
+ sa.Column("id", sa.String(length=64), nullable=False),
+ sa.Column("name", sa.UnicodeText(), nullable=True),
+ sa.Column("password", sa.String(length=128), nullable=True),
+ sa.Column("contact_email", sa.String(length=128), nullable=True),
+ sa.PrimaryKeyConstraint("id"),
)
- op.create_table('archive',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('project_id', sa.String(length=64), nullable=True),
- sa.Column('name', sa.UnicodeText(), nullable=True),
- sa.ForeignKeyConstraint(['project_id'], ['project.id'], ),
- sa.PrimaryKeyConstraint('id')
+ op.create_table(
+ "archive",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("project_id", sa.String(length=64), nullable=True),
+ sa.Column("name", sa.UnicodeText(), nullable=True),
+ sa.ForeignKeyConstraint(["project_id"], ["project.id"]),
+ sa.PrimaryKeyConstraint("id"),
)
- op.create_table('person',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('project_id', sa.String(length=64), nullable=True),
- sa.Column('name', sa.UnicodeText(), nullable=True),
- sa.Column('activated', sa.Boolean(), nullable=True),
- sa.ForeignKeyConstraint(['project_id'], ['project.id'], ),
- sa.PrimaryKeyConstraint('id')
+ op.create_table(
+ "person",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("project_id", sa.String(length=64), nullable=True),
+ sa.Column("name", sa.UnicodeText(), nullable=True),
+ sa.Column("activated", sa.Boolean(), nullable=True),
+ sa.ForeignKeyConstraint(["project_id"], ["project.id"]),
+ sa.PrimaryKeyConstraint("id"),
)
- op.create_table('bill',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('payer_id', sa.Integer(), nullable=True),
- sa.Column('amount', sa.Float(), nullable=True),
- sa.Column('date', sa.Date(), nullable=True),
- sa.Column('what', sa.UnicodeText(), nullable=True),
- sa.Column('archive', sa.Integer(), nullable=True),
- sa.ForeignKeyConstraint(['archive'], ['archive.id'], ),
- sa.ForeignKeyConstraint(['payer_id'], ['person.id'], ),
- sa.PrimaryKeyConstraint('id')
+ op.create_table(
+ "bill",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("payer_id", sa.Integer(), nullable=True),
+ sa.Column("amount", sa.Float(), nullable=True),
+ sa.Column("date", sa.Date(), nullable=True),
+ sa.Column("what", sa.UnicodeText(), nullable=True),
+ sa.Column("archive", sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(["archive"], ["archive.id"]),
+ sa.ForeignKeyConstraint(["payer_id"], ["person.id"]),
+ sa.PrimaryKeyConstraint("id"),
)
- op.create_table('billowers',
- sa.Column('bill_id', sa.Integer(), nullable=True),
- sa.Column('person_id', sa.Integer(), nullable=True),
- sa.ForeignKeyConstraint(['bill_id'], ['bill.id'], ),
- sa.ForeignKeyConstraint(['person_id'], ['person.id'], )
+ op.create_table(
+ "billowers",
+ sa.Column("bill_id", sa.Integer(), nullable=True),
+ sa.Column("person_id", sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(["bill_id"], ["bill.id"]),
+ sa.ForeignKeyConstraint(["person_id"], ["person.id"]),
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
- op.drop_table('billowers')
- op.drop_table('bill')
- op.drop_table('person')
- op.drop_table('archive')
- op.drop_table('project')
+ op.drop_table("billowers")
+ op.drop_table("bill")
+ op.drop_table("person")
+ op.drop_table("archive")
+ op.drop_table("project")
### end Alembic commands ###
diff --git a/ihatemoney/migrations/versions/f629c8ef4ab0_initialize_all_members_weights_to_1.py b/ihatemoney/migrations/versions/f629c8ef4ab0_initialize_all_members_weights_to_1.py
index 5542146..481c2d9 100644
--- a/ihatemoney/migrations/versions/f629c8ef4ab0_initialize_all_members_weights_to_1.py
+++ b/ihatemoney/migrations/versions/f629c8ef4ab0_initialize_all_members_weights_to_1.py
@@ -7,30 +7,29 @@ Create Date: 2016-06-15 09:40:30.400862
"""
# revision identifiers, used by Alembic.
-revision = 'f629c8ef4ab0'
-down_revision = '26d6a218c329'
+revision = "f629c8ef4ab0"
+down_revision = "26d6a218c329"
from alembic import op
import sqlalchemy as sa
# Snapshot of the person table
person_helper = sa.Table(
- 'person', sa.MetaData(),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('project_id', sa.String(length=64), nullable=True),
- sa.Column('name', sa.UnicodeText(), nullable=True),
- sa.Column('activated', sa.Boolean(), nullable=True),
- sa.Column('weight', sa.Float(), nullable=True),
- sa.ForeignKeyConstraint(['project_id'], ['project.id'], ),
- sa.PrimaryKeyConstraint('id')
+ "person",
+ sa.MetaData(),
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("project_id", sa.String(length=64), nullable=True),
+ sa.Column("name", sa.UnicodeText(), nullable=True),
+ sa.Column("activated", sa.Boolean(), nullable=True),
+ sa.Column("weight", sa.Float(), nullable=True),
+ sa.ForeignKeyConstraint(["project_id"], ["project.id"]),
+ sa.PrimaryKeyConstraint("id"),
)
def upgrade():
op.execute(
- person_helper.update()
- .where(person_helper.c.weight == None)
- .values(weight=1)
+ person_helper.update().where(person_helper.c.weight == None).values(weight=1)
)
diff --git a/ihatemoney/models.py b/ihatemoney/models.py
index 48e16a7..250f009 100644
--- a/ihatemoney/models.py
+++ b/ihatemoney/models.py
@@ -6,8 +6,12 @@ from flask import g, current_app
from debts import settle
from sqlalchemy import orm
-from itsdangerous import (TimedJSONWebSignatureSerializer, URLSafeSerializer,
- BadSignature, SignatureExpired)
+from itsdangerous import (
+ TimedJSONWebSignatureSerializer,
+ URLSafeSerializer,
+ BadSignature,
+ SignatureExpired,
+)
db = SQLAlchemy()
@@ -33,8 +37,8 @@ class Project(db.Model):
balance = self.balance
for member in self.members:
member_obj = member._to_serialize
- member_obj['balance'] = balance.get(member.id, 0)
- obj['members'].append(member_obj)
+ member_obj["balance"] = balance.get(member.id, 0)
+ obj["members"].append(member_obj)
return obj
@@ -45,14 +49,14 @@ class Project(db.Model):
@property
def balance(self):
- balances, should_pay, should_receive = (defaultdict(int)
- for time in (1, 2, 3))
+ balances, should_pay, should_receive = (defaultdict(int) 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))
+ Bill.owers.contains(person)
+ )
for bill in bills.all():
if person != bill.payer:
share = bill.pay_each() * person.weight
@@ -72,18 +76,23 @@ class Project(db.Model):
:return: one stat dict per member
:rtype list:
"""
- return [{
- 'member': member,
- 'paid': sum([
- bill.amount
- for bill in self.get_member_bills(member.id).all()
- ]),
- 'spent': sum([
- bill.pay_each() * member.weight
- for bill in self.get_bills().all() if member in bill.owers
- ]),
- 'balance': self.balance[member.id]
- } for member in self.active_members]
+ return [
+ {
+ "member": member,
+ "paid": sum(
+ [bill.amount for bill in self.get_member_bills(member.id).all()]
+ ),
+ "spent": sum(
+ [
+ bill.pay_each() * member.weight
+ for bill in self.get_bills().all()
+ if member in bill.owers
+ ]
+ ),
+ "balance": self.balance[member.id],
+ }
+ for member in self.active_members
+ ]
@property
def uses_weights(self):
@@ -99,22 +108,27 @@ 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
members = {person.id: person for person in self.members}
settle_plan = settle(self.balance.items()) or []
- transactions = [{
- 'ower': members[ower_id],
- 'receiver': members[receiver_id],
- 'amount': amount
- } for ower_id, amount, receiver_id in settle_plan]
+ transactions = [
+ {
+ "ower": members[ower_id],
+ "receiver": members[receiver_id],
+ "amount": amount,
+ }
+ for ower_id, amount, receiver_id in settle_plan
+ ]
return prettify(transactions, pretty_output)
@@ -140,23 +154,27 @@ class Project(db.Model):
def get_bills(self):
"""Return the list of bills related to this project"""
- return Bill.query.join(Person, Project)\
- .filter(Bill.payer_id == Person.id)\
- .filter(Person.project_id == Project.id)\
- .filter(Project.id == self.id)\
- .order_by(Bill.date.desc())\
- .order_by(Bill.creation_date.desc())\
+ return (
+ Bill.query.join(Person, Project)
+ .filter(Bill.payer_id == Person.id)
+ .filter(Person.project_id == Project.id)
+ .filter(Project.id == self.id)
+ .order_by(Bill.date.desc())
+ .order_by(Bill.creation_date.desc())
.order_by(Bill.id.desc())
+ )
def get_member_bills(self, member_id):
"""Return the list of bills related to a specific member"""
- return Bill.query.join(Person, Project)\
- .filter(Bill.payer_id == Person.id)\
- .filter(Person.project_id == Project.id)\
- .filter(Person.id == member_id)\
- .filter(Project.id == self.id)\
- .order_by(Bill.date.desc())\
+ return (
+ Bill.query.join(Person, Project)
+ .filter(Bill.payer_id == Person.id)
+ .filter(Person.project_id == Project.id)
+ .filter(Person.id == member_id)
+ .filter(Project.id == self.id)
+ .order_by(Bill.date.desc())
.order_by(Bill.id.desc())
+ )
def get_pretty_bills(self, export_format="json"):
"""Return a list of project's bills with pretty formatting"""
@@ -166,16 +184,18 @@ class Project(db.Model):
if export_format == "json":
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
- })
+ 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,
+ }
+ )
return pretty_bills
def remove_member(self, member_id):
@@ -210,12 +230,12 @@ class Project(db.Model):
"""
if expiration:
serializer = TimedJSONWebSignatureSerializer(
- current_app.config['SECRET_KEY'],
- expiration)
- token = serializer.dumps({'project_id': self.id}).decode('utf-8')
+ current_app.config["SECRET_KEY"], expiration
+ )
+ token = serializer.dumps({"project_id": self.id}).decode("utf-8")
else:
- serializer = URLSafeSerializer(current_app.config['SECRET_KEY'])
- token = serializer.dumps({'project_id': self.id})
+ serializer = URLSafeSerializer(current_app.config["SECRET_KEY"])
+ token = serializer.dumps({"project_id": self.id})
return token
@staticmethod
@@ -226,34 +246,40 @@ class Project(db.Model):
:param token: Serialized TimedJsonWebToken
"""
if token_type == "timed_token":
- serializer = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
+ serializer = TimedJSONWebSignatureSerializer(
+ current_app.config["SECRET_KEY"]
+ )
else:
- serializer = URLSafeSerializer(current_app.config['SECRET_KEY'])
+ serializer = URLSafeSerializer(current_app.config["SECRET_KEY"])
try:
data = serializer.loads(token)
except SignatureExpired:
return None
except BadSignature:
return None
- return data['project_id']
+ return data["project_id"]
def __repr__(self):
return "<Project %s>" % self.name
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()
+ return (
+ Person.query.filter(Person.name == name)
+ .filter(Project.id == project.id)
+ .one()
+ )
def get(self, id, project=None):
if not project:
project = g.project
- return Person.query.filter(Person.id == id)\
- .filter(Project.id == project.id).one()
+ return (
+ Person.query.filter(Person.id == id)
+ .filter(Project.id == project.id)
+ .one()
+ )
query_class = PersonQuery
@@ -276,9 +302,11 @@ class Person(db.Model):
def has_bills(self):
"""return if the user do have bills or not"""
- bills_as_ower_number = db.session.query(billowers)\
- .filter(billowers.columns.get("person_id") == self.id)\
+ bills_as_ower_number = (
+ db.session.query(billowers)
+ .filter(billowers.columns.get("person_id") == self.id)
.count()
+ )
return bills_as_ower_number != 0 or len(self.bills) != 0
def __str__(self):
@@ -290,23 +318,24 @@ class Person(db.Model):
# We need to manually define a join table for m2m relations
billowers = db.Table(
- 'billowers',
- db.Column('bill_id', db.Integer, db.ForeignKey('bill.id')),
- db.Column('person_id', db.Integer, db.ForeignKey('person.id')),
+ "billowers",
+ db.Column("bill_id", db.Integer, db.ForeignKey("bill.id")),
+ db.Column("person_id", db.Integer, db.ForeignKey("person.id")),
)
class Bill(db.Model):
-
class BillQuery(BaseQuery):
-
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
@@ -356,7 +385,8 @@ class Bill(db.Model):
def __repr__(self):
return "<Bill of %s from %s for %s>" % (
self.amount,
- self.payer, ", ".join([o.name for o in self.owers])
+ self.payer,
+ ", ".join([o.name for o in self.owers]),
)
diff --git a/ihatemoney/run.py b/ihatemoney/run.py
index 9ef3198..6d1e032 100644
--- a/ihatemoney/run.py
+++ b/ihatemoney/run.py
@@ -10,8 +10,13 @@ from werkzeug.middleware.proxy_fix import ProxyFix
from ihatemoney.api import api
from ihatemoney.models import db
-from ihatemoney.utils import (IhmJSONEncoder, PrefixedWSGI, locale_from_iso,
- minimal_round, static_include)
+from ihatemoney.utils import (
+ IhmJSONEncoder,
+ PrefixedWSGI,
+ locale_from_iso,
+ minimal_round,
+ static_include,
+)
from ihatemoney.web import main as web_interface
from ihatemoney import default_settings
@@ -24,27 +29,27 @@ def setup_database(app):
""" 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')
+ 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
- sqlalchemy_url = app.config.get('SQLALCHEMY_DATABASE_URI')
- if sqlalchemy_url.startswith('sqlite:////tmp'):
+ sqlalchemy_url = app.config.get("SQLALCHEMY_DATABASE_URI")
+ if sqlalchemy_url.startswith("sqlite:////tmp"):
warnings.warn(
- 'The database is currently stored in /tmp and might be lost at '
- 'next reboot.'
+ "The database is currently stored in /tmp and might be lost at "
+ "next reboot."
)
db.init_app(app)
db.app = app
Migrate(app, db)
- migrations_path = os.path.join(app.root_path, 'migrations')
+ 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')
+ stamp(migrations_path, revision="b9a10d5d63ce")
# auto-execute migrations on runtime
with app.app_context():
@@ -60,38 +65,38 @@ def load_configuration(app, configuration=None):
- Otherwise, load the default settings.
"""
- env_var_config = os.environ.get('IHATEMONEY_SETTINGS_FILE_PATH')
- app.config.from_object('ihatemoney.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)
+ app.config.from_pyfile("ihatemoney.cfg", silent=True)
# Configure custom JSONEncoder used by the API
- app.config['RESTFUL_JSON'] = {'cls': IhmJSONEncoder}
+ app.config["RESTFUL_JSON"] = {"cls": IhmJSONEncoder}
def validate_configuration(app):
- if app.config['SECRET_KEY'] == default_settings.SECRET_KEY:
+ 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
+ UserWarning,
)
# Deprecations
- if 'DEFAULT_MAIL_SENDER' in app.config:
+ 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
+ UserWarning,
)
- if 'MAIL_DEFAULT_SENDER' not in app.config:
- app.config['MAIL_DEFAULT_SENDER'] = default_settings.DEFAULT_MAIL_SENDER
+ if "MAIL_DEFAULT_SENDER" not in app.config:
+ app.config["MAIL_DEFAULT_SENDER"] = default_settings.DEFAULT_MAIL_SENDER
- if "pbkdf2:" not in app.config['ADMIN_PASSWORD'] and app.config['ADMIN_PASSWORD']:
+ if "pbkdf2:" 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"
@@ -99,20 +104,22 @@ def validate_configuration(app):
+ " 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
+ UserWarning,
)
def page_not_found(e):
- return render_template('404.html', root="main"), 404
+ return render_template("404.html", root="main"), 404
-def create_app(configuration=None, instance_path='/etc/ihatemoney',
- instance_relative_config=True):
+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)
+ instance_relative_config=instance_relative_config,
+ )
# If a configuration object is passed, use it. Otherwise try to find one.
load_configuration(app, configuration)
@@ -136,9 +143,9 @@ def create_app(configuration=None, instance_path='/etc/ihatemoney',
app.mail = mail
# Jinja filters
- app.jinja_env.globals['static_include'] = static_include
- app.jinja_env.globals['locale_from_iso'] = locale_from_iso
- app.jinja_env.filters['minimal_round'] = minimal_round
+ app.jinja_env.globals["static_include"] = static_include
+ app.jinja_env.globals["locale_from_iso"] = locale_from_iso
+ app.jinja_env.filters["minimal_round"] = minimal_round
# Translations
babel = Babel(app)
@@ -148,10 +155,10 @@ def create_app(configuration=None, instance_path='/etc/ihatemoney',
# get the lang from the session if defined, fallback on the browser "accept
# languages" header.
lang = session.get(
- 'lang',
- request.accept_languages.best_match(app.config['SUPPORTED_LANGUAGES'])
+ "lang",
+ request.accept_languages.best_match(app.config["SUPPORTED_LANGUAGES"]),
)
- setattr(g, 'lang', lang)
+ setattr(g, "lang", lang)
return lang
return app
@@ -162,5 +169,5 @@ def main():
app.run(host="0.0.0.0", debug=True)
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
diff --git a/ihatemoney/tests/tests.py b/ihatemoney/tests/tests.py
index 3ff8a72..1767475 100644
--- a/ihatemoney/tests/tests.py
+++ b/ihatemoney/tests/tests.py
@@ -16,13 +16,12 @@ from flask import session
from flask_testing import TestCase
from ihatemoney.run import create_app, db, load_configuration
-from ihatemoney.manage import (
- GenerateConfig, GeneratePasswordHash, DeleteProject)
+from ihatemoney.manage import GenerateConfig, GeneratePasswordHash, DeleteProject
from ihatemoney import models
from ihatemoney import utils
# Unset configuration file env var if previously set
-os.environ.pop('IHATEMONEY_SETTINGS_FILE_PATH', None)
+os.environ.pop("IHATEMONEY_SETTINGS_FILE_PATH", None)
__HERE__ = os.path.dirname(os.path.abspath(__file__))
@@ -46,25 +45,32 @@ class BaseTestCase(TestCase):
def login(self, project, password=None, test_client=None):
password = password or project
- return self.client.post('/authenticate', data=dict(
- id=project, password=password), follow_redirects=True)
+ 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.client.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):
project = models.Project(
id=name,
name=str(name),
password=generate_password_hash(name),
- contact_email="%s@notmyidea.org" % name)
+ contact_email="%s@notmyidea.org" % name,
+ )
models.db.session.add(project)
models.db.session.commit()
@@ -75,46 +81,53 @@ class IhatemoneyTestCase(BaseTestCase):
WTF_CSRF_ENABLED = False # Simplifies the tests.
def assertStatus(self, expected, resp, url=""):
- return self.assertEqual(expected, resp.status_code,
- "%s expected %s, got %s" % (url, expected, resp.status_code))
+ return self.assertEqual(
+ expected,
+ resp.status_code,
+ "%s expected %s, got %s" % (url, expected, resp.status_code),
+ )
class ConfigurationTestCase(BaseTestCase):
-
def test_default_configuration(self):
"""Test that default settings are loaded when no other configuration file is specified"""
- self.assertFalse(self.app.config['DEBUG'])
- self.assertEqual(self.app.config['SQLALCHEMY_DATABASE_URI'], 'sqlite:////tmp/ihatemoney.db')
- self.assertFalse(self.app.config['SQLALCHEMY_TRACK_MODIFICATIONS'])
- self.assertEqual(self.app.config['MAIL_DEFAULT_SENDER'],
- ("Budget manager", "budget@notmyidea.org"))
+ self.assertFalse(self.app.config["DEBUG"])
+ self.assertEqual(
+ self.app.config["SQLALCHEMY_DATABASE_URI"], "sqlite:////tmp/ihatemoney.db"
+ )
+ 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")
+ os.environ["IHATEMONEY_SETTINGS_FILE_PATH"] = os.path.join(
+ __HERE__, "ihatemoney_envvar.cfg"
+ )
load_configuration(self.app)
- self.assertEqual(self.app.config['SECRET_KEY'], 'lalatra')
+ self.assertEqual(self.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")
+ os.environ["IHATEMONEY_SETTINGS_FILE_PATH"] = os.path.join(
+ __HERE__, "ihatemoney_envvar.cfg"
+ )
self.app.config.root_path = __HERE__
load_configuration(self.app)
- self.assertEqual(self.app.config['SECRET_KEY'], 'lalatra')
+ self.assertEqual(self.app.config["SECRET_KEY"], "lalatra")
- os.environ.pop('IHATEMONEY_SETTINGS_FILE_PATH', None)
+ os.environ.pop("IHATEMONEY_SETTINGS_FILE_PATH", None)
def test_default_configuration_file(self):
"""Test that settings are loaded from the default configuration file"""
self.app.config.root_path = __HERE__
load_configuration(self.app)
- self.assertEqual(self.app.config['SECRET_KEY'], 'supersecret')
+ self.assertEqual(self.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.
@@ -126,8 +139,9 @@ class BudgetTestCase(IhatemoneyTestCase):
self.login("raclette")
self.post_project("raclette")
- self.client.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"])
@@ -135,25 +149,28 @@ class BudgetTestCase(IhatemoneyTestCase):
# sending a message to multiple persons
with self.app.mail.record_messages() as outbox:
- self.client.post("/raclette/invite",
- data={"emails": 'alexis@notmyidea.org, toto@notmyidea.org'})
+ 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"])
+ self.assertEqual(
+ outbox[0].recipients, ["alexis@notmyidea.org", "toto@notmyidea.org"]
+ )
# mail address checking
with self.app.mail.record_messages() as outbox:
- response = self.client.post("/raclette/invite",
- data={"emails": "toto"})
+ 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'))
+ self.assertIn("The email toto is not valid", response.data.decode("utf-8"))
# mixing good and wrong adresses shouldn't send any messages
with self.app.mail.record_messages() as outbox:
- self.client.post("/raclette/invite",
- data={"emails": 'alexis@notmyidea.org, alexis'}) # not valid
+ 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)
@@ -164,25 +181,24 @@ class BudgetTestCase(IhatemoneyTestCase):
self.login("raclette")
self.post_project("raclette")
with self.app.mail.record_messages() as outbox:
- self.client.post("/raclette/invite",
- data={"emails": 'toto@notmyidea.org'})
+ self.client.post("/raclette/invite", data={"emails": "toto@notmyidea.org"})
self.assertEqual(len(outbox), 1)
- url_start = outbox[0].body.find('You can log in using this link: ') + 32
- url_end = outbox[0].body.find('.\n', url_start)
+ url_start = outbox[0].body.find("You can log in using this link: ") + 32
+ url_end = outbox[0].body.find(".\n", url_start)
url = outbox[0].body[url_start:url_end]
self.client.get("/exit")
# Test that we got a valid token
resp = self.client.get(url, follow_redirects=True)
self.assertIn(
'You probably want to <a href="/raclette/members/add"',
- resp.data.decode('utf-8')
+ resp.data.decode("utf-8"),
)
# Test empty and invalid tokens
self.client.get("/exit")
resp = self.client.get("/authenticate")
- self.assertIn("You either provided a bad token", resp.data.decode('utf-8'))
+ self.assertIn("You either provided a bad token", resp.data.decode("utf-8"))
resp = self.client.get("/authenticate?token=token")
- self.assertIn("You either provided a bad token", resp.data.decode('utf-8'))
+ self.assertIn("You either provided a bad token", resp.data.decode("utf-8"))
def test_password_reminder(self):
# test that it is possible to have an email cotaining the password of a
@@ -209,48 +225,58 @@ class BudgetTestCase(IhatemoneyTestCase):
with self.app.mail.record_messages() as outbox:
self.client.post("/password-reminder", data={"id": "raclette"})
self.assertEqual(len(outbox), 1)
- url_start = outbox[0].body.find('You can reset it here: ') + 23
- url_end = outbox[0].body.find('.\n', url_start)
+ url_start = outbox[0].body.find("You can reset it here: ") + 23
+ url_end = outbox[0].body.find(".\n", url_start)
url = outbox[0].body[url_start:url_end]
# Test that we got a valid token
resp = self.client.get(url)
- self.assertIn("Password confirmation</label>", resp.data.decode('utf-8'))
+ self.assertIn("Password confirmation</label>", resp.data.decode("utf-8"))
# Test that password can be changed
- self.client.post(url, data={'password': 'pass', 'password_confirmation': 'pass'})
- resp = self.login('raclette', password='pass')
- self.assertIn("<title>Account manager - raclette</title>", resp.data.decode('utf-8'))
+ self.client.post(
+ url, data={"password": "pass", "password_confirmation": "pass"}
+ )
+ resp = self.login("raclette", password="pass")
+ self.assertIn(
+ "<title>Account manager - raclette</title>", resp.data.decode("utf-8")
+ )
# Test empty and null tokens
resp = self.client.get("/reset-password")
- self.assertIn("No token provided", resp.data.decode('utf-8'))
+ self.assertIn("No token provided", resp.data.decode("utf-8"))
resp = self.client.get("/reset-password?token=token")
- self.assertIn("Invalid token", resp.data.decode('utf-8'))
+ self.assertIn("Invalid token", resp.data.decode("utf-8"))
def test_project_creation(self):
with self.app.test_client() as c:
# add a valid project
- c.post("/create", data={
- 'name': 'The fabulous raclette party',
- 'id': 'raclette',
- 'password': 'party',
- 'contact_email': 'raclette@notmyidea.org'
- })
+ c.post(
+ "/create",
+ data={
+ "name": "The fabulous raclette party",
+ "id": "raclette",
+ "password": "party",
+ "contact_email": "raclette@notmyidea.org",
+ },
+ )
# session is updated
- self.assertTrue(session['raclette'])
+ self.assertTrue(session["raclette"])
# project is created
self.assertEqual(len(models.Project.query.all()), 1)
# Add a second project with the same id
- models.Project.query.get('raclette')
+ models.Project.query.get("raclette")
- c.post("/create", data={
- 'name': 'Another raclette party',
- 'id': 'raclette', # already used !
- 'password': 'party',
- 'contact_email': 'raclette@notmyidea.org'
- })
+ c.post(
+ "/create",
+ data={
+ "name": "Another raclette party",
+ "id": "raclette", # already used !
+ "password": "party",
+ "contact_email": "raclette@notmyidea.org",
+ },
+ )
# no new project added
self.assertEqual(len(models.Project.query.all()), 1)
@@ -258,17 +284,20 @@ class BudgetTestCase(IhatemoneyTestCase):
def test_project_deletion(self):
with self.app.test_client() as c:
- c.post("/create", data={
- 'name': 'raclette party',
- 'id': 'raclette',
- 'password': 'party',
- 'contact_email': 'raclette@notmyidea.org'
- })
+ c.post(
+ "/create",
+ data={
+ "name": "raclette party",
+ "id": "raclette",
+ "password": "party",
+ "contact_email": "raclette@notmyidea.org",
+ },
+ )
# project added
self.assertEqual(len(models.Project.query.all()), 1)
- c.get('/raclette/delete')
+ c.get("/raclette/delete")
# project removed
self.assertEqual(len(models.Project.query.all()), 0)
@@ -282,18 +311,16 @@ class BudgetTestCase(IhatemoneyTestCase):
# Empty bill list and no members, should now propose to add members first
self.assertIn(
'You probably want to <a href="/raclette/members/add"',
- result.data.decode('utf-8')
+ result.data.decode("utf-8"),
)
- result = self.client.post("/raclette/members/add",
- data={'name': 'alexis'})
+ result = self.client.post("/raclette/members/add", data={"name": "alexis"})
result = self.client.get("/raclette/")
# Empty bill with member, list should now propose to add bills
self.assertIn(
- 'You probably want to <a href="/raclette/add"',
- result.data.decode('utf-8')
+ 'You probably want to <a href="/raclette/add"', result.data.decode("utf-8")
)
def test_membership(self):
@@ -301,92 +328,98 @@ class BudgetTestCase(IhatemoneyTestCase):
self.login("raclette")
# adds a member to this project
- self.client.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.client.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.client.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.client.get("/raclette/")
- self.assertIn("fred", result.data.decode('utf-8'))
+ self.assertIn("fred", result.data.decode("utf-8"))
# remove fred
- self.client.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.client.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.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'fromage à raclette',
- 'payer': fred_id,
- 'payed_for': [fred_id, ],
- 'amount': '25',
- })
+ result = self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "fromage à raclette",
+ "payer": fred_id,
+ "payed_for": [fred_id],
+ "amount": "25",
+ },
+ )
# remove fred
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)
+ self.assertEqual(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.client.get("/raclette/")
- self.assertNotIn(("/raclette/members/%s/delete" % fred_id), result.data.decode('utf-8'))
+ self.assertNotIn(
+ ("/raclette/members/%s/delete" % fred_id), result.data.decode("utf-8")
+ )
result = self.client.get("/raclette/add")
- self.assertNotIn("fred", result.data.decode('utf-8'))
+ self.assertNotIn("fred", result.data.decode("utf-8"))
# adding him again should reactivate him
- self.client.post("/raclette/members/add", data={'name': 'fred'})
- self.assertEqual(
- len(models.Project.query.get("raclette").active_members), 2)
+ self.client.post("/raclette/members/add", data={"name": "fred"})
+ self.assertEqual(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.client.post("/randomid/members/add", data={'name': 'fred'})
- self.assertEqual(
- len(models.Project.query.get("randomid").active_members), 1)
+ self.client.post("/randomid/members/add", data={"name": "fred"})
+ self.assertEqual(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.client.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.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'fromage à raclette',
- 'payer': alexis.id,
- 'payed_for': [alexis.id, ],
- 'amount': '25',
- })
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "fromage à raclette",
+ "payer": alexis.id,
+ "payed_for": [alexis.id],
+ "amount": "25",
+ },
+ )
# should have a bill now
alexis = models.Project.query.get("raclette").members[-1]
@@ -397,7 +430,7 @@ class BudgetTestCase(IhatemoneyTestCase):
self.login("raclette")
# adds a member to this project
- self.client.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.client.get("/raclette/members/1/delete")
@@ -405,8 +438,7 @@ class BudgetTestCase(IhatemoneyTestCase):
# delete user using POST method
self.client.post("/raclette/members/1/delete")
- self.assertEqual(
- len(models.Project.query.get("raclette").active_members), 0)
+ self.assertEqual(len(models.Project.query.get("raclette").active_members), 0)
# try to delete an user already deleted
self.client.post("/raclette/members/1/delete")
@@ -417,17 +449,17 @@ class BudgetTestCase(IhatemoneyTestCase):
self.assertTrue(models.Project.query.get("demo") is not None)
def test_deactivated_demo(self):
- self.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.client.get("/demo")
- self.assertIn('<a href="/create?project_id=demo">', resp.data.decode('utf-8'))
+ 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.client.post("/authenticate")
- self.assertIn("Authentication", resp.data.decode('utf-8'))
+ self.assertIn("Authentication", resp.data.decode("utf-8"))
# raclette that the login / logout process works
self.create_project("raclette")
@@ -435,110 +467,128 @@ class BudgetTestCase(IhatemoneyTestCase):
# try to see the project while not being authenticated should redirect
# to the authentication page
resp = self.client.get("/raclette", follow_redirects=True)
- self.assertIn("Authentication", resp.data.decode('utf-8'))
+ self.assertIn("Authentication", resp.data.decode("utf-8"))
# try to connect with wrong credentials should not work
with self.app.test_client() as c:
- resp = c.post("/authenticate",
- data={'id': 'raclette', 'password': 'nope'})
+ resp = c.post("/authenticate", data={"id": "raclette", "password": "nope"})
- self.assertIn("Authentication", resp.data.decode('utf-8'))
- self.assertNotIn('raclette', session)
+ self.assertIn("Authentication", resp.data.decode("utf-8"))
+ self.assertNotIn("raclette", session)
# try to connect with the right credentials should work
with self.app.test_client() as c:
- resp = c.post("/authenticate",
- data={'id': 'raclette', 'password': 'raclette'})
+ resp = c.post(
+ "/authenticate", data={"id": "raclette", "password": "raclette"}
+ )
- self.assertNotIn("Authentication", resp.data.decode('utf-8'))
- self.assertIn('raclette', session)
- self.assertTrue(session['raclette'])
+ self.assertNotIn("Authentication", resp.data.decode("utf-8"))
+ self.assertIn("raclette", session)
+ self.assertTrue(session["raclette"])
# logout should wipe the session out
c.get("/exit")
- self.assertNotIn('raclette', session)
+ self.assertNotIn("raclette", session)
# test that with admin credentials, one can access every project
- self.app.config['ADMIN_PASSWORD'] = generate_password_hash("pass")
+ self.app.config["ADMIN_PASSWORD"] = generate_password_hash("pass")
with self.app.test_client() as c:
- resp = c.post("/admin?goto=%2Fraclette", data={'admin_password': 'pass'})
- self.assertNotIn("Authentication", resp.data.decode('utf-8'))
- self.assertTrue(session['is_admin'])
+ resp = c.post("/admin?goto=%2Fraclette", data={"admin_password": "pass"})
+ self.assertNotIn("Authentication", resp.data.decode("utf-8"))
+ self.assertTrue(session["is_admin"])
def test_admin_authentication(self):
- self.app.config['ADMIN_PASSWORD'] = generate_password_hash("pass")
+ self.app.config["ADMIN_PASSWORD"] = generate_password_hash("pass")
# Disable public project creation so we have an admin endpoint to test
- self.app.config['ALLOW_PUBLIC_PROJECT_CREATION'] = False
+ self.app.config["ALLOW_PUBLIC_PROJECT_CREATION"] = False
# test the redirection to the authentication page when trying to access admin endpoints
resp = self.client.get("/create")
- self.assertIn('<a href="/admin?goto=%2Fcreate">', resp.data.decode('utf-8'))
+ self.assertIn('<a href="/admin?goto=%2Fcreate">', resp.data.decode("utf-8"))
# test right password
- resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'pass'})
- self.assertIn('<a href="/create">/create</a>', resp.data.decode('utf-8'))
+ 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.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'})
- self.assertNotIn('<a href="/create">/create</a>', resp.data.decode('utf-8'))
+ 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.client.post("/admin?goto=%2Fcreate", data={'admin_password': ''})
- self.assertNotIn('<a href="/create">/create</a>', resp.data.decode('utf-8'))
+ resp = self.client.post("/admin?goto=%2Fcreate", data={"admin_password": ""})
+ self.assertNotIn('<a href="/create">/create</a>', resp.data.decode("utf-8"))
def test_login_throttler(self):
- self.app.config['ADMIN_PASSWORD'] = generate_password_hash("pass")
+ self.app.config["ADMIN_PASSWORD"] = generate_password_hash("pass")
# Activate admin login throttling by authenticating 4 times with a wrong passsword
- self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'})
- self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'})
- self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'})
- resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'})
+ self.client.post("/admin?goto=%2Fcreate", data={"admin_password": "wrong"})
+ self.client.post("/admin?goto=%2Fcreate", data={"admin_password": "wrong"})
+ self.client.post("/admin?goto=%2Fcreate", data={"admin_password": "wrong"})
+ resp = self.client.post(
+ "/admin?goto=%2Fcreate", data={"admin_password": "wrong"}
+ )
- self.assertIn('Too many failed login attempts, please retry later.',
- resp.data.decode('utf-8'))
+ self.assertIn(
+ "Too many failed login attempts, please retry later.",
+ resp.data.decode("utf-8"),
+ )
# Change throttling delay
import gc
+
for obj in gc.get_objects():
if isinstance(obj, utils.LoginThrottler):
obj._delay = 0.005
break
# Wait for delay to expire and retry logging in
sleep(1)
- resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'})
- self.assertNotIn('Too many failed login attempts, please retry later.',
- resp.data.decode('utf-8'))
+ resp = self.client.post(
+ "/admin?goto=%2Fcreate", data={"admin_password": "wrong"}
+ )
+ self.assertNotIn(
+ "Too many failed login attempts, please retry later.",
+ resp.data.decode("utf-8"),
+ )
def test_manage_bills(self):
self.post_project("raclette")
# add two persons
- 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": "alexis"})
+ self.client.post("/raclette/members/add", data={"name": "fred"})
- members_ids = [m.id for m in
- models.Project.query.get("raclette").members]
+ members_ids = [m.id for m in models.Project.query.get("raclette").members]
# create a bill
- self.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'fromage à raclette',
- 'payer': members_ids[0],
- 'payed_for': members_ids,
- 'amount': '25',
- })
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "fromage à raclette",
+ "payer": members_ids[0],
+ "payed_for": members_ids,
+ "amount": "25",
+ },
+ )
models.Project.query.get("raclette")
bill = models.Bill.query.one()
self.assertEqual(bill.amount, 25)
# edit the bill
- self.client.post("/raclette/edit/%s" % bill.id, data={
- 'date': '2011-08-10',
- 'what': 'fromage à raclette',
- 'payer': members_ids[0],
- 'payed_for': members_ids,
- 'amount': '10',
- })
+ self.client.post(
+ "/raclette/edit/%s" % bill.id,
+ data={
+ "date": "2011-08-10",
+ "what": "fromage à raclette",
+ "payer": members_ids[0],
+ "payed_for": members_ids,
+ "amount": "10",
+ },
+ )
bill = models.Bill.query.one()
self.assertEqual(bill.amount, 10, "bill edition")
@@ -548,81 +598,103 @@ class BudgetTestCase(IhatemoneyTestCase):
self.assertEqual(0, len(models.Bill.query.all()), "bill deletion")
# test balance
- self.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'fromage à raclette',
- 'payer': members_ids[0],
- 'payed_for': members_ids,
- 'amount': '19',
- })
-
- self.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'fromage à raclette',
- 'payer': members_ids[1],
- 'payed_for': members_ids[0],
- 'amount': '20',
- })
-
- self.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'fromage à raclette',
- 'payer': members_ids[1],
- 'payed_for': members_ids,
- 'amount': '17',
- })
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "fromage à raclette",
+ "payer": members_ids[0],
+ "payed_for": members_ids,
+ "amount": "19",
+ },
+ )
+
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "fromage à raclette",
+ "payer": members_ids[1],
+ "payed_for": members_ids[0],
+ "amount": "20",
+ },
+ )
+
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "fromage à raclette",
+ "payer": members_ids[1],
+ "payed_for": members_ids,
+ "amount": "17",
+ },
+ )
balance = models.Project.query.get("raclette").balance
self.assertEqual(set(balance.values()), set([19.0, -19.0]))
# Bill with negative amount
- self.client.post("/raclette/add", data={
- 'date': '2011-08-12',
- 'what': 'fromage à raclette',
- 'payer': members_ids[0],
- 'payed_for': members_ids,
- 'amount': '-25'
- })
- bill = models.Bill.query.filter(models.Bill.date == '2011-08-12')[0]
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-12",
+ "what": "fromage à raclette",
+ "payer": members_ids[0],
+ "payed_for": members_ids,
+ "amount": "-25",
+ },
+ )
+ bill = models.Bill.query.filter(models.Bill.date == "2011-08-12")[0]
self.assertEqual(bill.amount, -25)
# add a bill with a comma
- self.client.post("/raclette/add", data={
- 'date': '2011-08-01',
- 'what': 'fromage à raclette',
- 'payer': members_ids[0],
- 'payed_for': members_ids,
- 'amount': '25,02',
- })
- bill = models.Bill.query.filter(models.Bill.date == '2011-08-01')[0]
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-01",
+ "what": "fromage à raclette",
+ "payer": members_ids[0],
+ "payed_for": members_ids,
+ "amount": "25,02",
+ },
+ )
+ bill = models.Bill.query.filter(models.Bill.date == "2011-08-01")[0]
self.assertEqual(bill.amount, 25.02)
def test_weighted_balance(self):
self.post_project("raclette")
# add two persons
- self.client.post("/raclette/members/add", data={'name': 'alexis'})
- self.client.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]
+ members_ids = [m.id for m in models.Project.query.get("raclette").members]
# test balance
- self.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'fromage à raclette',
- 'payer': members_ids[0],
- 'payed_for': members_ids,
- 'amount': '10',
- })
-
- self.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'pommes de terre',
- 'payer': members_ids[1],
- 'payed_for': members_ids,
- 'amount': '10',
- })
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "fromage à raclette",
+ "payer": members_ids[0],
+ "payed_for": members_ids,
+ "amount": "10",
+ },
+ )
+
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "pommes de terre",
+ "payer": members_ids[1],
+ "payed_for": members_ids,
+ "amount": "10",
+ },
+ )
balance = models.Project.query.get("raclette").balance
self.assertEqual(set(balance.values()), set([6, -6]))
@@ -631,8 +703,8 @@ class BudgetTestCase(IhatemoneyTestCase):
self.post_project("raclette")
# Add two times the same person (with a space at the end).
- self.client.post("/raclette/members/add", data={'name': 'alexis'})
- self.client.post("/raclette/members/add", data={'name': 'alexis '})
+ self.client.post("/raclette/members/add", data={"name": "alexis"})
+ self.client.post("/raclette/members/add", data={"name": "alexis "})
members = models.Project.query.get("raclette").members
self.assertEqual(len(members), 1)
@@ -641,61 +713,74 @@ class BudgetTestCase(IhatemoneyTestCase):
self.post_project("raclette")
# add two persons
- self.client.post("/raclette/members/add", data={'name': 'alexis'})
- self.client.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.client.get("/raclette/")
- self.assertIn('extra-info', resp.data.decode('utf-8'))
+ self.assertIn("extra-info", resp.data.decode("utf-8"))
- self.client.post("/raclette/members/add", data={'name': 'freddy familly', 'weight': 4})
+ self.client.post(
+ "/raclette/members/add", data={"name": "freddy familly", "weight": 4}
+ )
resp = self.client.get("/raclette/")
- self.assertNotIn('extra-info', resp.data.decode('utf-8'))
+ self.assertNotIn("extra-info", resp.data.decode("utf-8"))
def test_negative_weight(self):
self.post_project("raclette")
# Add one user and edit it to have a negative share
- self.client.post("/raclette/members/add", data={'name': 'alexis'})
- resp = self.client.post("/raclette/members/1/edit", data={'name': 'alexis', 'weight': -1})
+ self.client.post("/raclette/members/add", data={"name": "alexis"})
+ resp = self.client.post(
+ "/raclette/members/1/edit", data={"name": "alexis", "weight": -1}
+ )
# An error should be generated, and its weight should still be 1.
- self.assertIn('<p class="alert alert-danger">', resp.data.decode('utf-8'))
- self.assertEqual(len(models.Project.query.get('raclette').members), 1)
- self.assertEqual(models.Project.query.get('raclette').members[0].weight, 1)
+ self.assertIn('<p class="alert alert-danger">', resp.data.decode("utf-8"))
+ self.assertEqual(len(models.Project.query.get("raclette").members), 1)
+ self.assertEqual(models.Project.query.get("raclette").members[0].weight, 1)
def test_rounding(self):
self.post_project("raclette")
# add members
- 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'})
+ 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.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'fromage à raclette',
- 'payer': 1,
- 'payed_for': [1, 2, 3],
- 'amount': '24.36',
- })
-
- self.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'red wine',
- 'payer': 2,
- 'payed_for': [1],
- 'amount': '19.12',
- })
-
- self.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'delicatessen',
- 'payer': 1,
- 'payed_for': [1, 2],
- 'amount': '22',
- })
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "fromage à raclette",
+ "payer": 1,
+ "payed_for": [1, 2, 3],
+ "amount": "24.36",
+ },
+ )
+
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "red wine",
+ "payer": 2,
+ "payed_for": [1],
+ "amount": "19.12",
+ },
+ )
+
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "delicatessen",
+ "payer": 1,
+ "payed_for": [1, 2],
+ "amount": "22",
+ },
+ )
balance = models.Project.query.get("raclette").balance
result = {}
@@ -714,45 +799,46 @@ class BudgetTestCase(IhatemoneyTestCase):
self.post_project("raclette")
new_data = {
- 'name': 'Super raclette party!',
- 'contact_email': 'alexis@notmyidea.org',
- 'password': 'didoudida'
+ "name": "Super raclette party!",
+ "contact_email": "alexis@notmyidea.org",
+ "password": "didoudida",
}
- resp = self.client.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")
- self.assertEqual(project.name, new_data['name'])
- self.assertEqual(project.contact_email, new_data['contact_email'])
- self.assertTrue(check_password_hash(project.password, new_data['password']))
+ self.assertEqual(project.name, new_data["name"])
+ self.assertEqual(project.contact_email, new_data["contact_email"])
+ self.assertTrue(check_password_hash(project.password, new_data["password"]))
# Editing a project with a wrong email address should fail
- new_data['contact_email'] = 'wrong_email'
+ new_data["contact_email"] = "wrong_email"
- resp = self.client.post("/raclette/edit", data=new_data,
- follow_redirects=True)
- self.assertIn("Invalid email address", resp.data.decode('utf-8'))
+ 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):
# test that the dashboard is deactivated by default
resp = self.client.post(
"/admin?goto=%2Fdashboard",
- data={'admin_password': 'adminpass'},
- follow_redirects=True
+ data={"admin_password": "adminpass"},
+ follow_redirects=True,
)
- self.assertIn('<div class="alert alert-danger">', resp.data.decode('utf-8'))
+ self.assertIn('<div class="alert alert-danger">', resp.data.decode("utf-8"))
# test access to the dashboard when it is activated
- self.app.config['ACTIVATE_ADMIN_DASHBOARD'] = True
- self.app.config['ADMIN_PASSWORD'] = generate_password_hash("adminpass")
+ self.app.config["ACTIVATE_ADMIN_DASHBOARD"] = True
+ self.app.config["ADMIN_PASSWORD"] = generate_password_hash("adminpass")
resp = self.client.post(
"/admin?goto=%2Fdashboard",
- data={'admin_password': 'adminpass'},
- follow_redirects=True
+ data={"admin_password": "adminpass"},
+ follow_redirects=True,
+ )
+ self.assertIn(
+ "<thead><tr><th>Project</th><th>Number of members",
+ resp.data.decode("utf-8"),
)
- self.assertIn('<thead><tr><th>Project</th><th>Number of members', resp.data.decode('utf-8'))
def test_statistics_page(self):
self.post_project("raclette")
@@ -763,58 +849,75 @@ class BudgetTestCase(IhatemoneyTestCase):
self.post_project("raclette")
# add members
- 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": "alexis", "weight": 2})
+ 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'})
+ self.client.post("/raclette/members/add", data={"name": "toto"})
# create bills
- self.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'fromage à raclette',
- 'payer': 1,
- 'payed_for': [1, 2, 3],
- 'amount': '10.0',
- })
-
- self.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'red wine',
- 'payer': 2,
- 'payed_for': [1],
- 'amount': '20',
- })
-
- self.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'delicatessen',
- 'payer': 1,
- 'payed_for': [1, 2],
- 'amount': '10',
- })
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "fromage à raclette",
+ "payer": 1,
+ "payed_for": [1, 2, 3],
+ "amount": "10.0",
+ },
+ )
+
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "red wine",
+ "payer": 2,
+ "payed_for": [1],
+ "amount": "20",
+ },
+ )
+
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "delicatessen",
+ "payer": 1,
+ "payed_for": [1, 2],
+ "amount": "10",
+ },
+ )
response = self.client.get("/raclette/statistics")
- self.assertIn("<td>alexis</td>\n "
- + "<td>20.00</td>\n "
- + "<td>31.67</td>\n "
- + "<td>-11.67</td>\n",
- response.data.decode('utf-8'))
- self.assertIn("<td>fred</td>\n "
- + "<td>20.00</td>\n "
- + "<td>5.83</td>\n "
- + "<td>14.17</td>\n",
- response.data.decode('utf-8'))
- self.assertIn("<td>tata</td>\n "
- + "<td>0.00</td>\n "
- + "<td>2.50</td>\n "
- + "<td>-2.50</td>\n",
- response.data.decode('utf-8'))
- self.assertIn("<td>toto</td>\n "
- + "<td>0.00</td>\n "
- + "<td>0.00</td>\n "
- + "<td>0.00</td>\n",
- response.data.decode('utf-8'))
+ self.assertIn(
+ "<td>alexis</td>\n "
+ + "<td>20.00</td>\n "
+ + "<td>31.67</td>\n "
+ + "<td>-11.67</td>\n",
+ response.data.decode("utf-8"),
+ )
+ self.assertIn(
+ "<td>fred</td>\n "
+ + "<td>20.00</td>\n "
+ + "<td>5.83</td>\n "
+ + "<td>14.17</td>\n",
+ response.data.decode("utf-8"),
+ )
+ self.assertIn(
+ "<td>tata</td>\n "
+ + "<td>0.00</td>\n "
+ + "<td>2.50</td>\n "
+ + "<td>-2.50</td>\n",
+ response.data.decode("utf-8"),
+ )
+ self.assertIn(
+ "<td>toto</td>\n "
+ + "<td>0.00</td>\n "
+ + "<td>0.00</td>\n "
+ + "<td>0.00</td>\n",
+ response.data.decode("utf-8"),
+ )
def test_settle_page(self):
self.post_project("raclette")
@@ -825,43 +928,52 @@ class BudgetTestCase(IhatemoneyTestCase):
self.post_project("raclette")
# add members
- 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'})
+ 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'})
+ self.client.post("/raclette/members/add", data={"name": "toto"})
# create bills
- self.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'fromage à raclette',
- 'payer': 1,
- 'payed_for': [1, 2, 3],
- 'amount': '10.0',
- })
-
- self.client.post("/raclette/add", data={
- 'date': '2011-08-10',
- 'what': 'red wine',
- 'payer': 2,
- 'payed_for': [1],
- 'amount': '20',
- })
-
- 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')
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "fromage à raclette",
+ "payer": 1,
+ "payed_for": [1, 2, 3],
+ "amount": "10.0",
+ },
+ )
+
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2011-08-10",
+ "what": "red wine",
+ "payer": 2,
+ "payed_for": [1],
+ "amount": "20",
+ },
+ )
+
+ 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")
transactions = project.get_transactions_to_settle_bill()
members = defaultdict(int)
# 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():
assert abs(a - balance[m.id]) < 0.01
@@ -871,116 +983,141 @@ class BudgetTestCase(IhatemoneyTestCase):
self.post_project("raclette")
# add members
- 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'})
+ 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.client.post("/raclette/add", data={
- 'date': '2016-12-31',
- 'what': 'fromage à raclette',
- 'payer': 1,
- 'payed_for': [1, 2, 3],
- 'amount': '10.0',
- })
-
- self.client.post("/raclette/add", data={
- 'date': '2016-12-31',
- 'what': 'red wine',
- 'payer': 2,
- 'payed_for': [1, 3],
- 'amount': '20',
- })
-
- 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')
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2016-12-31",
+ "what": "fromage à raclette",
+ "payer": 1,
+ "payed_for": [1, 2, 3],
+ "amount": "10.0",
+ },
+ )
+
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2016-12-31",
+ "what": "red wine",
+ "payer": 2,
+ "payed_for": [1, 3],
+ "amount": "20",
+ },
+ )
+
+ 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")
transactions = project.get_transactions_to_settle_bill()
# There should not be any zero-amount transfer after rounding
for t in transactions:
- rounded_amount = round(t['amount'], 2)
- self.assertNotEqual(0.0, rounded_amount,
- msg='%f is equal to zero after rounding' % t['amount'])
+ rounded_amount = round(t["amount"], 2)
+ self.assertNotEqual(
+ 0.0,
+ rounded_amount,
+ msg="%f is equal to zero after rounding" % t["amount"],
+ )
def test_export(self):
self.post_project("raclette")
# add members
- 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é'})
+ 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.client.post("/raclette/add", data={
- 'date': '2016-12-31',
- 'what': 'fromage à raclette',
- 'payer': 1,
- 'payed_for': [1, 2, 3, 4],
- 'amount': '10.0',
- })
-
- self.client.post("/raclette/add", data={
- 'date': '2016-12-31',
- 'what': 'red wine',
- 'payer': 2,
- 'payed_for': [1, 3],
- 'amount': '200',
- })
-
- self.client.post("/raclette/add", data={
- 'date': '2017-01-01',
- 'what': 'refund',
- 'payer': 3,
- 'payed_for': [2],
- 'amount': '13.33',
- })
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2016-12-31",
+ "what": "fromage à raclette",
+ "payer": 1,
+ "payed_for": [1, 2, 3, 4],
+ "amount": "10.0",
+ },
+ )
+
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2016-12-31",
+ "what": "red wine",
+ "payer": 2,
+ "payed_for": [1, 3],
+ "amount": "200",
+ },
+ )
+
+ self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2017-01-01",
+ "what": "refund",
+ "payer": 3,
+ "payed_for": [2],
+ "amount": "13.33",
+ },
+ )
# generate json export of bills
resp = self.client.get("/raclette/export/bills.json")
- 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)
+ 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.client.get("/raclette/export/bills.csv")
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")
+ '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):
self.assertEqual(
- set(line.split(",")),
- set(received_lines[i].strip("\r").split(","))
+ set(line.split(",")), set(received_lines[i].strip("\r").split(","))
)
# generate json export of transactions
@@ -991,7 +1128,7 @@ class BudgetTestCase(IhatemoneyTestCase):
{"amount": 127.33, "receiver": "fred", "ower": "alexis"},
]
- self.assertEqual(json.loads(resp.data.decode('utf-8')), expected)
+ self.assertEqual(json.loads(resp.data.decode("utf-8")), expected)
# generate csv export of transactions
resp = self.client.get("/raclette/export/transactions.csv")
@@ -1002,12 +1139,11 @@ class BudgetTestCase(IhatemoneyTestCase):
"55.34,fred,tata",
"127.33,fred,alexis",
]
- received_lines = resp.data.decode('utf-8').split("\n")
+ received_lines = resp.data.decode("utf-8").split("\n")
for i, line in enumerate(expected):
self.assertEqual(
- set(line.split(",")),
- set(received_lines[i].strip("\r").split(","))
+ set(line.split(",")), set(received_lines[i].strip("\r").split(","))
)
# wrong export_format should return a 404
@@ -1024,22 +1160,30 @@ class APITestCase(IhatemoneyTestCase):
password = password or name
contact = contact or "%s@notmyidea.org" % name
- return self.client.post("/api/projects", data={
- 'name': name,
- 'id': id,
- 'password': password,
- 'contact_email': contact
- })
+ return self.client.post(
+ "/api/projects",
+ data={
+ "name": name,
+ "id": id,
+ "password": password,
+ "contact_email": contact,
+ },
+ )
def api_add_member(self, project, name, weight=1):
- self.client.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
- base64string = base64.encodebytes(
- ('%s:%s' % (username, password)).encode('utf-8')).decode('utf-8').replace('\n', '')
+ base64string = (
+ base64.encodebytes(("%s:%s" % (username, password)).encode("utf-8"))
+ .decode("utf-8")
+ .replace("\n", "")
+ )
return {"Authorization": "Basic %s" % base64string}
def test_cors_requests(self):
@@ -1048,9 +1192,10 @@ class APITestCase(IhatemoneyTestCase):
self.assertStatus(201, resp)
# Try to do an OPTIONS requests and see if the headers are correct.
- resp = self.client.options("/api/projects/raclette",
- headers=self.get_auth("raclette"))
- self.assertEqual(resp.headers['Access-Control-Allow-Origin'], '*')
+ resp = self.client.options(
+ "/api/projects/raclette", headers=self.get_auth("raclette")
+ )
+ self.assertEqual(resp.headers["Access-Control-Allow-Origin"], "*")
def test_basic_auth(self):
# create a project
@@ -1063,32 +1208,33 @@ class APITestCase(IhatemoneyTestCase):
# PUT / POST / DELETE / GET on the different resources
# should also return a 401
- for verb in ('post',):
+ for verb in ("post",):
for resource in ("/raclette/members", "/raclette/bills"):
url = "/api/projects" + resource
- self.assertStatus(401, getattr(self.client, 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"):
+ for verb in ("get", "delete", "put"):
+ for resource in ("/raclette", "/raclette/members/1", "/raclette/bills/1"):
url = "/api/projects" + resource
- self.assertStatus(401, getattr(self.client, 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.client.post("/api/projects", data={
- 'name': "raclette",
- 'id': "raclette",
- 'password': "raclette",
- 'contact_email': "not-an-email"
- })
+ resp = self.client.post(
+ "/api/projects",
+ data={
+ "name": "raclette",
+ "id": "raclette",
+ "password": "raclette",
+ "contact_email": "not-an-email",
+ },
+ )
self.assertTrue(400, resp.status_code)
- self.assertEqual('{"contact_email": ["Invalid email address."]}\n',
- resp.data.decode('utf-8'))
+ self.assertEqual(
+ '{"contact_email": ["Invalid email address."]}\n', resp.data.decode("utf-8")
+ )
# create it
resp = self.api_create("raclette")
@@ -1098,11 +1244,12 @@ class APITestCase(IhatemoneyTestCase):
resp = self.api_create("raclette")
self.assertTrue(400, resp.status_code)
- self.assertIn('id', json.loads(resp.data.decode('utf-8')))
+ self.assertIn("id", json.loads(resp.data.decode("utf-8")))
# get information about it
- resp = self.client.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 = {
@@ -1111,20 +1258,25 @@ class APITestCase(IhatemoneyTestCase):
"contact_email": "raclette@notmyidea.org",
"id": "raclette",
}
- decoded_resp = json.loads(resp.data.decode('utf-8'))
+ decoded_resp = json.loads(resp.data.decode("utf-8"))
self.assertDictEqual(decoded_resp, expected)
# edit should work
- resp = self.client.put("/api/projects/raclette", data={
- "contact_email": "yeah@notmyidea.org",
- "password": "raclette",
- "name": "The raclette party",
- }, headers=self.get_auth("raclette"))
+ resp = self.client.put(
+ "/api/projects/raclette",
+ data={
+ "contact_email": "yeah@notmyidea.org",
+ "password": "raclette",
+ "name": "The raclette party",
+ },
+ headers=self.get_auth("raclette"),
+ )
self.assertEqual(200, resp.status_code)
- resp = self.client.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 = {
@@ -1133,31 +1285,36 @@ class APITestCase(IhatemoneyTestCase):
"members": [],
"id": "raclette",
}
- decoded_resp = json.loads(resp.data.decode('utf-8'))
+ decoded_resp = json.loads(resp.data.decode("utf-8"))
self.assertDictEqual(decoded_resp, expected)
# password change is possible via API
- resp = self.client.put("/api/projects/raclette", data={
- "contact_email": "yeah@notmyidea.org",
- "password": "tartiflette",
- "name": "The raclette party",
- }, headers=self.get_auth("raclette"))
+ resp = self.client.put(
+ "/api/projects/raclette",
+ data={
+ "contact_email": "yeah@notmyidea.org",
+ "password": "tartiflette",
+ "name": "The raclette party",
+ },
+ headers=self.get_auth("raclette"),
+ )
self.assertEqual(200, resp.status_code)
- resp = self.client.get("/api/projects/raclette",
- headers=self.get_auth(
- "raclette", "tartiflette"))
+ resp = self.client.get(
+ "/api/projects/raclette", headers=self.get_auth("raclette", "tartiflette")
+ )
self.assertEqual(200, resp.status_code)
# delete should work
- resp = self.client.delete("/api/projects/raclette",
- headers=self.get_auth(
- "raclette", "tartiflette"))
+ resp = self.client.delete(
+ "/api/projects/raclette", headers=self.get_auth("raclette", "tartiflette")
+ )
# get should return a 401 on an unknown resource
- resp = self.client.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):
@@ -1165,94 +1322,110 @@ class APITestCase(IhatemoneyTestCase):
self.api_create("raclette")
# get the list of members (should be empty)
- req = self.client.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('[]\n', req.data.decode('utf-8'))
+ self.assertEqual("[]\n", req.data.decode("utf-8"))
# add a member
- req = self.client.post("/api/projects/raclette/members", data={
- "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\n", req.data.decode('utf-8'))
+ self.assertEqual("1\n", req.data.decode("utf-8"))
# the list of members should contain one member
- req = self.client.get("/api/projects/raclette/members",
- 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)
+ self.assertEqual(len(json.loads(req.data.decode("utf-8"))), 1)
# Try to add another member with the same name.
- req = self.client.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"),
+ )
self.assertStatus(400, req)
# edit the member
- req = self.client.put("/api/projects/raclette/members/1", data={
- "name": "Fred",
- "weight": 2,
- }, headers=self.get_auth("raclette"))
+ req = self.client.put(
+ "/api/projects/raclette/members/1",
+ data={"name": "Fred", "weight": 2},
+ headers=self.get_auth("raclette"),
+ )
self.assertStatus(200, req)
# get should return the new name
- req = self.client.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"])
- self.assertEqual(2, json.loads(req.data.decode('utf-8'))["weight"])
+ self.assertEqual("Fred", json.loads(req.data.decode("utf-8"))["name"])
+ self.assertEqual(2, json.loads(req.data.decode("utf-8"))["weight"])
# edit this member with same information
# (test PUT idemopotence)
- req = self.client.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)
# de-activate the user
- req = self.client.put("/api/projects/raclette/members/1", data={
- "name": "Fred",
- "activated": False,
- }, headers=self.get_auth("raclette"))
+ req = self.client.put(
+ "/api/projects/raclette/members/1",
+ data={"name": "Fred", "activated": False},
+ headers=self.get_auth("raclette"),
+ )
self.assertStatus(200, req)
- req = self.client.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(False, json.loads(req.data.decode('utf-8'))["activated"])
+ self.assertEqual(False, json.loads(req.data.decode("utf-8"))["activated"])
# re-activate the user
- req = self.client.put("/api/projects/raclette/members/1", data={
- "name": "Fred",
- "activated": True,
- }, headers=self.get_auth("raclette"))
+ req = self.client.put(
+ "/api/projects/raclette/members/1",
+ data={"name": "Fred", "activated": True},
+ headers=self.get_auth("raclette"),
+ )
- req = self.client.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(True, json.loads(req.data.decode('utf-8'))["activated"])
+ self.assertEqual(True, json.loads(req.data.decode("utf-8"))["activated"])
# delete a member
- req = self.client.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
- req = self.client.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('[]\n', req.data.decode('utf-8'))
+ self.assertEqual("[]\n", req.data.decode("utf-8"))
def test_bills(self):
# create a project
@@ -1264,29 +1437,35 @@ class APITestCase(IhatemoneyTestCase):
self.api_add_member("raclette", "arnaud")
# get the list of bills (should be empty)
- req = self.client.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("[]\n", req.data.decode('utf-8'))
+ self.assertEqual("[]\n", req.data.decode("utf-8"))
# add a bill
- req = self.client.post("/api/projects/raclette/bills", data={
- 'date': '2011-08-10',
- 'what': 'fromage',
- 'payer': "1",
- 'payed_for': ["1", "2"],
- 'amount': '25',
- 'external_link': "https://raclette.fr"
- }, headers=self.get_auth("raclette"))
+ req = self.client.post(
+ "/api/projects/raclette/bills",
+ data={
+ "date": "2011-08-10",
+ "what": "fromage",
+ "payer": "1",
+ "payed_for": ["1", "2"],
+ "amount": "25",
+ "external_link": "https://raclette.fr",
+ },
+ headers=self.get_auth("raclette"),
+ )
# should return the id
self.assertStatus(201, req)
- self.assertEqual(req.data.decode('utf-8'), "1\n")
+ self.assertEqual(req.data.decode("utf-8"), "1\n")
# get this bill details
- req = self.client.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)
@@ -1295,56 +1474,68 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 1,
"owers": [
{"activated": True, "id": 1, "name": "alexis", "weight": 1},
- {"activated": True, "id": 2, "name": "fred", "weight": 1}],
+ {"activated": True, "id": 2, "name": "fred", "weight": 1},
+ ],
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
- 'external_link': "https://raclette.fr"
+ "external_link": "https://raclette.fr",
}
- got = json.loads(req.data.decode('utf-8'))
+ got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
datetime.date.today(),
- datetime.datetime.strptime(got["creation_date"], '%Y-%m-%d').date()
+ datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
)
del got["creation_date"]
self.assertDictEqual(expected, got)
# the list of bills should length 1
- req = self.client.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'))))
+ self.assertEqual(1, len(json.loads(req.data.decode("utf-8"))))
# edit with errors should return an error
- 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',
- 'external_link': "https://raclette.fr",
- }, headers=self.get_auth("raclette"))
+ 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",
+ "external_link": "https://raclette.fr",
+ },
+ headers=self.get_auth("raclette"),
+ )
self.assertStatus(400, req)
- self.assertEqual('{"date": ["This field is required."]}\n', req.data.decode('utf-8'))
+ self.assertEqual(
+ '{"date": ["This field is required."]}\n', req.data.decode("utf-8")
+ )
# edit a bill
- req = self.client.put("/api/projects/raclette/bills/1", data={
- 'date': '2011-09-10',
- 'what': 'beer',
- 'payer': "2",
- 'payed_for': ["1", "2"],
- 'amount': '25',
- 'external_link': "https://raclette.fr",
- }, headers=self.get_auth("raclette"))
+ req = self.client.put(
+ "/api/projects/raclette/bills/1",
+ data={
+ "date": "2011-09-10",
+ "what": "beer",
+ "payer": "2",
+ "payed_for": ["1", "2"],
+ "amount": "25",
+ "external_link": "https://raclette.fr",
+ },
+ headers=self.get_auth("raclette"),
+ )
# check its fields
- req = self.client.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")
+ )
creation_date = datetime.datetime.strptime(
- json.loads(req.data.decode('utf-8'))["creation_date"],
- '%Y-%m-%d'
+ json.loads(req.data.decode("utf-8"))["creation_date"], "%Y-%m-%d"
).date()
expected = {
@@ -1352,29 +1543,32 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 2,
"owers": [
{"activated": True, "id": 1, "name": "alexis", "weight": 1},
- {"activated": True, "id": 2, "name": "fred", "weight": 1}],
+ {"activated": True, "id": 2, "name": "fred", "weight": 1},
+ ],
"amount": 25.0,
"date": "2011-09-10",
- 'external_link': "https://raclette.fr",
- "id": 1
- }
+ "external_link": "https://raclette.fr",
+ "id": 1,
+ }
- got = json.loads(req.data.decode('utf-8'))
+ got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
creation_date,
- datetime.datetime.strptime(got["creation_date"], '%Y-%m-%d').date()
+ datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
)
del got["creation_date"]
self.assertDictEqual(expected, got)
# delete a bill
- req = self.client.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.client.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_bills_with_calculation(self):
@@ -1399,23 +1593,23 @@ class APITestCase(IhatemoneyTestCase):
req = self.client.post(
"/api/projects/raclette/bills",
data={
- 'date': '2011-08-10',
- 'what': 'fromage',
- 'payer': "1",
- 'payed_for': ["1", "2"],
- 'amount': input_amount,
+ "date": "2011-08-10",
+ "what": "fromage",
+ "payer": "1",
+ "payed_for": ["1", "2"],
+ "amount": input_amount,
},
- 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'), "{}\n".format(id))
+ self.assertEqual(req.data.decode("utf-8"), "{}\n".format(id))
# get this bill's details
req = self.client.get(
"/api/projects/raclette/bills/{}".format(id),
- headers=self.get_auth("raclette")
+ headers=self.get_auth("raclette"),
)
# compare with the added info
@@ -1425,17 +1619,18 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 1,
"owers": [
{"activated": True, "id": 1, "name": "alexis", "weight": 1},
- {"activated": True, "id": 2, "name": "fred", "weight": 1}],
+ {"activated": True, "id": 2, "name": "fred", "weight": 1},
+ ],
"amount": expected_amount,
"date": "2011-08-10",
"id": id,
- "external_link": '',
+ "external_link": "",
}
- got = json.loads(req.data.decode('utf-8'))
+ got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
datetime.date.today(),
- datetime.datetime.strptime(got["creation_date"], '%Y-%m-%d').date()
+ datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
)
del got["creation_date"]
self.assertDictEqual(expected, got)
@@ -1450,13 +1645,17 @@ class APITestCase(IhatemoneyTestCase):
]
for amount in erroneous_amounts:
- req = self.client.post("/api/projects/raclette/bills", data={
- 'date': '2011-08-10',
- 'what': 'fromage',
- 'payer': "1",
- 'payed_for': ["1", "2"],
- 'amount': amount,
- }, headers=self.get_auth("raclette"))
+ req = self.client.post(
+ "/api/projects/raclette/bills",
+ data={
+ "date": "2011-08-10",
+ "what": "fromage",
+ "payer": "1",
+ "payed_for": ["1", "2"],
+ "amount": amount,
+ },
+ headers=self.get_auth("raclette"),
+ )
self.assertStatus(400, req)
def test_statistics(self):
@@ -1468,30 +1667,50 @@ class APITestCase(IhatemoneyTestCase):
self.api_add_member("raclette", "fred")
# add a bill
- req = self.client.post("/api/projects/raclette/bills", data={
- 'date': '2011-08-10',
- 'what': 'fromage',
- 'payer': "1",
- 'payed_for': ["1", "2"],
- 'amount': '25',
- }, headers=self.get_auth("raclette"))
+ req = self.client.post(
+ "/api/projects/raclette/bills",
+ data={
+ "date": "2011-08-10",
+ "what": "fromage",
+ "payer": "1",
+ "payed_for": ["1", "2"],
+ "amount": "25",
+ },
+ headers=self.get_auth("raclette"),
+ )
# get the list of bills (should be empty)
- req = self.client.get("/api/projects/raclette/statistics",
- headers=self.get_auth("raclette"))
+ req = self.client.get(
+ "/api/projects/raclette/statistics", headers=self.get_auth("raclette")
+ )
self.assertStatus(200, req)
- self.assertEqual([
- {'balance': 12.5,
- 'member': {'activated': True, 'id': 1,
- 'name': 'alexis', 'weight': 1.0},
- 'paid': 25.0,
- 'spent': 12.5},
- {'balance': -12.5,
- 'member': {'activated': True, 'id': 2,
- 'name': 'fred', 'weight': 1.0},
- 'paid': 0,
- 'spent': 12.5}],
- json.loads(req.data.decode('utf-8')))
+ self.assertEqual(
+ [
+ {
+ "balance": 12.5,
+ "member": {
+ "activated": True,
+ "id": 1,
+ "name": "alexis",
+ "weight": 1.0,
+ },
+ "paid": 25.0,
+ "spent": 12.5,
+ },
+ {
+ "balance": -12.5,
+ "member": {
+ "activated": True,
+ "id": 2,
+ "name": "fred",
+ "weight": 1.0,
+ },
+ "paid": 0,
+ "spent": 12.5,
+ },
+ ],
+ json.loads(req.data.decode("utf-8")),
+ )
def test_username_xss(self):
# create a project
@@ -1502,8 +1721,8 @@ class APITestCase(IhatemoneyTestCase):
# add members
self.api_add_member("raclette", "<script>")
- result = self.client.get('/raclette/')
- self.assertNotIn("<script>", result.data.decode('utf-8'))
+ result = self.client.get("/raclette/")
+ self.assertNotIn("<script>", result.data.decode("utf-8"))
def test_weighted_bills(self):
# create a project
@@ -1515,20 +1734,24 @@ class APITestCase(IhatemoneyTestCase):
self.api_add_member("raclette", "arnaud")
# add a bill
- req = self.client.post("/api/projects/raclette/bills", data={
- 'date': '2011-08-10',
- 'what': "fromage",
- 'payer': "1",
- 'payed_for': ["1", "2"],
- 'amount': '25',
- }, headers=self.get_auth("raclette"))
+ req = self.client.post(
+ "/api/projects/raclette/bills",
+ data={
+ "date": "2011-08-10",
+ "what": "fromage",
+ "payer": "1",
+ "payed_for": ["1", "2"],
+ "amount": "25",
+ },
+ headers=self.get_auth("raclette"),
+ )
# get this bill details
- req = self.client.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")
+ )
creation_date = datetime.datetime.strptime(
- json.loads(req.data.decode('utf-8'))["creation_date"],
- '%Y-%m-%d'
+ json.loads(req.data.decode("utf-8"))["creation_date"], "%Y-%m-%d"
).date()
# compare with the added info
@@ -1538,30 +1761,49 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 1,
"owers": [
{"activated": True, "id": 1, "name": "alexis", "weight": 1},
- {"activated": True, "id": 2, "name": "freddy familly", "weight": 4}],
+ {"activated": True, "id": 2, "name": "freddy familly", "weight": 4},
+ ],
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
- "external_link": ''
+ "external_link": "",
}
- got = json.loads(req.data.decode('utf-8'))
+ got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
creation_date,
- datetime.datetime.strptime(got["creation_date"], '%Y-%m-%d').date()
+ datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
)
del got["creation_date"]
self.assertDictEqual(expected, got)
# getting it should return a 404
- req = self.client.get("/api/projects/raclette",
- headers=self.get_auth("raclette"))
+ req = self.client.get(
+ "/api/projects/raclette", headers=self.get_auth("raclette")
+ )
expected = {
"members": [
- {"activated": True, "id": 1, "name": "alexis", "weight": 1.0, "balance": 20.0},
- {"activated": True, "id": 2, "name": "freddy familly", "weight": 4.0,
- "balance": -20.0},
- {"activated": True, "id": 3, "name": "arnaud", "weight": 1.0, "balance": 0},
+ {
+ "activated": True,
+ "id": 1,
+ "name": "alexis",
+ "weight": 1.0,
+ "balance": 20.0,
+ },
+ {
+ "activated": True,
+ "id": 2,
+ "name": "freddy familly",
+ "weight": 4.0,
+ "balance": -20.0,
+ },
+ {
+ "activated": True,
+ "id": 3,
+ "name": "arnaud",
+ "weight": 1.0,
+ "balance": 0,
+ },
],
"contact_email": "raclette@notmyidea.org",
"id": "raclette",
@@ -1569,25 +1811,24 @@ class APITestCase(IhatemoneyTestCase):
}
self.assertStatus(200, req)
- decoded_req = json.loads(req.data.decode('utf-8'))
+ decoded_req = json.loads(req.data.decode("utf-8"))
self.assertDictEqual(decoded_req, expected)
class ServerTestCase(IhatemoneyTestCase):
-
def test_homepage(self):
# See https://github.com/spiral-project/ihatemoney/pull/358
- self.app.config['APPLICATION_ROOT'] = '/'
+ self.app.config["APPLICATION_ROOT"] = "/"
req = self.client.get("/")
self.assertStatus(200, req)
def test_unprefixed(self):
- self.app.config['APPLICATION_ROOT'] = '/'
+ self.app.config["APPLICATION_ROOT"] = "/"
req = self.client.get("/foo/")
self.assertStatus(303, req)
def test_prefixed(self):
- self.app.config['APPLICATION_ROOT'] = '/foo'
+ self.app.config["APPLICATION_ROOT"] = "/foo"
req = self.client.get("/foo/")
self.assertStatus(200, req)
@@ -1599,26 +1840,27 @@ class CommandTestCase(BaseTestCase):
- produce something non-empty
"""
cmd = GenerateConfig()
- for config_file in cmd.get_options()[0].kwargs['choices']:
- with patch('sys.stdout', new=io.StringIO()) as stdout:
+ for config_file in cmd.get_options()[0].kwargs["choices"]:
+ with patch("sys.stdout", new=io.StringIO()) as stdout:
cmd.run(config_file)
print(stdout.getvalue())
self.assertNotEqual(len(stdout.getvalue().strip()), 0)
def test_generate_password_hash(self):
cmd = GeneratePasswordHash()
- with patch('sys.stdout', new=io.StringIO()) as stdout, \
- patch('getpass.getpass', new=lambda prompt: 'secret'): # NOQA
+ with patch("sys.stdout", new=io.StringIO()) as stdout, patch(
+ "getpass.getpass", new=lambda prompt: "secret"
+ ): # NOQA
cmd.run()
print(stdout.getvalue())
self.assertEqual(len(stdout.getvalue().strip()), 189)
def test_demo_project_deletion(self):
- self.create_project('demo')
- self.assertEquals(models.Project.query.get('demo').name, 'demo')
+ self.create_project("demo")
+ self.assertEquals(models.Project.query.get("demo").name, "demo")
cmd = DeleteProject()
- cmd.run('demo')
+ cmd.run("demo")
self.assertEqual(len(models.Project.query.all()), 0)
diff --git a/ihatemoney/web.py b/ihatemoney/web.py
index 3e74362..fc12e9d 100644
--- a/ihatemoney/web.py
+++ b/ihatemoney/web.py
@@ -11,8 +11,18 @@ and `add_project_id` for a quick overview)
import os
from flask import (
- abort, Blueprint, current_app, flash, g, redirect, render_template, request,
- session, url_for, send_file, send_from_directory
+ abort,
+ Blueprint,
+ current_app,
+ flash,
+ g,
+ redirect,
+ render_template,
+ request,
+ session,
+ url_for,
+ send_file,
+ send_from_directory,
)
from flask_mail import Message
from flask_babel import get_locale, gettext as _
@@ -24,10 +34,22 @@ from functools import wraps
from ihatemoney.models import db, Project, Person, Bill
from ihatemoney.forms import (
- AdminAuthenticationForm, AuthenticationForm, EditProjectForm,
- InviteForm, MemberForm, PasswordReminder, ResetPasswordForm, ProjectForm, get_billform_for
+ AdminAuthenticationForm,
+ AuthenticationForm,
+ EditProjectForm,
+ InviteForm,
+ MemberForm,
+ PasswordReminder,
+ ResetPasswordForm,
+ ProjectForm,
+ get_billform_for,
+)
+from ihatemoney.utils import (
+ Redirect303,
+ list_of_dicts2json,
+ list_of_dicts2csv,
+ LoginThrottler,
)
-from ihatemoney.utils import Redirect303, list_of_dicts2json, list_of_dicts2csv, LoginThrottler
main = Blueprint("main", __name__)
@@ -46,17 +68,20 @@ def requires_admin(bypass=None):
Admin authentication will be bypassed when ALLOW_PUBLIC_PROJECT_CREATION is
set to True.
"""
+
def check_admin(f):
@wraps(f)
def admin_auth(*args, **kws):
is_admin_auth_bypassed = False
if bypass is not None and current_app.config.get(bypass[0]) == bypass[1]:
is_admin_auth_bypassed = True
- is_admin = session.get('is_admin')
+ is_admin = session.get("is_admin")
if is_admin or is_admin_auth_bypassed:
return f(*args, **kws)
- raise Redirect303(url_for('.admin', goto=request.path))
+ raise Redirect303(url_for(".admin", goto=request.path))
+
return admin_auth
+
return check_admin
@@ -66,10 +91,10 @@ def add_project_id(endpoint, values):
This is to not carry it everywhere in the templates.
"""
- if 'project_id' in values or not hasattr(g, 'project'):
+ if "project_id" in values or not hasattr(g, "project"):
return
- if current_app.url_map.is_endpoint_expecting(endpoint, 'project_id'):
- values['project_id'] = g.project.id
+ if current_app.url_map.is_endpoint_expecting(endpoint, "project_id"):
+ values["project_id"] = g.project.id
@main.url_value_preprocessor
@@ -97,21 +122,19 @@ def pull_project(endpoint, values):
return
if not values:
values = {}
- project_id = values.pop('project_id', None)
+ project_id = values.pop("project_id", None)
if project_id:
project = Project.query.get(project_id)
if not project:
- raise Redirect303(url_for(".create_project",
- project_id=project_id))
+ raise Redirect303(url_for(".create_project", project_id=project_id))
- is_admin = session.get('is_admin')
+ is_admin = session.get("is_admin")
if session.get(project.id) or is_admin:
# 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))
+ raise Redirect303(url_for(".authenticate", project_id=project_id))
@main.route("/admin", methods=["GET", "POST"])
@@ -121,30 +144,41 @@ def admin():
When ADMIN_PASSWORD is empty, admin authentication is deactivated.
"""
form = AdminAuthenticationForm()
- goto = request.args.get('goto', url_for('.home'))
- is_admin_auth_enabled = bool(current_app.config['ADMIN_PASSWORD'])
+ goto = request.args.get("goto", url_for(".home"))
+ is_admin_auth_enabled = bool(current_app.config["ADMIN_PASSWORD"])
if request.method == "POST":
client_ip = request.remote_addr
if not login_throttler.is_login_allowed(client_ip):
msg = _("Too many failed login attempts, please retry later.")
- form.errors['admin_password'] = [msg]
- return render_template("admin.html", form=form, admin_auth=True,
- is_admin_auth_enabled=is_admin_auth_enabled)
+ form.errors["admin_password"] = [msg]
+ return render_template(
+ "admin.html",
+ form=form,
+ admin_auth=True,
+ is_admin_auth_enabled=is_admin_auth_enabled,
+ )
if form.validate():
# Valid password
- if (check_password_hash(current_app.config['ADMIN_PASSWORD'],
- form.admin_password.data)):
- session['is_admin'] = True
+ if check_password_hash(
+ current_app.config["ADMIN_PASSWORD"], form.admin_password.data
+ ):
+ session["is_admin"] = True
session.update()
login_throttler.reset(client_ip)
return redirect(goto)
# Invalid password
login_throttler.increment_attempts_counter(client_ip)
- msg = _("This admin password is not the right one. Only %(num)d attempts left.",
- num=login_throttler.get_remaining_attempts(client_ip))
- form.errors['admin_password'] = [msg]
- return render_template("admin.html", form=form, admin_auth=True,
- is_admin_auth_enabled=is_admin_auth_enabled)
+ msg = _(
+ "This admin password is not the right one. Only %(num)d attempts left.",
+ num=login_throttler.get_remaining_attempts(client_ip),
+ )
+ form.errors["admin_password"] = [msg]
+ return render_template(
+ "admin.html",
+ form=form,
+ admin_auth=True,
+ is_admin_auth_enabled=is_admin_auth_enabled,
+ )
@main.route("/authenticate", methods=["GET", "POST"])
@@ -152,13 +186,13 @@ def authenticate(project_id=None):
"""Authentication form"""
form = AuthenticationForm()
# Try to get project_id from token first
- token = request.args.get('token')
+ token = request.args.get("token")
if token:
- project_id = Project.verify_token(token, token_type='non_timed_token')
+ project_id = Project.verify_token(token, token_type="non_timed_token")
token_auth = True
else:
- if not form.id.data and request.args.get('project_id'):
- form.id.data = request.args['project_id']
+ if not form.id.data and request.args.get("project_id"):
+ form.id.data = request.args["project_id"]
project_id = form.id.data
token_auth = False
if project_id is None:
@@ -172,16 +206,22 @@ def authenticate(project_id=None):
if not project:
# If the user try to connect to an unexisting project, we will
# propose him a link to the creation form.
- return render_template("authenticate.html", form=form, create_project=project_id)
+ return render_template(
+ "authenticate.html", form=form, create_project=project_id
+ )
# if credentials are already in session, redirect
if session.get(project_id):
- setattr(g, 'project', project)
+ setattr(g, "project", project)
return redirect(url_for(".list_bills"))
# else do form authentication or token authentication
is_post_auth = request.method == "POST" and form.validate()
- if is_post_auth and check_password_hash(project.password, form.password.data) or token_auth:
+ if (
+ is_post_auth
+ and check_password_hash(project.password, form.password.data)
+ or token_auth
+ ):
# maintain a list of visited projects
if "projects" not in session:
session["projects"] = []
@@ -189,11 +229,11 @@ def authenticate(project_id=None):
session["projects"].insert(0, (project_id, project.name))
session[project_id] = True
session.update()
- setattr(g, 'project', project)
+ setattr(g, "project", project)
return redirect(url_for(".list_bills"))
if is_post_auth and not check_password_hash(project.password, form.password.data):
msg = _("This private code is not the right one")
- form.errors['password'] = [msg]
+ form.errors["password"] = [msg]
return render_template("authenticate.html", form=form)
@@ -202,21 +242,27 @@ def authenticate(project_id=None):
def home():
project_form = ProjectForm()
auth_form = AuthenticationForm()
- is_demo_project_activated = current_app.config['ACTIVATE_DEMO_PROJECT']
- is_public_project_creation_allowed = current_app.config['ALLOW_PUBLIC_PROJECT_CREATION']
+ is_demo_project_activated = current_app.config["ACTIVATE_DEMO_PROJECT"]
+ is_public_project_creation_allowed = current_app.config[
+ "ALLOW_PUBLIC_PROJECT_CREATION"
+ ]
- return render_template("home.html", project_form=project_form,
- is_demo_project_activated=is_demo_project_activated,
- is_public_project_creation_allowed=is_public_project_creation_allowed,
- auth_form=auth_form, session=session)
+ return render_template(
+ "home.html",
+ project_form=project_form,
+ is_demo_project_activated=is_demo_project_activated,
+ is_public_project_creation_allowed=is_public_project_creation_allowed,
+ auth_form=auth_form,
+ session=session,
+ )
@main.route("/create", methods=["GET", "POST"])
@requires_admin(bypass=("ALLOW_PUBLIC_PROJECT_CREATION", True))
def create_project():
form = ProjectForm()
- if request.method == "GET" and 'project_id' in request.values:
- form.name.data = request.values['project_id']
+ if request.method == "GET" and "project_id" in request.values:
+ form.name.data = request.values["project_id"]
if request.method == "POST":
# At first, we don't want the user to bother with the identifier
@@ -239,26 +285,34 @@ def create_project():
# send reminder email
g.project = project
- message_title = _("You have just created '%(project)s' "
- "to share your expenses", project=g.project.name)
+ message_title = _(
+ "You have just created '%(project)s' " "to share your expenses",
+ project=g.project.name,
+ )
- message_body = render_template("reminder_mail.%s.j2" %
- get_locale().language)
+ message_body = render_template(
+ "reminder_mail.%s.j2" % get_locale().language
+ )
- msg = Message(message_title,
- body=message_body,
- recipients=[project.contact_email])
+ msg = Message(
+ message_title, body=message_body, recipients=[project.contact_email]
+ )
try:
current_app.mail.send(msg)
except SMTPRecipientsRefused:
- msg_compl = 'Problem sending mail. '
+ msg_compl = "Problem sending mail. "
# TODO: destroy the project and cancel instead?
else:
- msg_compl = ''
+ msg_compl = ""
# redirect the user to the next step (invite)
- flash(_("%(msg_compl)sThe project identifier is %(project)s",
- msg_compl=msg_compl, project=project.id))
+ flash(
+ _(
+ "%(msg_compl)sThe project identifier is %(project)s",
+ msg_compl=msg_compl,
+ project=project.id,
+ )
+ )
return redirect(url_for(".list_bills", project_id=project.id))
return render_template("create_project.html", form=form)
@@ -273,10 +327,13 @@ def remind_password():
project = Project.query.get(form.id.data)
# send a link to reset the password
password_reminder = "password_reminder.%s.j2" % get_locale().language
- current_app.mail.send(Message(
- "password recovery",
- body=render_template(password_reminder, project=project),
- recipients=[project.contact_email]))
+ current_app.mail.send(
+ Message(
+ "password recovery",
+ body=render_template(password_reminder, project=project),
+ recipients=[project.contact_email],
+ )
+ )
return redirect(url_for(".password_reminder_sent"))
return render_template("password_reminder.html", form=form)
@@ -287,18 +344,24 @@ def password_reminder_sent():
return render_template("password_reminder_sent.html")
-@main.route('/reset-password', methods=['GET', 'POST'])
+@main.route("/reset-password", methods=["GET", "POST"])
def reset_password():
form = ResetPasswordForm()
- token = request.args.get('token')
+ token = request.args.get("token")
if not token:
- return render_template('reset_password.html', form=form, error=_("No token provided"))
+ return render_template(
+ "reset_password.html", form=form, error=_("No token provided")
+ )
project_id = Project.verify_token(token)
if not project_id:
- return render_template('reset_password.html', form=form, error=_("Invalid token"))
+ return render_template(
+ "reset_password.html", form=form, error=_("Invalid token")
+ )
project = Project.query.get(project_id)
if not project:
- return render_template('reset_password.html', form=form, error=_("Unknown project"))
+ return render_template(
+ "reset_password.html", form=form, error=_("Unknown project")
+ )
if request.method == "POST" and form.validate():
project.password = generate_password_hash(form.password.data)
@@ -306,7 +369,7 @@ def reset_password():
db.session.commit()
flash(_("Password successfully reset."))
return redirect(url_for(".home"))
- return render_template('reset_password.html', form=form)
+ return render_template("reset_password.html", form=form)
@main.route("/<project_id>/edit", methods=["GET", "POST"])
@@ -324,40 +387,38 @@ def edit_project():
edit_form.contact_email.data = g.project.contact_email
return render_template(
- "edit_project.html",
- edit_form=edit_form,
- current_view="edit_project"
+ "edit_project.html", edit_form=edit_form, current_view="edit_project"
)
@main.route("/<project_id>/delete")
def delete_project():
g.project.remove_project()
- flash(_('Project successfully deleted'))
+ flash(_("Project successfully deleted"))
- return redirect(request.headers.get('Referer') or url_for('.home'))
+ return redirect(request.headers.get("Referer") or url_for(".home"))
@main.route("/<project_id>/export/<string:file>.<string:format>")
def export_project(file, format):
- if file == 'transactions':
+ if file == "transactions":
export = g.project.get_transactions_to_settle_bill(pretty_output=True)
elif file == "bills":
export = g.project.get_pretty_bills(export_format=format)
else:
- abort(404, 'No such export type')
+ abort(404, "No such export type")
if format == "json":
file2export = list_of_dicts2json(export)
elif format == "csv":
file2export = list_of_dicts2csv(export)
else:
- abort(404, 'No such export format')
+ abort(404, "No such export format")
return send_file(
file2export,
attachment_filename="%s-%s.%s" % (g.project.id, file, format),
- as_attachment=True
+ as_attachment=True,
)
@@ -377,16 +438,18 @@ def demo():
Create a demo project if it doesn't exists yet (or has been deleted)
If the demo project is deactivated, one is redirected to the create project form
"""
- is_demo_project_activated = current_app.config['ACTIVATE_DEMO_PROJECT']
+ is_demo_project_activated = current_app.config["ACTIVATE_DEMO_PROJECT"]
project = Project.query.get("demo")
if not project and not is_demo_project_activated:
- raise Redirect303(url_for(".create_project",
- project_id='demo'))
+ raise Redirect303(url_for(".create_project", project_id="demo"))
if not project and is_demo_project_activated:
- project = Project(id="demo", name=u"demonstration",
- password=generate_password_hash("demo"),
- contact_email="demo@notmyidea.org")
+ project = Project(
+ id="demo",
+ name="demonstration",
+ password=generate_password_hash("demo"),
+ contact_email="demo@notmyidea.org",
+ )
db.session.add(project)
db.session.commit()
session[project.id] = True
@@ -403,15 +466,19 @@ def invite():
if form.validate():
# send the email
- message_body = render_template("invitation_mail.%s.j2" %
- get_locale().language)
-
- message_title = _("You have been invited to share your "
- "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(",")])
+ message_body = render_template(
+ "invitation_mail.%s.j2" % get_locale().language
+ )
+
+ message_title = _(
+ "You have been invited to share your " "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(",")],
+ )
current_app.mail.send(msg)
flash(_("Your invitations have been sent"))
return redirect(url_for(".list_bills"))
@@ -423,17 +490,19 @@ def invite():
def list_bills():
bill_form = get_billform_for(g.project)
# set the last selected payer as default choice if exists
- if 'last_selected_payer' in session:
- bill_form.payer.data = session['last_selected_payer']
+ if "last_selected_payer" in session:
+ bill_form.payer.data = session["last_selected_payer"]
# Preload the "owers" relationship for all 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",
- )
+ 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",
+ )
@main.route("/<project_id>/members/add", methods=["GET", "POST"])
@@ -452,8 +521,11 @@ 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()
+ person = (
+ Person.query.filter(Person.id == member_id)
+ .filter(Project.id == g.project.id)
+ .all()
+ )
if person:
person[0].activated = True
db.session.commit()
@@ -466,23 +538,27 @@ def remove_member(member_id):
member = g.project.remove_member(member_id)
if member:
if not member.activated:
- flash(_("User '%(name)s' has been deactivated. It will still "
+ flash(
+ _(
+ "User '%(name)s' has been deactivated. It will still "
"appear in the users list until its balance "
- "becomes zero.", name=member.name))
+ "becomes zero.",
+ name=member.name,
+ )
+ )
else:
flash(_("User '%(name)s' has been removed", name=member.name))
return redirect(url_for(".list_bills"))
-@main.route("/<project_id>/members/<member_id>/edit",
- methods=["POST", "GET"])
+@main.route("/<project_id>/members/<member_id>/edit", methods=["POST", "GET"])
def edit_member(member_id):
member = Person.query.get(member_id, g.project)
if not member:
raise NotFound()
form = MemberForm(g.project, edit=True)
- if request.method == 'POST' and form.validate():
+ if request.method == "POST" and form.validate():
form.save(g.project, member)
db.session.commit()
flash(_("User '%(name)s' has been edited", name=member.name))
@@ -495,10 +571,10 @@ def edit_member(member_id):
@main.route("/<project_id>/add", methods=["GET", "POST"])
def add_bill():
form = get_billform_for(g.project)
- if request.method == 'POST':
+ if request.method == "POST":
if form.validate():
# save last selected payer in session
- session['last_selected_payer'] = form.payer.data
+ session["last_selected_payer"] = form.payer.data
session.update()
bill = Bill()
@@ -509,9 +585,9 @@ def add_bill():
args = {}
if form.submit2.data:
- args['add_bill'] = True
+ args["add_bill"] = True
- return redirect(url_for('.list_bills', **args))
+ return redirect(url_for(".list_bills", **args))
return render_template("add_bill.html", form=form)
@@ -521,13 +597,13 @@ def delete_bill(bill_id):
# fixme: everyone is able to delete a bill
bill = Bill.query.get(g.project, bill_id)
if not bill:
- return redirect(url_for('.list_bills'))
+ return redirect(url_for(".list_bills"))
db.session.delete(bill)
db.session.commit()
flash(_("The bill has been deleted"))
- return redirect(url_for('.list_bills'))
+ return redirect(url_for(".list_bills"))
@main.route("/<project_id>/edit/<int:bill_id>", methods=["GET", "POST"])
@@ -539,12 +615,12 @@ def edit_bill(bill_id):
form = get_billform_for(g.project, set_default=False)
- if request.method == 'POST' and form.validate():
+ if request.method == "POST" and form.validate():
form.save(bill, g.project)
db.session.commit()
flash(_("The bill has been modified"))
- return redirect(url_for('.list_bills'))
+ return redirect(url_for(".list_bills"))
if not form.errors:
form.fill(bill)
@@ -554,21 +630,17 @@ def edit_bill(bill_id):
@main.route("/lang/<lang>")
def change_lang(lang):
- session['lang'] = lang
+ session["lang"] = lang
session.update()
- return redirect(request.headers.get('Referer') or url_for('.home'))
+ return redirect(request.headers.get("Referer") or url_for(".home"))
@main.route("/<project_id>/settle_bills")
def settle_bill():
"""Compute the sum each one have to pay to each other and display it"""
bills = g.project.get_transactions_to_settle_bill()
- return render_template(
- "settle_bills.html",
- bills=bills,
- current_view='settle_bill',
- )
+ return render_template("settle_bills.html", bills=bills, current_view="settle_bill")
@main.route("/<project_id>/statistics")
@@ -577,22 +649,25 @@ def statistics():
return render_template(
"statistics.html",
members_stats=g.project.members_stats,
- current_view='statistics',
+ current_view="statistics",
)
@main.route("/dashboard")
@requires_admin()
def dashboard():
- is_admin_dashboard_activated = current_app.config['ACTIVATE_ADMIN_DASHBOARD']
+ is_admin_dashboard_activated = current_app.config["ACTIVATE_ADMIN_DASHBOARD"]
return render_template(
"dashboard.html",
projects=Project.query.all(),
- is_admin_dashboard_activated=is_admin_dashboard_activated
+ is_admin_dashboard_activated=is_admin_dashboard_activated,
)
-@main.route('/favicon.ico')
+@main.route("/favicon.ico")
def favicon():
- return send_from_directory(os.path.join(main.root_path, 'static'),
- 'favicon.ico', mimetype='image/vnd.microsoft.icon')
+ return send_from_directory(
+ os.path.join(main.root_path, "static"),
+ "favicon.ico",
+ mimetype="image/vnd.microsoft.icon",
+ )