From 51b5bb46f7e3112540dfe9599dd50b7bb055158e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20M=C3=A9taireau?= Date: Wed, 28 Jun 2017 21:31:31 +0200 Subject: [PATCH 1/2] Show some respect to pep8 and fellow readers. --- budget/default_settings.py | 17 ++++++++ budget/forms.py | 51 +++++++++++++----------- budget/manage.py | 3 +- budget/models.py | 80 ++++++++++++++++++++++++-------------- budget/run.py | 10 +++-- budget/utils.py | 15 +++++-- budget/web.py | 63 ++++++++++++++++-------------- docs/installation.rst | 5 +-- 8 files changed, 150 insertions(+), 94 deletions(-) diff --git a/budget/default_settings.py b/budget/default_settings.py index 15fe9cdd..80cede28 100644 --- a/budget/default_settings.py +++ b/budget/default_settings.py @@ -1,14 +1,31 @@ +# You can find more information about what these settings mean in the +# documentation, available online at +# http://ihatemoney.readthedocs.io/en/latest/installation.html#configuration + +# Turn this on if you want to have more output on what's happening under the +# hood. DEBUG = False + +# The database URI, reprensenting the type of database and how to connect to it. +# Enter an absolute path here. SQLALCHEMY_DATABASE_URI = 'sqlite:///budget.db' SQLACHEMY_ECHO = DEBUG + # Will likely become the default value in flask-sqlalchemy >=3 ; could be removed # then: SQLALCHEMY_TRACK_MODIFICATIONS = False +# You need to change this secret key, otherwise bad things might happen to your +# users. SECRET_KEY = "tralala" +# A python tuple describing the name and email adress of the sender of the mails. MAIL_DEFAULT_SENDER = ("Budget manager", "budget@notmyidea.org") +# If set to True, a demonstration project will be activated. ACTIVATE_DEMO_PROJECT = True +# If not empty, the specified password must be entered to create new projects. +# DO NOT enter the password in cleartext. Generate a password hash with +# "ihatemoney generate_password_hash" instead. ADMIN_PASSWORD = "" diff --git a/budget/forms.py b/budget/forms.py index 06df7430..f586c9ac 100644 --- a/budget/forms.py +++ b/budget/forms.py @@ -12,6 +12,7 @@ from datetime import datetime from jinja2 import Markup from utils import slugify + def get_billform_for(project, set_default=True, **kwargs): """Return an instance of BillForm configured for a particular project. @@ -20,8 +21,9 @@ def get_billform_for(project, set_default=True, **kwargs): """ form = BillForm(**kwargs) - form.payed_for.choices = form.payer.choices = [(m.id, m.name) - for m in project.active_members] + active_members = [(m.id, m.name) for m in project.active_members] + + form.payed_for.choices = form.payer.choices = active_members form.payed_for.default = [m.id for m in project.active_members] if set_default and request.method == "GET": @@ -30,7 +32,9 @@ def get_billform_for(project, set_default=True, **kwargs): class CommaDecimalField(DecimalField): + """A class to deal with comma in Decimal Field""" + def process_formdata(self, value): if value: value[0] = str(value[0]).replace(',', '.') @@ -48,8 +52,8 @@ class EditProjectForm(FlaskForm): Returns the created instance """ project = Project(name=self.name.data, id=self.id.data, - password=self.password.data, - contact_email=self.contact_email.data) + password=self.password.data, + contact_email=self.contact_email.data) return project def update(self, project): @@ -69,12 +73,13 @@ class ProjectForm(EditProjectForm): def validate_id(form, field): form.id.data = slugify(field.data) if (form.id.data == "dashboard") or Project.query.get(form.id.data): - raise ValidationError(Markup(_("The project identifier is used " - "to log in and for the URL of the project. " - "We tried to generate an identifier for you but a project " - "with this identifier already exists. " - "Please create a new identifier " - "that you will be able to remember."))) + message = _("The project identifier is used to log in and for the " + "URL of the project. " + "We tried to generate an identifier for you but a " + "project with this identifier already exists. " + "Please create a new identifier that you will be able " + "to remember") + raise ValidationError(Markup(message)) class AuthenticationForm(FlaskForm): @@ -103,7 +108,7 @@ class BillForm(FlaskForm): payer = SelectField(_("Payer"), validators=[Required()], coerce=int) amount = CommaDecimalField(_("Amount paid"), validators=[Required()]) payed_for = SelectMultipleField(_("For whom?"), - validators=[Required()], coerce=int) + validators=[Required()], coerce=int) submit = SubmitField(_("Submit")) submit2 = SubmitField(_("Submit and add a new one")) @@ -113,7 +118,7 @@ class BillForm(FlaskForm): bill.what = self.what.data bill.date = self.date.data bill.owers = [Person.query.get(ower, project) - for ower in self.payed_for.data] + for ower in self.payed_for.data] return bill @@ -174,17 +179,17 @@ class InviteForm(FlaskForm): for email in [email.strip() for email in form.emails.data.split(",")]: if not validator.regex.match(email): raise ValidationError(_("The email %(email)s is not valid", - email=email)) + email=email)) class ExportForm(FlaskForm): - export_type = SelectField(_("What do you want to download ?"), - validators=[Required()], - coerce=str, - choices=[("bills", _("bills")), ("transactions", _("transactions"))] - ) - export_format = SelectField(_("Export file format"), - validators=[Required()], - coerce=str, - choices=[("csv", "csv"), ("json", "json")] - ) + export_type = SelectField( + _("What do you want to download ?"), + validators=[Required()], + coerce=str, + choices=[("bills", _("bills")), ("transactions", _("transactions"))]) + export_format = SelectField( + _("Export file format"), + validators=[Required()], + coerce=str, + choices=[("csv", "csv"), ("json", "json")]) diff --git a/budget/manage.py b/budget/manage.py index f717fed5..e2bc7c5a 100755 --- a/budget/manage.py +++ b/budget/manage.py @@ -10,7 +10,8 @@ from getpass import getpass class GeneratePasswordHash(Command): - "Get password from user and hash it without printing it in clear text" + + """Get password from user and hash it without printing it in clear text.""" def run(self): password = getpass(prompt='Password: ') diff --git a/budget/models.py b/budget/models.py index 1da44893..1def44fa 100644 --- a/budget/models.py +++ b/budget/models.py @@ -9,13 +9,10 @@ from sqlalchemy import orm db = SQLAlchemy() -# define models - - class Project(db.Model): _to_serialize = ("id", "name", "password", "contact_email", - "members", "active_members", "balance") + "members", "active_members", "balance") id = db.Column(db.String(64), primary_key=True) @@ -32,12 +29,13 @@ class Project(db.Model): def balance(self): balances, should_pay, should_receive = (defaultdict(int) - for time in (1, 2, 3)) + for time in (1, 2, 3)) # for each person for person in self.members: # get the list of bills he has to pay - bills = Bill.query.options(orm.subqueryload(Bill.owers)).filter(Bill.owers.contains(person)) + bills = Bill.query.options(orm.subqueryload(Bill.owers)).filter( + Bill.owers.contains(person)) for bill in bills.all(): if person != bill.payer: share = bill.pay_each() * person.weight @@ -56,6 +54,7 @@ class Project(db.Model): def get_transactions_to_settle_bill(self, pretty_output=False): """Return a list of transactions that could be made to settle the bill""" + def prettify(transactions, pretty_output): """ Return pretty transactions """ @@ -63,36 +62,52 @@ class Project(db.Model): return transactions pretty_transactions = [] for transaction in transactions: - pretty_transactions.append({'ower': transaction['ower'].name, - 'receiver': transaction['receiver'].name, - 'amount': round(transaction['amount'], 2)}) + pretty_transactions.append({ + 'ower': transaction['ower'].name, + 'receiver': transaction['receiver'].name, + 'amount': round(transaction['amount'], 2) + }) return pretty_transactions - #cache value for better performance + # cache value for better performance balance = self.balance - credits, debts, transactions = [],[],[] + credits, debts, transactions = [], [], [] # Create lists of credits and debts for person in self.members: if round(balance[person.id], 2) > 0: credits.append({"person": person, "balance": balance[person.id]}) elif round(balance[person.id], 2) < 0: debts.append({"person": person, "balance": -balance[person.id]}) + # Try and find exact matches for credit in credits: match = self.exactmatch(round(credit["balance"], 2), debts) if match: for m in match: - transactions.append({"ower": m["person"], "receiver": credit["person"], "amount": m["balance"]}) + transactions.append({ + "ower": m["person"], + "receiver": credit["person"], + "amount": m["balance"] + }) debts.remove(m) credits.remove(credit) # Split any remaining debts & credits while credits and debts: + if credits[0]["balance"] > debts[0]["balance"]: - transactions.append({"ower": debts[0]["person"], "receiver": credits[0]["person"], "amount": debts[0]["balance"]}) + transactions.append({ + "ower": debts[0]["person"], + "receiver": credits[0]["person"], + "amount": debts[0]["balance"] + }) credits[0]["balance"] = credits[0]["balance"] - debts[0]["balance"] del debts[0] else: - transactions.append({"ower": debts[0]["person"], "receiver": credits[0]["person"], "amount": credits[0]["balance"]}) + transactions.append({ + "ower": debts[0]["person"], + "receiver": credits[0]["person"], + "amount": credits[0]["balance"] + }) debts[0]["balance"] = debts[0]["balance"] - credits[0]["balance"] del credits[0] @@ -107,7 +122,7 @@ class Project(db.Model): elif debts[0]["balance"] == credit: return [debts[0]] else: - match = self.exactmatch(credit-debts[0]["balance"], debts[1:]) + match = self.exactmatch(credit - debts[0]["balance"], debts[1:]) if match: match.append(debts[0]) else: @@ -136,12 +151,15 @@ class Project(db.Model): owers = [ower.name for ower in bill.owers] else: owers = ', '.join([ower.name for ower in bill.owers]) - pretty_bills.append({"what": bill.what, - "amount": round(bill.amount, 2), - "date": str(bill.date), - "payer_name": Person.query.get(bill.payer_id).name, - "payer_weight": Person.query.get(bill.payer_id).weight, - "owers": owers}) + + pretty_bills.append({ + "what": bill.what, + "amount": round(bill.amount, 2), + "date": str(bill.date), + "payer_name": Person.query.get(bill.payer_id).name, + "payer_weight": Person.query.get(bill.payer_id).weight, + "owers": owers + }) return pretty_bills def remove_member(self, member_id): @@ -176,6 +194,7 @@ class Project(db.Model): class Person(db.Model): class PersonQuery(BaseQuery): + def get_by_name(self, name, project): return Person.query.filter(Person.name == name)\ .filter(Project.id == project.id).one() @@ -212,7 +231,8 @@ class Person(db.Model): return "" % (self.name, self.project.name) # We need to manually define a join table for m2m relations -billowers = db.Table('billowers', +billowers = db.Table( + 'billowers', db.Column('bill_id', db.Integer, db.ForeignKey('bill.id')), db.Column('person_id', db.Integer, db.ForeignKey('person.id')), ) @@ -224,11 +244,11 @@ class Bill(db.Model): def get(self, project, id): try: - return self.join(Person, Project)\ - .filter(Bill.payer_id == Person.id)\ - .filter(Person.project_id == Project.id)\ - .filter(Project.id == project.id)\ - .filter(Bill.id == id).one() + return (self.join(Person, Project) + .filter(Bill.payer_id == Person.id) + .filter(Person.project_id == Project.id) + .filter(Project.id == project.id) + .filter(Bill.id == id).one()) except orm.exc.NoResultFound: return None @@ -262,8 +282,10 @@ class Bill(db.Model): return 0 def __repr__(self): - return "" % (self.amount, - self.payer, ", ".join([o.name for o in self.owers])) + return "" % ( + self.amount, + self.payer, ", ".join([o.name for o in self.owers]) + ) class Archive(db.Model): diff --git a/budget/run.py b/budget/run.py index 7fe4e245..51ecfb10 100644 --- a/budget/run.py +++ b/budget/run.py @@ -64,10 +64,10 @@ def configure(): # Since 2.0 warnings.warn( "The way Ihatemoney stores your ADMIN_PASSWORD has changed. You are using an unhashed" - +" ADMIN_PASSWORD, which is not supported anymore and won't let you access your admin" - +" endpoints. Please use the command './budget/manage.py generate_password_hash'" - +" to generate a proper password HASH and copy the output to the value of" - +" ADMIN_PASSWORD in your settings file.", + + " ADMIN_PASSWORD, which is not supported anymore and won't let you access your admin" + + " endpoints. Please use the command './budget/manage.py generate_password_hash'" + + " to generate a proper password HASH and copy the output to the value of" + + " ADMIN_PASSWORD in your settings file.", UserWarning ) @@ -106,6 +106,7 @@ babel = Babel(app) # sentry sentry = Sentry(app) + @babel.localeselector def get_locale(): # get the lang from the session if defined, fallback on the browser "accept @@ -114,6 +115,7 @@ def get_locale(): setattr(g, 'lang', lang) return lang + def main(): app.run(host="0.0.0.0", debug=True) diff --git a/budget/utils.py b/budget/utils.py index 0e6251b2..ee77f01d 100644 --- a/budget/utils.py +++ b/budget/utils.py @@ -26,7 +26,9 @@ def slugify(value): value = six.text_type(re.sub('[^\w\s-]', '', value).strip().lower()) return re.sub('[-\s]+', '-', value) + class Redirect303(HTTPException, RoutingException): + """Raise if the map requests a redirect. This is for example the case if `strict_slashes` are activated and an url that requires a trailing slash. @@ -43,6 +45,7 @@ class Redirect303(HTTPException, RoutingException): class PrefixedWSGI(object): + ''' Wrap the application in this middleware and configure the front-end server to add these headers, to let you quietly bind @@ -55,6 +58,7 @@ class PrefixedWSGI(object): :param app: the WSGI application ''' + def __init__(self, app): self.app = app self.wsgi_app = app.wsgi_app @@ -85,12 +89,14 @@ def minimal_round(*args, **kw): ires = int(res) return (res if res != ires else ires) + def list_of_dicts2json(dict_to_convert): """Take a list of dictionnaries and turns it into a json in-memory file """ return BytesIO(dumps(dict_to_convert).encode('utf-8')) + def list_of_dicts2csv(dict_to_convert): """Take a list of dictionnaries and turns it into a csv in-memory file, assume all dict have the same keys @@ -110,9 +116,10 @@ def list_of_dicts2csv(dict_to_convert): csv_data = [] csv_data.append([key.encode('utf-8') for key in dict_to_convert[0].keys()]) for dic in dict_to_convert: - csv_data.append([dic[h].encode('utf8') - if isinstance(dic[h], unicode) else str(dic[h]).encode('utf8') - for h in dict_to_convert[0].keys()]) + csv_data.append( + [dic[h].encode('utf8') + if isinstance(dic[h], unicode) else str(dic[h]).encode('utf8') + for h in dict_to_convert[0].keys()]) except (KeyError, IndexError): csv_data = [] writer = csv.writer(csv_file) @@ -123,4 +130,4 @@ def list_of_dicts2csv(dict_to_convert): return csv_file # base64 encoding that works with both py2 and py3 and yield no warning -base64_encode = base64.encodestring if six.PY2 else base64.encodebytes +base64_encode = base64.encodestring if six.PY2 else base64.encodebytes diff --git a/budget/web.py b/budget/web.py index f4961cbf..3e1d475c 100644 --- a/budget/web.py +++ b/budget/web.py @@ -9,12 +9,13 @@ some shortcuts to make your life better when coding (see `pull_project` and `add_project_id` for a quick overview) """ -from flask import Blueprint, current_app, flash, g, redirect, \ - render_template, request, session, url_for, send_file +from flask import ( + Blueprint, current_app, flash, g, redirect, render_template, request, + session, url_for, send_file +) from flask_mail import Mail, Message from flask_babel import get_locale, gettext as _ -from werkzeug.security import generate_password_hash, \ - check_password_hash +from werkzeug.security import generate_password_hash, check_password_hash from smtplib import SMTPRecipientsRefused import werkzeug from sqlalchemy import orm @@ -22,9 +23,11 @@ from functools import wraps # local modules from models import db, Project, Person, Bill -from forms import AdminAuthenticationForm, AuthenticationForm, EditProjectForm, \ - InviteForm, MemberForm, PasswordReminder, ProjectForm, get_billform_for, \ +from forms import ( + AdminAuthenticationForm, AuthenticationForm, EditProjectForm, + InviteForm, MemberForm, PasswordReminder, ProjectForm, get_billform_for, ExportForm +) from utils import Redirect303, list_of_dicts2json, list_of_dicts2csv main = Blueprint("main", __name__) @@ -72,14 +75,14 @@ def pull_project(endpoint, values): project = Project.query.get(project_id) if not project: raise Redirect303(url_for(".create_project", - project_id=project_id)) + project_id=project_id)) if project.id in session and session[project.id] == project.password: # add project into kwargs and call the original function g.project = project else: # redirect to authentication page raise Redirect303( - url_for(".authenticate", project_id=project_id)) + url_for(".authenticate", project_id=project_id)) @main.route("/admin", methods=["GET", "POST"]) @@ -107,7 +110,7 @@ def authenticate(project_id=None): form.id.data = request.args['project_id'] project_id = form.id.data if project_id is None: - #User doesn't provide project identifier, return to authenticate form + # User doesn't provide project identifier, return to authenticate form msg = _("You need to enter a project identifier") form.errors["id"] = [msg] return render_template("authenticate.html", form=form) @@ -147,7 +150,7 @@ def authenticate(project_id=None): return redirect(url_for(".list_bills")) return render_template("authenticate.html", form=form, - create_project=create_project) + create_project=create_project) @main.route("/") @@ -193,14 +196,14 @@ def create_project(): g.project = project message_title = _("You have just created '%(project)s' " - "to share your expenses", project=g.project.name) + "to share your expenses", project=g.project.name) message_body = render_template("reminder_mail.%s" % - get_locale().language) + get_locale().language) msg = Message(message_title, - body=message_body, - recipients=[project.contact_email]) + body=message_body, + recipients=[project.contact_email]) try: mail.send(msg) except SMTPRecipientsRefused: @@ -211,7 +214,7 @@ def create_project(): # redirect the user to the next step (invite) flash(_("%(msg_compl)sThe project identifier is %(project)s", - msg_compl=msg_compl, project=project.id)) + msg_compl=msg_compl, project=project.id)) return redirect(url_for(".invite", project_id=project.id)) return render_template("create_project.html", form=form) @@ -228,8 +231,8 @@ def remind_password(): # send the password reminder password_reminder = "password_reminder.%s" % get_locale().language mail.send(Message("password recovery", - body=render_template(password_reminder, project=project), - recipients=[project.contact_email])) + body=render_template(password_reminder, project=project), + recipients=[project.contact_email])) flash(_("a mail has been sent to you with the password")) return render_template("password_reminder.html", form=form) @@ -267,7 +270,7 @@ def edit_project(): attachment_filename="%s-%s.%s" % (g.project.id, export_type, export_format), as_attachment=True - ) + ) else: edit_form.name.data = g.project.name edit_form.password.data = g.project.password @@ -308,7 +311,7 @@ def demo(): project_id='demo')) if not project and is_demo_project_activated: project = Project(id="demo", name=u"demonstration", password="demo", - contact_email="demo@notmyidea.org") + contact_email="demo@notmyidea.org") db.session.add(project) db.session.commit() session[project.id] = project.password @@ -326,14 +329,14 @@ def invite(): # send the email message_body = render_template("invitation_mail.%s" % - get_locale().language) + get_locale().language) message_title = _("You have been invited to share your " - "expenses for %(project)s", project=g.project.name) + "expenses for %(project)s", project=g.project.name) msg = Message(message_title, - body=message_body, - recipients=[email.strip() - for email in form.emails.data.split(",")]) + body=message_body, + recipients=[email.strip() + for email in form.emails.data.split(",")]) mail.send(msg) flash(_("Your invitations have been sent")) return redirect(url_for(".list_bills")) @@ -351,11 +354,11 @@ def list_bills(): bills = g.project.get_bills().options(orm.subqueryload(Bill.owers)) return render_template("list_bills.html", - bills=bills, member_form=MemberForm(g.project), - bill_form=bill_form, - add_bill=request.values.get('add_bill', False), - current_view="list_bills", - ) + bills=bills, member_form=MemberForm(g.project), + bill_form=bill_form, + add_bill=request.values.get('add_bill', False), + current_view="list_bills", + ) @main.route("//members/add", methods=["GET", "POST"]) @@ -375,7 +378,7 @@ def add_member(): @main.route("//members//reactivate", methods=["POST"]) def reactivate(member_id): person = Person.query.filter(Person.id == member_id)\ - .filter(Project.id == g.project.id).all() + .filter(Project.id == g.project.id).all() if person: person[0].activated = True db.session.commit() diff --git a/docs/installation.rst b/docs/installation.rst index 3cd143d0..618421ef 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -76,7 +76,8 @@ properly. | Setting name | Default | What does it do? | +============================+===========================+========================================================================================+ | SQLALCHEMY_DATABASE_URI | ``sqlite:///budget.db`` | Specifies the type of backend to use and its location. More information | -| | | on the format used can be found on `the SQLAlchemy documentation`. | +| | | on the format used can be found on `the SQLAlchemy documentation | +| | | `_. | +----------------------------+---------------------------+----------------------------------------------------------------------------------------+ | SECRET_KEY | ``tralala`` | The secret key used to encrypt the cookies. **This needs to be changed**. | +----------------------------+---------------------------+----------------------------------------------------------------------------------------+ @@ -90,8 +91,6 @@ properly. | | | and copy its output into the value of *ADMIN_PASSWORD*. | +----------------------------+---------------------------+----------------------------------------------------------------------------------------+ -.. _`the SQLAlechemy documentation`: http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls - In a production environment --------------------------- From e70bd029b4470423b34d8dfffc6adb6db192bbe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20M=C3=A9taireau?= Date: Wed, 28 Jun 2017 22:00:35 +0200 Subject: [PATCH 2/2] pep8ify the tests --- budget/tests/tests.py | 121 ++++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 59 deletions(-) diff --git a/budget/tests/tests.py b/budget/tests/tests.py index 386920f5..7c739ce8 100644 --- a/budget/tests/tests.py +++ b/budget/tests/tests.py @@ -58,7 +58,7 @@ class TestCase(unittest.TestCase): """Create a fake project""" # create the project self.app.post("/create", data={ - 'name': name, + 'name': name, 'id': name, 'password': name, 'contact_email': '%s@notmyidea.org' % name @@ -66,7 +66,7 @@ class TestCase(unittest.TestCase): def create_project(self, name): models.db.session.add(models.Project(id=name, name=six.text_type(name), - password=name, contact_email="%s@notmyidea.org" % name)) + password=name, contact_email="%s@notmyidea.org" % name)) models.db.session.commit() @@ -127,12 +127,12 @@ class BudgetTestCase(TestCase): # sending a message to multiple persons with run.mail.record_messages() as outbox: self.app.post("/raclette/invite", - data={"emails": 'alexis@notmyidea.org, toto@notmyidea.org'}) + data={"emails": 'alexis@notmyidea.org, toto@notmyidea.org'}) # only one message is sent to multiple persons self.assertEqual(len(outbox), 1) self.assertEqual(outbox[0].recipients, - ["alexis@notmyidea.org", "toto@notmyidea.org"]) + ["alexis@notmyidea.org", "toto@notmyidea.org"]) # mail address checking with run.mail.record_messages() as outbox: @@ -144,7 +144,7 @@ class BudgetTestCase(TestCase): # mixing good and wrong adresses shouldn't send any messages with run.mail.record_messages() as outbox: self.app.post("/raclette/invite", - data={"emails": 'alexis@notmyidea.org, alexis'}) # not valid + data={"emails": 'alexis@notmyidea.org, alexis'}) # not valid # only one message is sent to multiple persons self.assertEqual(len(outbox), 0) @@ -239,7 +239,7 @@ class BudgetTestCase(TestCase): # remove fred self.app.post("/raclette/members/%s/delete" % - models.Project.query.get("raclette").members[-1].id) + models.Project.query.get("raclette").members[-1].id) # as fred is not bound to any bill, he is removed self.assertEqual(len(models.Project.query.get("raclette").members), 1) @@ -263,7 +263,7 @@ class BudgetTestCase(TestCase): # he is still in the database, but is deactivated self.assertEqual(len(models.Project.query.get("raclette").members), 2) self.assertEqual( - len(models.Project.query.get("raclette").active_members), 1) + len(models.Project.query.get("raclette").active_members), 1) # as fred is now deactivated, check that he is not listed when adding # a bill or displaying the balance @@ -276,7 +276,7 @@ class BudgetTestCase(TestCase): # adding him again should reactivate him self.app.post("/raclette/members/add", data={'name': 'fred'}) self.assertEqual( - len(models.Project.query.get("raclette").active_members), 2) + len(models.Project.query.get("raclette").active_members), 2) # adding an user with the same name as another user from a different # project should not cause any troubles @@ -284,7 +284,7 @@ class BudgetTestCase(TestCase): self.login("randomid") self.app.post("/randomid/members/add", data={'name': 'fred'}) self.assertEqual( - len(models.Project.query.get("randomid").active_members), 1) + len(models.Project.query.get("randomid").active_members), 1) def test_person_model(self): self.post_project("raclette") @@ -321,11 +321,11 @@ class BudgetTestCase(TestCase): response = self.app.get("/raclette/members/1/delete") self.assertEqual(response.status_code, 405) - #delete user using POST method + # delete user using POST method self.app.post("/raclette/members/1/delete") self.assertEqual( - len(models.Project.query.get("raclette").active_members), 0) - #try to delete an user already deleted + len(models.Project.query.get("raclette").active_members), 0) + # try to delete an user already deleted self.app.post("/raclette/members/1/delete") def test_demo(self): @@ -358,7 +358,7 @@ class BudgetTestCase(TestCase): # try to connect with wrong credentials should not work with run.app.test_client() as c: resp = c.post("/authenticate", - data={'id': 'raclette', 'password': 'nope'}) + data={'id': 'raclette', 'password': 'nope'}) self.assertIn("Authentication", resp.data.decode('utf-8')) self.assertNotIn('raclette', session) @@ -366,7 +366,7 @@ class BudgetTestCase(TestCase): # try to connect with the right credentials should work with run.app.test_client() as c: resp = c.post("/authenticate", - data={'id': 'raclette', 'password': 'raclette'}) + data={'id': 'raclette', 'password': 'raclette'}) self.assertNotIn("Authentication", resp.data.decode('utf-8')) self.assertIn('raclette', session) @@ -461,7 +461,7 @@ class BudgetTestCase(TestCase): balance = models.Project.query.get("raclette").balance self.assertEqual(set(balance.values()), set([19.0, -19.0])) - #Bill with negative amount + # Bill with negative amount self.app.post("/raclette/add", data={ 'date': '2011-08-12', 'what': 'fromage à raclette', @@ -472,7 +472,7 @@ class BudgetTestCase(TestCase): bill = models.Bill.query.filter(models.Bill.date == '2011-08-12')[0] self.assertEqual(bill.amount, -25) - #add a bill with a comma + # add a bill with a comma self.app.post("/raclette/add", data={ 'date': '2011-08-01', 'what': 'fromage à raclette', @@ -520,15 +520,14 @@ class BudgetTestCase(TestCase): self.app.post("/raclette/members/add", data={'name': 'alexis'}) self.app.post("/raclette/members/add", data={'name': 'tata', 'weight': 1}) - resp = self.app.get("/raclette/") + resp = self.app.get("/raclette/") self.assertIn('extra-info', resp.data.decode('utf-8')) self.app.post("/raclette/members/add", data={'name': 'freddy familly', 'weight': 4}) - resp = self.app.get("/raclette/") + resp = self.app.get("/raclette/") self.assertNotIn('extra-info', resp.data.decode('utf-8')) - def test_rounding(self): self.post_project("raclette") @@ -568,7 +567,8 @@ class BudgetTestCase(TestCase): result[models.Project.query.get("raclette").members[1].id] = 0.0 result[models.Project.query.get("raclette").members[2].id] = -8.12 # Since we're using floating point to store currency, we can have some rounding issues that prevent test from working. - # However, we should obtain the same values as the theorical ones if we round to 2 decimals, like in the UI. + # However, we should obtain the same values as the theorical ones if we + # round to 2 decimals, like in the UI. for key, value in six.iteritems(balance): self.assertEqual(round(value, 2), result[key]) @@ -583,7 +583,7 @@ class BudgetTestCase(TestCase): } resp = self.app.post("/raclette/edit", data=new_data, - follow_redirects=True) + follow_redirects=True) self.assertEqual(resp.status_code, 200) project = models.Project.query.get("raclette") @@ -594,7 +594,7 @@ class BudgetTestCase(TestCase): new_data['contact_email'] = 'wrong_email' resp = self.app.post("/raclette/edit", data=new_data, - follow_redirects=True) + follow_redirects=True) self.assertIn("Invalid email address", resp.data.decode('utf-8')) def test_dashboard(self): @@ -613,7 +613,7 @@ class BudgetTestCase(TestCase): self.app.post("/raclette/members/add", data={'name': 'alexis'}) self.app.post("/raclette/members/add", data={'name': 'fred'}) self.app.post("/raclette/members/add", data={'name': 'tata'}) - #Add a member with a balance=0 : + # Add a member with a balance=0 : self.app.post("/raclette/members/add", data={'name': 'toto'}) # create bills @@ -640,13 +640,13 @@ class BudgetTestCase(TestCase): 'payed_for': [1, 2], 'amount': '10', }) - project = models.Project.query.get('raclette') + project = models.Project.query.get('raclette') transactions = project.get_transactions_to_settle_bill() members = defaultdict(int) - #We should have the same values between transactions and project balances + # We should have the same values between transactions and project balances for t in transactions: - members[t['ower']]-=t['amount'] - members[t['receiver']]+=t['amount'] + members[t['ower']] -= t['amount'] + members[t['receiver']] += t['amount'] balance = models.Project.query.get("raclette").balance for m, a in members.items(): self.assertEqual(a, balance[m.id]) @@ -684,7 +684,7 @@ class BudgetTestCase(TestCase): 'payed_for': [2], 'amount': '13.33', }) - project = models.Project.query.get('raclette') + project = models.Project.query.get('raclette') transactions = project.get_transactions_to_settle_bill() members = defaultdict(int) # There should not be any zero-amount transfer after rounding @@ -805,6 +805,7 @@ class BudgetTestCase(TestCase): class APITestCase(TestCase): + """Tests the API""" def api_create(self, name, id=None, password=None, contact=None): @@ -833,7 +834,7 @@ class APITestCase(TestCase): def assertStatus(self, expected, resp, url=""): return self.assertEqual(expected, resp.status_code, - "%s expected %s, got %s" % (url, expected, resp.status_code)) + "%s expected %s, got %s" % (url, expected, resp.status_code)) def test_basic_auth(self): # create a project @@ -850,15 +851,15 @@ class APITestCase(TestCase): for resource in ("/raclette/members", "/raclette/bills"): url = "/api/projects" + resource self.assertStatus(401, getattr(self.app, verb)(url), - verb + resource) + verb + resource) for verb in ('get', 'delete', 'put'): for resource in ("/raclette", "/raclette/members/1", - "/raclette/bills/1"): + "/raclette/bills/1"): url = "/api/projects" + resource self.assertStatus(401, getattr(self.app, verb)(url), - verb + resource) + verb + resource) def test_project(self): # wrong email should return an error @@ -885,7 +886,7 @@ class APITestCase(TestCase): # get information about it resp = self.app.get("/api/projects/raclette", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) self.assertTrue(200, resp.status_code) expected = { @@ -904,12 +905,12 @@ class APITestCase(TestCase): "contact_email": "yeah@notmyidea.org", "password": "raclette", "name": "The raclette party", - }, headers=self.get_auth("raclette")) + }, headers=self.get_auth("raclette")) self.assertEqual(200, resp.status_code) resp = self.app.get("/api/projects/raclette", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) self.assertEqual(200, resp.status_code) expected = { @@ -925,13 +926,13 @@ class APITestCase(TestCase): # delete should work resp = self.app.delete("/api/projects/raclette", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) self.assertEqual(200, resp.status_code) # get should return a 401 on an unknown resource resp = self.app.get("/api/projects/raclette", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) self.assertEqual(401, resp.status_code) def test_member(self): @@ -940,15 +941,15 @@ class APITestCase(TestCase): # get the list of members (should be empty) req = self.app.get("/api/projects/raclette/members", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) self.assertStatus(200, req) self.assertEqual('[]', req.data.decode('utf-8')) # add a member req = self.app.post("/api/projects/raclette/members", data={ - "name": "Alexis" - }, headers=self.get_auth("raclette")) + "name": "Alexis" + }, headers=self.get_auth("raclette")) # the id of the new member should be returned self.assertStatus(201, req) @@ -956,21 +957,21 @@ class APITestCase(TestCase): # the list of members should contain one member req = self.app.get("/api/projects/raclette/members", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) self.assertStatus(200, req) self.assertEqual(len(json.loads(req.data.decode('utf-8'))), 1) # edit this member req = self.app.put("/api/projects/raclette/members/1", data={ - "name": "Fred" - }, headers=self.get_auth("raclette")) + "name": "Fred" + }, headers=self.get_auth("raclette")) self.assertStatus(200, req) # get should return the new name req = self.app.get("/api/projects/raclette/members/1", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) self.assertStatus(200, req) self.assertEqual("Fred", json.loads(req.data.decode('utf-8'))["name"]) @@ -978,14 +979,14 @@ class APITestCase(TestCase): # delete a member req = self.app.delete("/api/projects/raclette/members/1", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) self.assertStatus(200, req) # the list of members should be empty # get the list of members (should be empty) req = self.app.get("/api/projects/raclette/members", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) self.assertStatus(200, req) self.assertEqual('[]', req.data.decode('utf-8')) @@ -1001,7 +1002,7 @@ class APITestCase(TestCase): # get the list of bills (should be empty) req = self.app.get("/api/projects/raclette/bills", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) self.assertStatus(200, req) self.assertEqual("[]", req.data.decode('utf-8')) @@ -1013,7 +1014,7 @@ class APITestCase(TestCase): 'payer': "1", 'payed_for': ["1", "2"], 'amount': '25', - }, headers=self.get_auth("raclette")) + }, headers=self.get_auth("raclette")) # should return the id self.assertStatus(201, req) @@ -1021,7 +1022,7 @@ class APITestCase(TestCase): # get this bill details req = self.app.get("/api/projects/raclette/bills/1", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) # compare with the added info self.assertStatus(200, req) @@ -1039,7 +1040,7 @@ class APITestCase(TestCase): # the list of bills should lenght 1 req = self.app.get("/api/projects/raclette/bills", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) self.assertStatus(200, req) self.assertEqual(1, len(json.loads(req.data.decode('utf-8')))) @@ -1050,7 +1051,7 @@ class APITestCase(TestCase): 'payer': "1", 'payed_for': ["1", "2"], 'amount': '25', - }, headers=self.get_auth("raclette")) + }, headers=self.get_auth("raclette")) self.assertStatus(400, req) self.assertEqual('{"date": ["This field is required."]}', req.data.decode('utf-8')) @@ -1062,11 +1063,11 @@ class APITestCase(TestCase): 'payer': "2", 'payed_for': ["1", "2"], 'amount': '25', - }, headers=self.get_auth("raclette")) + }, headers=self.get_auth("raclette")) # check its fields req = self.app.get("/api/projects/raclette/bills/1", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) expected = { "what": "beer", @@ -1082,17 +1083,17 @@ class APITestCase(TestCase): # delete a bill req = self.app.delete("/api/projects/raclette/bills/1", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) self.assertStatus(200, req) # getting it should return a 404 req = self.app.get("/api/projects/raclette/bills/1", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) self.assertStatus(404, req) def test_username_xss(self): # create a project - #self.api_create("raclette") + # self.api_create("raclette") self.post_project("raclette") self.login("raclette") @@ -1118,11 +1119,11 @@ class APITestCase(TestCase): 'payer': "1", 'payed_for': ["1", "2"], 'amount': '25', - }, headers=self.get_auth("raclette")) + }, headers=self.get_auth("raclette")) # get this bill details req = self.app.get("/api/projects/raclette/bills/1", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) # compare with the added info self.assertStatus(200, req) @@ -1139,7 +1140,7 @@ class APITestCase(TestCase): # getting it should return a 404 req = self.app.get("/api/projects/raclette", - headers=self.get_auth("raclette")) + headers=self.get_auth("raclette")) expected = { "active_members": [ @@ -1162,7 +1163,9 @@ class APITestCase(TestCase): self.assertStatus(200, req) self.assertEqual(expected, json.loads(req.data.decode('utf-8'))) + class ServerTestCase(APITestCase): + def setUp(self): run.configure() super(ServerTestCase, self).setUp()