From f260a2c9e7b2f34d49ef4c2e50ce83a2361cf343 Mon Sep 17 00:00:00 2001 From: Alexis M Date: Fri, 11 Oct 2019 20:20:13 +0200 Subject: [PATCH] Use black to refomat the files. --- docs/conf.py | 5 +- ihatemoney/api.py | 42 +- ihatemoney/default_settings.py | 4 +- ihatemoney/forms.py | 72 +- ihatemoney/manage.py | 55 +- ihatemoney/migrations/env.py | 33 +- .../migrations/versions/26d6a218c329_.py | 8 +- .../migrations/versions/6c6fb2b7f229_.py | 8 +- .../a67119aa3ee5_migrate_negative_weights.py | 26 +- ...afbf27e6ef20_add_bill_import_date_field.py | 8 +- .../b78f8a8bdb16_hash_project_passwords.py | 25 +- .../migrations/versions/b9a10d5d63ce_.py | 85 +- ...ab0_initialize_all_members_weights_to_1.py | 25 +- ihatemoney/models.py | 184 +- ihatemoney/run.py | 73 +- ihatemoney/tests/tests.py | 1574 ++++++++++------- ihatemoney/web.py | 327 ++-- 17 files changed, 1479 insertions(+), 1075 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 4f26391a..82c0e03d 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 00ebe21a..dc2f598c 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/') +restful_api.add_resource(ProjectsHandler, "/projects") +restful_api.add_resource(ProjectHandler, "/projects/") restful_api.add_resource(MembersHandler, "/projects//members") -restful_api.add_resource(ProjectStatsHandler, "/projects//statistics") -restful_api.add_resource(MemberHandler, "/projects//members/") +restful_api.add_resource( + ProjectStatsHandler, "/projects//statistics" +) +restful_api.add_resource( + MemberHandler, "/projects//members/" +) restful_api.add_resource(BillsHandler, "/projects//bills") -restful_api.add_resource(BillHandler, "/projects//bills/") +restful_api.add_resource( + BillHandler, "/projects//bills/" +) diff --git a/ihatemoney/default_settings.py b/ihatemoney/default_settings.py index f1aaa342..7ce44220 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 1e0ba00e..c9b0547a 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 8e73bc9b..6343ee7c 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 e2f9a287..4d4729c5 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 859b9af0..6d5e2373 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 0336f6c4..da31578c 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 ec234706..8061896b 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 41791558..0ccfac2e 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 e32983db..e730b8d4 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 92bb4462..3c927804 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 5542146c..481c2d95 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 48e16a79..250f009b 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]) + owers = ", ".join([ower.name for ower in bill.owers]) - pretty_bills.append({ - "what": bill.what, - "amount": round(bill.amount, 2), - "date": str(bill.date), - "payer_name": Person.query.get(bill.payer_id).name, - "payer_weight": Person.query.get(bill.payer_id).weight, - "owers": owers - }) + pretty_bills.append( + { + "what": bill.what, + "amount": round(bill.amount, 2), + "date": str(bill.date), + "payer_name": Person.query.get(bill.payer_id).name, + "payer_weight": Person.query.get(bill.payer_id).weight, + "owers": owers, + } + ) return pretty_bills def remove_member(self, member_id): @@ -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 "" % 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 "" % ( 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 9ef31984..6d1e0329 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 3ff8a727..17674757 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 ", resp.data.decode('utf-8')) + self.assertIn("Password confirmation", 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("Account manager - raclette", resp.data.decode('utf-8')) + self.client.post( + url, data={"password": "pass", "password_confirmation": "pass"} + ) + resp = self.login("raclette", password="pass") + self.assertIn( + "Account manager - raclette", 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 ', resp.data.decode('utf-8')) + self.assertIn('', 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('', resp.data.decode('utf-8')) + self.assertIn('', resp.data.decode("utf-8")) # test right password - resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'pass'}) - self.assertIn('/create', resp.data.decode('utf-8')) + resp = self.client.post( + "/admin?goto=%2Fcreate", data={"admin_password": "pass"} + ) + self.assertIn('/create', resp.data.decode("utf-8")) # test wrong password - resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'}) - self.assertNotIn('/create', resp.data.decode('utf-8')) + resp = self.client.post( + "/admin?goto=%2Fcreate", data={"admin_password": "wrong"} + ) + self.assertNotIn('/create', resp.data.decode("utf-8")) # test empty password - resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': ''}) - self.assertNotIn('/create', resp.data.decode('utf-8')) + resp = self.client.post("/admin?goto=%2Fcreate", data={"admin_password": ""}) + self.assertNotIn('/create', 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[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[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[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": "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": "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('

', 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('

', 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": "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": "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": "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('

', resp.data.decode('utf-8')) + self.assertIn('
', 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( + "ProjectNumber of members", + resp.data.decode("utf-8"), ) - self.assertIn('ProjectNumber 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": "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": "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": "delicatessen", + "payer": 1, + "payed_for": [1, 2], + "amount": "10", + }, + ) response = self.client.get("/raclette/statistics") - self.assertIn("alexis\n " - + "20.00\n " - + "31.67\n " - + "-11.67\n", - response.data.decode('utf-8')) - self.assertIn("fred\n " - + "20.00\n " - + "5.83\n " - + "14.17\n", - response.data.decode('utf-8')) - self.assertIn("tata\n " - + "0.00\n " - + "2.50\n " - + "-2.50\n", - response.data.decode('utf-8')) - self.assertIn("toto\n " - + "0.00\n " - + "0.00\n " - + "0.00\n", - response.data.decode('utf-8')) + self.assertIn( + "alexis\n " + + "20.00\n " + + "31.67\n " + + "-11.67\n", + response.data.decode("utf-8"), + ) + self.assertIn( + "fred\n " + + "20.00\n " + + "5.83\n " + + "14.17\n", + response.data.decode("utf-8"), + ) + self.assertIn( + "tata\n " + + "0.00\n " + + "2.50\n " + + "-2.50\n", + response.data.decode("utf-8"), + ) + self.assertIn( + "toto\n " + + "0.00\n " + + "0.00\n " + + "0.00\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": "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": "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": "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": "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": "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": "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": "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": "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": "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", "