mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-05-06 21:11:49 +02:00
Show some respect to pep8 and fellow readers.
This commit is contained in:
parent
b5777e0584
commit
eade8519cb
8 changed files with 133 additions and 83 deletions
|
@ -76,7 +76,8 @@ properly.
|
||||||
| Setting name | Default | What does it do? |
|
| 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 |
|
| SQLALCHEMY_DATABASE_URI | ``sqlite:///budget.db`` | Specifies the type of backend to use and its location. More information |
|
||||||
| | | on the format used can be found on `the SQLAlchemy documentation`. |
|
| | | on the format used can be found on `the SQLAlchemy documentation |
|
||||||
|
| | | <http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls>`_. |
|
||||||
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
+----------------------------+---------------------------+----------------------------------------------------------------------------------------+
|
||||||
| SECRET_KEY | ``tralala`` | The secret key used to encrypt the cookies. **This needs to be changed**. |
|
| 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*. |
|
| | | 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
|
In a production environment
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
|
|
|
@ -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
|
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://'
|
SQLALCHEMY_DATABASE_URI = 'sqlite://'
|
||||||
SQLACHEMY_ECHO = DEBUG
|
SQLACHEMY_ECHO = DEBUG
|
||||||
|
|
||||||
# Will likely become the default value in flask-sqlalchemy >=3 ; could be removed
|
# Will likely become the default value in flask-sqlalchemy >=3 ; could be removed
|
||||||
# then:
|
# then:
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
|
# You need to change this secret key, otherwise bad things might happen to your
|
||||||
|
# users.
|
||||||
SECRET_KEY = "tralala"
|
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")
|
MAIL_DEFAULT_SENDER = ("Budget manager", "budget@notmyidea.org")
|
||||||
|
|
||||||
|
# If set to True, a demonstration project will be activated.
|
||||||
ACTIVATE_DEMO_PROJECT = True
|
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 = ""
|
ADMIN_PASSWORD = ""
|
||||||
|
|
|
@ -13,6 +13,7 @@ from jinja2 import Markup
|
||||||
from ihatemoney.models import Project, Person
|
from ihatemoney.models import Project, Person
|
||||||
from ihatemoney.utils import slugify
|
from ihatemoney.utils import slugify
|
||||||
|
|
||||||
|
|
||||||
def get_billform_for(project, set_default=True, **kwargs):
|
def get_billform_for(project, set_default=True, **kwargs):
|
||||||
"""Return an instance of BillForm configured for a particular project.
|
"""Return an instance of BillForm configured for a particular project.
|
||||||
|
|
||||||
|
@ -21,8 +22,9 @@ def get_billform_for(project, set_default=True, **kwargs):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
form = BillForm(**kwargs)
|
form = BillForm(**kwargs)
|
||||||
form.payed_for.choices = form.payer.choices = [(m.id, m.name)
|
active_members = [(m.id, m.name) for m in project.active_members]
|
||||||
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]
|
form.payed_for.default = [m.id for m in project.active_members]
|
||||||
|
|
||||||
if set_default and request.method == "GET":
|
if set_default and request.method == "GET":
|
||||||
|
@ -31,7 +33,9 @@ def get_billform_for(project, set_default=True, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
class CommaDecimalField(DecimalField):
|
class CommaDecimalField(DecimalField):
|
||||||
|
|
||||||
"""A class to deal with comma in Decimal Field"""
|
"""A class to deal with comma in Decimal Field"""
|
||||||
|
|
||||||
def process_formdata(self, value):
|
def process_formdata(self, value):
|
||||||
if value:
|
if value:
|
||||||
value[0] = str(value[0]).replace(',', '.')
|
value[0] = str(value[0]).replace(',', '.')
|
||||||
|
@ -70,12 +74,13 @@ class ProjectForm(EditProjectForm):
|
||||||
def validate_id(form, field):
|
def validate_id(form, field):
|
||||||
form.id.data = slugify(field.data)
|
form.id.data = slugify(field.data)
|
||||||
if (form.id.data == "dashboard") or Project.query.get(form.id.data):
|
if (form.id.data == "dashboard") or Project.query.get(form.id.data):
|
||||||
raise ValidationError(Markup(_("The project identifier is used "
|
message = _("The project identifier is used to log in and for the "
|
||||||
"to log in and for the URL of the project. "
|
"URL of the project. "
|
||||||
"We tried to generate an identifier for you but a project "
|
"We tried to generate an identifier for you but a "
|
||||||
"with this identifier already exists. "
|
"project with this identifier already exists. "
|
||||||
"Please create a new identifier "
|
"Please create a new identifier that you will be able "
|
||||||
"that you will be able to remember.")))
|
"to remember")
|
||||||
|
raise ValidationError(Markup(message))
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationForm(FlaskForm):
|
class AuthenticationForm(FlaskForm):
|
||||||
|
@ -179,13 +184,13 @@ class InviteForm(FlaskForm):
|
||||||
|
|
||||||
|
|
||||||
class ExportForm(FlaskForm):
|
class ExportForm(FlaskForm):
|
||||||
export_type = SelectField(_("What do you want to download ?"),
|
export_type = SelectField(
|
||||||
|
_("What do you want to download ?"),
|
||||||
validators=[Required()],
|
validators=[Required()],
|
||||||
coerce=str,
|
coerce=str,
|
||||||
choices=[("bills", _("bills")), ("transactions", _("transactions"))]
|
choices=[("bills", _("bills")), ("transactions", _("transactions"))])
|
||||||
)
|
export_format = SelectField(
|
||||||
export_format = SelectField(_("Export file format"),
|
_("Export file format"),
|
||||||
validators=[Required()],
|
validators=[Required()],
|
||||||
coerce=str,
|
coerce=str,
|
||||||
choices=[("csv", "csv"), ("json", "json")]
|
choices=[("csv", "csv"), ("json", "json")])
|
||||||
)
|
|
||||||
|
|
|
@ -10,7 +10,8 @@ from ihatemoney.models import db
|
||||||
|
|
||||||
|
|
||||||
class GeneratePasswordHash(Command):
|
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):
|
def run(self):
|
||||||
password = getpass(prompt='Password: ')
|
password = getpass(prompt='Password: ')
|
||||||
|
|
|
@ -9,9 +9,6 @@ from sqlalchemy import orm
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
|
|
||||||
# define models
|
|
||||||
|
|
||||||
|
|
||||||
class Project(db.Model):
|
class Project(db.Model):
|
||||||
|
|
||||||
_to_serialize = ("id", "name", "password", "contact_email",
|
_to_serialize = ("id", "name", "password", "contact_email",
|
||||||
|
@ -37,7 +34,8 @@ class Project(db.Model):
|
||||||
# for each person
|
# for each person
|
||||||
for person in self.members:
|
for person in self.members:
|
||||||
# get the list of bills he has to pay
|
# 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():
|
for bill in bills.all():
|
||||||
if person != bill.payer:
|
if person != bill.payer:
|
||||||
share = bill.pay_each() * person.weight
|
share = bill.pay_each() * person.weight
|
||||||
|
@ -56,6 +54,7 @@ class Project(db.Model):
|
||||||
|
|
||||||
def get_transactions_to_settle_bill(self, pretty_output=False):
|
def get_transactions_to_settle_bill(self, pretty_output=False):
|
||||||
"""Return a list of transactions that could be made to settle the bill"""
|
"""Return a list of transactions that could be made to settle the bill"""
|
||||||
|
|
||||||
def prettify(transactions, pretty_output):
|
def prettify(transactions, pretty_output):
|
||||||
""" Return pretty transactions
|
""" Return pretty transactions
|
||||||
"""
|
"""
|
||||||
|
@ -63,9 +62,11 @@ class Project(db.Model):
|
||||||
return transactions
|
return transactions
|
||||||
pretty_transactions = []
|
pretty_transactions = []
|
||||||
for transaction in transactions:
|
for transaction in transactions:
|
||||||
pretty_transactions.append({'ower': transaction['ower'].name,
|
pretty_transactions.append({
|
||||||
|
'ower': transaction['ower'].name,
|
||||||
'receiver': transaction['receiver'].name,
|
'receiver': transaction['receiver'].name,
|
||||||
'amount': round(transaction['amount'], 2)})
|
'amount': round(transaction['amount'], 2)
|
||||||
|
})
|
||||||
return pretty_transactions
|
return pretty_transactions
|
||||||
|
|
||||||
# cache value for better performance
|
# cache value for better performance
|
||||||
|
@ -77,22 +78,36 @@ class Project(db.Model):
|
||||||
credits.append({"person": person, "balance": balance[person.id]})
|
credits.append({"person": person, "balance": balance[person.id]})
|
||||||
elif round(balance[person.id], 2) < 0:
|
elif round(balance[person.id], 2) < 0:
|
||||||
debts.append({"person": person, "balance": -balance[person.id]})
|
debts.append({"person": person, "balance": -balance[person.id]})
|
||||||
|
|
||||||
# Try and find exact matches
|
# Try and find exact matches
|
||||||
for credit in credits:
|
for credit in credits:
|
||||||
match = self.exactmatch(round(credit["balance"], 2), debts)
|
match = self.exactmatch(round(credit["balance"], 2), debts)
|
||||||
if match:
|
if match:
|
||||||
for m in 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)
|
debts.remove(m)
|
||||||
credits.remove(credit)
|
credits.remove(credit)
|
||||||
# Split any remaining debts & credits
|
# Split any remaining debts & credits
|
||||||
while credits and debts:
|
while credits and debts:
|
||||||
|
|
||||||
if credits[0]["balance"] > debts[0]["balance"]:
|
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"]
|
credits[0]["balance"] = credits[0]["balance"] - debts[0]["balance"]
|
||||||
del debts[0]
|
del debts[0]
|
||||||
else:
|
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"]
|
debts[0]["balance"] = debts[0]["balance"] - credits[0]["balance"]
|
||||||
del credits[0]
|
del credits[0]
|
||||||
|
|
||||||
|
@ -136,12 +151,15 @@ class Project(db.Model):
|
||||||
owers = [ower.name for ower in bill.owers]
|
owers = [ower.name for ower in bill.owers]
|
||||||
else:
|
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,
|
|
||||||
|
pretty_bills.append({
|
||||||
|
"what": bill.what,
|
||||||
"amount": round(bill.amount, 2),
|
"amount": round(bill.amount, 2),
|
||||||
"date": str(bill.date),
|
"date": str(bill.date),
|
||||||
"payer_name": Person.query.get(bill.payer_id).name,
|
"payer_name": Person.query.get(bill.payer_id).name,
|
||||||
"payer_weight": Person.query.get(bill.payer_id).weight,
|
"payer_weight": Person.query.get(bill.payer_id).weight,
|
||||||
"owers": owers})
|
"owers": owers
|
||||||
|
})
|
||||||
return pretty_bills
|
return pretty_bills
|
||||||
|
|
||||||
def remove_member(self, member_id):
|
def remove_member(self, member_id):
|
||||||
|
@ -176,6 +194,7 @@ class Project(db.Model):
|
||||||
class Person(db.Model):
|
class Person(db.Model):
|
||||||
|
|
||||||
class PersonQuery(BaseQuery):
|
class PersonQuery(BaseQuery):
|
||||||
|
|
||||||
def get_by_name(self, name, project):
|
def get_by_name(self, name, project):
|
||||||
return Person.query.filter(Person.name == name)\
|
return Person.query.filter(Person.name == name)\
|
||||||
.filter(Project.id == project.id).one()
|
.filter(Project.id == project.id).one()
|
||||||
|
@ -212,7 +231,8 @@ class Person(db.Model):
|
||||||
return "<Person %s for project %s>" % (self.name, self.project.name)
|
return "<Person %s for project %s>" % (self.name, self.project.name)
|
||||||
|
|
||||||
# We need to manually define a join table for m2m relations
|
# 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('bill_id', db.Integer, db.ForeignKey('bill.id')),
|
||||||
db.Column('person_id', db.Integer, db.ForeignKey('person.id')),
|
db.Column('person_id', db.Integer, db.ForeignKey('person.id')),
|
||||||
)
|
)
|
||||||
|
@ -224,11 +244,11 @@ class Bill(db.Model):
|
||||||
|
|
||||||
def get(self, project, id):
|
def get(self, project, id):
|
||||||
try:
|
try:
|
||||||
return self.join(Person, Project)\
|
return (self.join(Person, Project)
|
||||||
.filter(Bill.payer_id == Person.id)\
|
.filter(Bill.payer_id == Person.id)
|
||||||
.filter(Person.project_id == Project.id)\
|
.filter(Person.project_id == Project.id)
|
||||||
.filter(Project.id == project.id)\
|
.filter(Project.id == project.id)
|
||||||
.filter(Bill.id == id).one()
|
.filter(Bill.id == id).one())
|
||||||
except orm.exc.NoResultFound:
|
except orm.exc.NoResultFound:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -262,8 +282,10 @@ class Bill(db.Model):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Bill of %s from %s for %s>" % (self.amount,
|
return "<Bill of %s from %s for %s>" % (
|
||||||
self.payer, ", ".join([o.name for o in self.owers]))
|
self.amount,
|
||||||
|
self.payer, ", ".join([o.name for o in self.owers])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Archive(db.Model):
|
class Archive(db.Model):
|
||||||
|
|
|
@ -84,7 +84,6 @@ class DefaultConfigurationTestCase(BaseTestCase):
|
||||||
("Budget manager", "budget@notmyidea.org"))
|
("Budget manager", "budget@notmyidea.org"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BudgetTestCase(IhatemoneyTestCase):
|
class BudgetTestCase(IhatemoneyTestCase):
|
||||||
|
|
||||||
def test_notifications(self):
|
def test_notifications(self):
|
||||||
|
|
|
@ -26,7 +26,9 @@ def slugify(value):
|
||||||
value = six.text_type(re.sub('[^\w\s-]', '', value).strip().lower())
|
value = six.text_type(re.sub('[^\w\s-]', '', value).strip().lower())
|
||||||
return re.sub('[-\s]+', '-', value)
|
return re.sub('[-\s]+', '-', value)
|
||||||
|
|
||||||
|
|
||||||
class Redirect303(HTTPException, RoutingException):
|
class Redirect303(HTTPException, RoutingException):
|
||||||
|
|
||||||
"""Raise if the map requests a redirect. This is for example the case if
|
"""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.
|
`strict_slashes` are activated and an url that requires a trailing slash.
|
||||||
|
|
||||||
|
@ -43,6 +45,7 @@ class Redirect303(HTTPException, RoutingException):
|
||||||
|
|
||||||
|
|
||||||
class PrefixedWSGI(object):
|
class PrefixedWSGI(object):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Wrap the application in this middleware and configure the
|
Wrap the application in this middleware and configure the
|
||||||
front-end server to add these headers, to let you quietly bind
|
front-end server to add these headers, to let you quietly bind
|
||||||
|
@ -55,6 +58,7 @@ class PrefixedWSGI(object):
|
||||||
|
|
||||||
:param app: the WSGI application
|
:param app: the WSGI application
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.wsgi_app = app.wsgi_app
|
self.wsgi_app = app.wsgi_app
|
||||||
|
@ -85,12 +89,14 @@ def minimal_round(*args, **kw):
|
||||||
ires = int(res)
|
ires = int(res)
|
||||||
return (res if res != ires else ires)
|
return (res if res != ires else ires)
|
||||||
|
|
||||||
|
|
||||||
def list_of_dicts2json(dict_to_convert):
|
def list_of_dicts2json(dict_to_convert):
|
||||||
"""Take a list of dictionnaries and turns it into
|
"""Take a list of dictionnaries and turns it into
|
||||||
a json in-memory file
|
a json in-memory file
|
||||||
"""
|
"""
|
||||||
return BytesIO(dumps(dict_to_convert).encode('utf-8'))
|
return BytesIO(dumps(dict_to_convert).encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
def list_of_dicts2csv(dict_to_convert):
|
def list_of_dicts2csv(dict_to_convert):
|
||||||
"""Take a list of dictionnaries and turns it into
|
"""Take a list of dictionnaries and turns it into
|
||||||
a csv in-memory file, assume all dict have the same keys
|
a csv in-memory file, assume all dict have the same keys
|
||||||
|
@ -110,7 +116,8 @@ def list_of_dicts2csv(dict_to_convert):
|
||||||
csv_data = []
|
csv_data = []
|
||||||
csv_data.append([key.encode('utf-8') for key in dict_to_convert[0].keys()])
|
csv_data.append([key.encode('utf-8') for key in dict_to_convert[0].keys()])
|
||||||
for dic in dict_to_convert:
|
for dic in dict_to_convert:
|
||||||
csv_data.append([dic[h].encode('utf8')
|
csv_data.append(
|
||||||
|
[dic[h].encode('utf8')
|
||||||
if isinstance(dic[h], unicode) else str(dic[h]).encode('utf8')
|
if isinstance(dic[h], unicode) else str(dic[h]).encode('utf8')
|
||||||
for h in dict_to_convert[0].keys()])
|
for h in dict_to_convert[0].keys()])
|
||||||
except (KeyError, IndexError):
|
except (KeyError, IndexError):
|
||||||
|
|
Loading…
Reference in a new issue