From 3a4282fd75e3b3317b2b08b4aa2e6ac154310e73 Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Fri, 7 Jul 2017 00:06:56 +0200 Subject: Absolute imports & some other improvements (#243) * Use absolute imports and rename package to ihatemoney * Add a ihatemoney command * Factorize application creation logic * Refactor the tests * Update the wsgi.py module with the new create_app() function * Fix some styling thanks to Flake8. * Automate Flake8 check in the CI. --- budget/models.py | 283 ------------------------------------------------------- 1 file changed, 283 deletions(-) delete mode 100644 budget/models.py (limited to 'budget/models.py') diff --git a/budget/models.py b/budget/models.py deleted file mode 100644 index 1da4489..0000000 --- a/budget/models.py +++ /dev/null @@ -1,283 +0,0 @@ -from collections import defaultdict - -from datetime import datetime -from flask_sqlalchemy import SQLAlchemy, BaseQuery -from flask import g - -from sqlalchemy import orm - -db = SQLAlchemy() - - -# define models - - -class Project(db.Model): - - _to_serialize = ("id", "name", "password", "contact_email", - "members", "active_members", "balance") - - id = db.Column(db.String(64), primary_key=True) - - name = db.Column(db.UnicodeText) - password = db.Column(db.String(128)) - contact_email = db.Column(db.String(128)) - members = db.relationship("Person", backref="project") - - @property - def active_members(self): - return [m for m in self.members if m.activated] - - @property - def balance(self): - - 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)) - for bill in bills.all(): - if person != bill.payer: - share = bill.pay_each() * person.weight - should_pay[person] += share - should_receive[bill.payer] += share - - for person in self.members: - balance = should_receive[person] - should_pay[person] - balances[person.id] = balance - - return balances - - @property - def uses_weights(self): - return len([i for i in self.members if i.weight != 1]) > 0 - - 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 - """ - if not pretty_output: - return transactions - pretty_transactions = [] - for transaction in transactions: - pretty_transactions.append({'ower': transaction['ower'].name, - 'receiver': transaction['receiver'].name, - 'amount': round(transaction['amount'], 2)}) - return pretty_transactions - - #cache value for better performance - balance = self.balance - 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"]}) - 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"]}) - 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"]}) - debts[0]["balance"] = debts[0]["balance"] - credits[0]["balance"] - del credits[0] - - return prettify(transactions, pretty_output) - - def exactmatch(self, credit, debts): - """Recursively try and find subsets of 'debts' whose sum is equal to credit""" - if not debts: - return None - if debts[0]["balance"] > credit: - return self.exactmatch(credit, debts[1:]) - elif debts[0]["balance"] == credit: - return [debts[0]] - else: - match = self.exactmatch(credit-debts[0]["balance"], debts[1:]) - if match: - match.append(debts[0]) - else: - match = self.exactmatch(credit, debts[1:]) - return match - - def has_bills(self): - """return if the project do have bills or not""" - return self.get_bills().count() > 0 - - 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.id.desc()) - - def get_pretty_bills(self, export_format="json"): - """Return a list of project's bills with pretty formatting""" - bills = self.get_bills() - pretty_bills = [] - for bill in bills: - if export_format == "json": - owers = [ower.name for ower in bill.owers] - else: - owers = ', '.join([ower.name for ower in bill.owers]) - pretty_bills.append({"what": bill.what, - "amount": round(bill.amount, 2), - "date": str(bill.date), - "payer_name": Person.query.get(bill.payer_id).name, - "payer_weight": Person.query.get(bill.payer_id).weight, - "owers": owers}) - return pretty_bills - - def remove_member(self, member_id): - """Remove a member from the project. - - If the member is not bound to a bill, then he is deleted, otherwise - he is only deactivated. - - This method returns the status DELETED or DEACTIVATED regarding the - changes made. - """ - try: - person = Person.query.get(member_id, self) - except orm.exc.NoResultFound: - return None - if not person.has_bills(): - db.session.delete(person) - db.session.commit() - else: - person.activated = False - db.session.commit() - return person - - def remove_project(self): - db.session.delete(self) - db.session.commit() - - 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() - - 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() - - query_class = PersonQuery - - _to_serialize = ("id", "name", "weight", "activated") - - id = db.Column(db.Integer, primary_key=True) - project_id = db.Column(db.String(64), db.ForeignKey("project.id")) - bills = db.relationship("Bill", backref="payer") - - name = db.Column(db.UnicodeText) - weight = db.Column(db.Float, default=1) - activated = db.Column(db.Boolean, default=True) - - 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)\ - .count() - return bills_as_ower_number != 0 or len(self.bills) != 0 - - def __str__(self): - return self.name - - def __repr__(self): - return "" % (self.name, self.project.name) - -# 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')), -) - - -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() - except orm.exc.NoResultFound: - return None - - def delete(self, project, id): - bill = self.get(project, id) - if bill: - db.session.delete(bill) - return bill - - query_class = BillQuery - - _to_serialize = ("id", "payer_id", "owers", "amount", "date", "what") - - id = db.Column(db.Integer, primary_key=True) - - payer_id = db.Column(db.Integer, db.ForeignKey("person.id")) - owers = db.relationship(Person, secondary=billowers) - - amount = db.Column(db.Float) - date = db.Column(db.Date, default=datetime.now) - what = db.Column(db.UnicodeText) - - archive = db.Column(db.Integer, db.ForeignKey("archive.id")) - - def pay_each(self): - """Compute what each share has to pay""" - if self.owers: - # FIXME: SQL might dot that more efficiently - return self.amount / sum(i.weight for i in self.owers) - else: - return 0 - - def __repr__(self): - return "" % (self.amount, - self.payer, ", ".join([o.name for o in self.owers])) - - -class Archive(db.Model): - id = db.Column(db.Integer, primary_key=True) - project_id = db.Column(db.String(64), db.ForeignKey("project.id")) - name = db.Column(db.UnicodeText) - - @property - def start_date(self): - pass - - @property - def end_date(self): - pass - - def __repr__(self): - return "" -- cgit v1.1