This commit is contained in:
Alexis Metaireau 2017-06-28 20:00:45 +00:00 committed by GitHub
commit 9918b5afb6
9 changed files with 204 additions and 149 deletions

View file

@ -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:///budget.db' SQLALCHEMY_DATABASE_URI = 'sqlite:///budget.db'
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 = ""

View file

@ -13,6 +13,7 @@ from jinja2 import Markup
from .models import Project, Person from .models import Project, Person
from .utils import slugify from .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(',', '.')
@ -49,8 +53,8 @@ class EditProjectForm(FlaskForm):
Returns the created instance Returns the created instance
""" """
project = Project(name=self.name.data, id=self.id.data, project = Project(name=self.name.data, id=self.id.data,
password=self.password.data, password=self.password.data,
contact_email=self.contact_email.data) contact_email=self.contact_email.data)
return project return project
def update(self, project): def update(self, project):
@ -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):
@ -104,7 +109,7 @@ class BillForm(FlaskForm):
payer = SelectField(_("Payer"), validators=[Required()], coerce=int) payer = SelectField(_("Payer"), validators=[Required()], coerce=int)
amount = CommaDecimalField(_("Amount paid"), validators=[Required()]) amount = CommaDecimalField(_("Amount paid"), validators=[Required()])
payed_for = SelectMultipleField(_("For whom?"), payed_for = SelectMultipleField(_("For whom?"),
validators=[Required()], coerce=int) validators=[Required()], coerce=int)
submit = SubmitField(_("Submit")) submit = SubmitField(_("Submit"))
submit2 = SubmitField(_("Submit and add a new one")) submit2 = SubmitField(_("Submit and add a new one"))
@ -114,7 +119,7 @@ class BillForm(FlaskForm):
bill.what = self.what.data bill.what = self.what.data
bill.date = self.date.data bill.date = self.date.data
bill.owers = [Person.query.get(ower, project) bill.owers = [Person.query.get(ower, project)
for ower in self.payed_for.data] for ower in self.payed_for.data]
return bill return bill
@ -175,17 +180,17 @@ class InviteForm(FlaskForm):
for email in [email.strip() for email in form.emails.data.split(",")]: for email in [email.strip() for email in form.emails.data.split(",")]:
if not validator.regex.match(email): if not validator.regex.match(email):
raise ValidationError(_("The email %(email)s is not valid", raise ValidationError(_("The email %(email)s is not valid",
email=email)) email=email))
class ExportForm(FlaskForm): class ExportForm(FlaskForm):
export_type = SelectField(_("What do you want to download ?"), export_type = SelectField(
validators=[Required()], _("What do you want to download ?"),
coerce=str, validators=[Required()],
choices=[("bills", _("bills")), ("transactions", _("transactions"))] coerce=str,
) choices=[("bills", _("bills")), ("transactions", _("transactions"))])
export_format = SelectField(_("Export file format"), export_format = SelectField(
validators=[Required()], _("Export file format"),
coerce=str, validators=[Required()],
choices=[("csv", "csv"), ("json", "json")] coerce=str,
) choices=[("csv", "csv"), ("json", "json")])

View file

@ -10,7 +10,8 @@ from .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: ')

View file

@ -9,13 +9,10 @@ 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",
"members", "active_members", "balance") "members", "active_members", "balance")
id = db.Column(db.String(64), primary_key=True) id = db.Column(db.String(64), primary_key=True)
@ -32,12 +29,13 @@ class Project(db.Model):
def balance(self): def balance(self):
balances, should_pay, should_receive = (defaultdict(int) balances, should_pay, should_receive = (defaultdict(int)
for time in (1, 2, 3)) for time in (1, 2, 3))
# 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,36 +62,52 @@ 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({
'receiver': transaction['receiver'].name, 'ower': transaction['ower'].name,
'amount': round(transaction['amount'], 2)}) 'receiver': transaction['receiver'].name,
'amount': round(transaction['amount'], 2)
})
return pretty_transactions return pretty_transactions
#cache value for better performance # cache value for better performance
balance = self.balance balance = self.balance
credits, debts, transactions = [],[],[] credits, debts, transactions = [], [], []
# Create lists of credits and debts # Create lists of credits and debts
for person in self.members: for person in self.members:
if round(balance[person.id], 2) > 0: if round(balance[person.id], 2) > 0:
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]
@ -107,7 +122,7 @@ class Project(db.Model):
elif debts[0]["balance"] == credit: elif debts[0]["balance"] == credit:
return [debts[0]] return [debts[0]]
else: else:
match = self.exactmatch(credit-debts[0]["balance"], debts[1:]) match = self.exactmatch(credit - debts[0]["balance"], debts[1:])
if match: if match:
match.append(debts[0]) match.append(debts[0])
else: else:
@ -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,
"amount": round(bill.amount, 2), pretty_bills.append({
"date": str(bill.date), "what": bill.what,
"payer_name": Person.query.get(bill.payer_id).name, "amount": round(bill.amount, 2),
"payer_weight": Person.query.get(bill.payer_id).weight, "date": str(bill.date),
"owers": owers}) "payer_name": Person.query.get(bill.payer_id).name,
"payer_weight": Person.query.get(bill.payer_id).weight,
"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):

View file

@ -64,10 +64,10 @@ def configure():
# Since 2.0 # Since 2.0
warnings.warn( warnings.warn(
"The way Ihatemoney stores your ADMIN_PASSWORD has changed. You are using an unhashed" "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" + " 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'" + " 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" + " to generate a proper password HASH and copy the output to the value of"
+" ADMIN_PASSWORD in your settings file.", + " ADMIN_PASSWORD in your settings file.",
UserWarning UserWarning
) )
@ -106,6 +106,7 @@ babel = Babel(app)
# sentry # sentry
sentry = Sentry(app) sentry = Sentry(app)
@babel.localeselector @babel.localeselector
def get_locale(): def get_locale():
# get the lang from the session if defined, fallback on the browser "accept # get the lang from the session if defined, fallback on the browser "accept
@ -114,6 +115,7 @@ def get_locale():
setattr(g, 'lang', lang) setattr(g, 'lang', lang)
return lang return lang
def main(): def main():
app.run(host="0.0.0.0", debug=True) app.run(host="0.0.0.0", debug=True)

View file

@ -58,7 +58,7 @@ class TestCase(unittest.TestCase):
"""Create a fake project""" """Create a fake project"""
# create the project # create the project
self.app.post("/create", data={ self.app.post("/create", data={
'name': name, 'name': name,
'id': name, 'id': name,
'password': name, 'password': name,
'contact_email': '%s@notmyidea.org' % name 'contact_email': '%s@notmyidea.org' % name
@ -66,7 +66,7 @@ class TestCase(unittest.TestCase):
def create_project(self, name): def create_project(self, name):
models.db.session.add(models.Project(id=name, name=six.text_type(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() models.db.session.commit()
@ -127,12 +127,12 @@ class BudgetTestCase(TestCase):
# sending a message to multiple persons # sending a message to multiple persons
with run.mail.record_messages() as outbox: with run.mail.record_messages() as outbox:
self.app.post("/raclette/invite", 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 # only one message is sent to multiple persons
self.assertEqual(len(outbox), 1) self.assertEqual(len(outbox), 1)
self.assertEqual(outbox[0].recipients, self.assertEqual(outbox[0].recipients,
["alexis@notmyidea.org", "toto@notmyidea.org"]) ["alexis@notmyidea.org", "toto@notmyidea.org"])
# mail address checking # mail address checking
with run.mail.record_messages() as outbox: with run.mail.record_messages() as outbox:
@ -144,7 +144,7 @@ class BudgetTestCase(TestCase):
# mixing good and wrong adresses shouldn't send any messages # mixing good and wrong adresses shouldn't send any messages
with run.mail.record_messages() as outbox: with run.mail.record_messages() as outbox:
self.app.post("/raclette/invite", 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 # only one message is sent to multiple persons
self.assertEqual(len(outbox), 0) self.assertEqual(len(outbox), 0)
@ -239,7 +239,7 @@ class BudgetTestCase(TestCase):
# remove fred # remove fred
self.app.post("/raclette/members/%s/delete" % 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 # as fred is not bound to any bill, he is removed
self.assertEqual(len(models.Project.query.get("raclette").members), 1) 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 # 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").members), 2)
self.assertEqual( 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 # as fred is now deactivated, check that he is not listed when adding
# a bill or displaying the balance # a bill or displaying the balance
@ -276,7 +276,7 @@ class BudgetTestCase(TestCase):
# adding him again should reactivate him # adding him again should reactivate him
self.app.post("/raclette/members/add", data={'name': 'fred'}) self.app.post("/raclette/members/add", data={'name': 'fred'})
self.assertEqual( 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 # adding an user with the same name as another user from a different
# project should not cause any troubles # project should not cause any troubles
@ -284,7 +284,7 @@ class BudgetTestCase(TestCase):
self.login("randomid") self.login("randomid")
self.app.post("/randomid/members/add", data={'name': 'fred'}) self.app.post("/randomid/members/add", data={'name': 'fred'})
self.assertEqual( 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): def test_person_model(self):
self.post_project("raclette") self.post_project("raclette")
@ -321,11 +321,11 @@ class BudgetTestCase(TestCase):
response = self.app.get("/raclette/members/1/delete") response = self.app.get("/raclette/members/1/delete")
self.assertEqual(response.status_code, 405) self.assertEqual(response.status_code, 405)
#delete user using POST method # delete user using POST method
self.app.post("/raclette/members/1/delete") self.app.post("/raclette/members/1/delete")
self.assertEqual( self.assertEqual(
len(models.Project.query.get("raclette").active_members), 0) len(models.Project.query.get("raclette").active_members), 0)
#try to delete an user already deleted # try to delete an user already deleted
self.app.post("/raclette/members/1/delete") self.app.post("/raclette/members/1/delete")
def test_demo(self): def test_demo(self):
@ -358,7 +358,7 @@ class BudgetTestCase(TestCase):
# try to connect with wrong credentials should not work # try to connect with wrong credentials should not work
with run.app.test_client() as c: with run.app.test_client() as c:
resp = c.post("/authenticate", resp = c.post("/authenticate",
data={'id': 'raclette', 'password': 'nope'}) data={'id': 'raclette', 'password': 'nope'})
self.assertIn("Authentication", resp.data.decode('utf-8')) self.assertIn("Authentication", resp.data.decode('utf-8'))
self.assertNotIn('raclette', session) self.assertNotIn('raclette', session)
@ -366,7 +366,7 @@ class BudgetTestCase(TestCase):
# try to connect with the right credentials should work # try to connect with the right credentials should work
with run.app.test_client() as c: with run.app.test_client() as c:
resp = c.post("/authenticate", resp = c.post("/authenticate",
data={'id': 'raclette', 'password': 'raclette'}) data={'id': 'raclette', 'password': 'raclette'})
self.assertNotIn("Authentication", resp.data.decode('utf-8')) self.assertNotIn("Authentication", resp.data.decode('utf-8'))
self.assertIn('raclette', session) self.assertIn('raclette', session)
@ -461,7 +461,7 @@ class BudgetTestCase(TestCase):
balance = models.Project.query.get("raclette").balance balance = models.Project.query.get("raclette").balance
self.assertEqual(set(balance.values()), set([19.0, -19.0])) self.assertEqual(set(balance.values()), set([19.0, -19.0]))
#Bill with negative amount # Bill with negative amount
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-12', 'date': '2011-08-12',
'what': 'fromage à raclette', 'what': 'fromage à raclette',
@ -472,7 +472,7 @@ class BudgetTestCase(TestCase):
bill = models.Bill.query.filter(models.Bill.date == '2011-08-12')[0] bill = models.Bill.query.filter(models.Bill.date == '2011-08-12')[0]
self.assertEqual(bill.amount, -25) self.assertEqual(bill.amount, -25)
#add a bill with a comma # add a bill with a comma
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-01', 'date': '2011-08-01',
'what': 'fromage à raclette', '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': 'alexis'})
self.app.post("/raclette/members/add", data={'name': 'tata', 'weight': 1}) 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.assertIn('extra-info', resp.data.decode('utf-8'))
self.app.post("/raclette/members/add", data={'name': 'freddy familly', 'weight': 4}) 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')) self.assertNotIn('extra-info', resp.data.decode('utf-8'))
def test_rounding(self): def test_rounding(self):
self.post_project("raclette") 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[1].id] = 0.0
result[models.Project.query.get("raclette").members[2].id] = -8.12 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. # 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): for key, value in six.iteritems(balance):
self.assertEqual(round(value, 2), result[key]) self.assertEqual(round(value, 2), result[key])
@ -583,7 +583,7 @@ class BudgetTestCase(TestCase):
} }
resp = self.app.post("/raclette/edit", data=new_data, resp = self.app.post("/raclette/edit", data=new_data,
follow_redirects=True) follow_redirects=True)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
project = models.Project.query.get("raclette") project = models.Project.query.get("raclette")
@ -594,7 +594,7 @@ class BudgetTestCase(TestCase):
new_data['contact_email'] = 'wrong_email' new_data['contact_email'] = 'wrong_email'
resp = self.app.post("/raclette/edit", data=new_data, 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')) self.assertIn("Invalid email address", resp.data.decode('utf-8'))
def test_dashboard(self): 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': 'alexis'})
self.app.post("/raclette/members/add", data={'name': 'fred'}) self.app.post("/raclette/members/add", data={'name': 'fred'})
self.app.post("/raclette/members/add", data={'name': 'tata'}) 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'}) self.app.post("/raclette/members/add", data={'name': 'toto'})
# create bills # create bills
@ -640,13 +640,13 @@ class BudgetTestCase(TestCase):
'payed_for': [1, 2], 'payed_for': [1, 2],
'amount': '10', 'amount': '10',
}) })
project = models.Project.query.get('raclette') project = models.Project.query.get('raclette')
transactions = project.get_transactions_to_settle_bill() transactions = project.get_transactions_to_settle_bill()
members = defaultdict(int) 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: for t in transactions:
members[t['ower']]-=t['amount'] members[t['ower']] -= t['amount']
members[t['receiver']]+=t['amount'] members[t['receiver']] += t['amount']
balance = models.Project.query.get("raclette").balance balance = models.Project.query.get("raclette").balance
for m, a in members.items(): for m, a in members.items():
self.assertEqual(a, balance[m.id]) self.assertEqual(a, balance[m.id])
@ -684,7 +684,7 @@ class BudgetTestCase(TestCase):
'payed_for': [2], 'payed_for': [2],
'amount': '13.33', 'amount': '13.33',
}) })
project = models.Project.query.get('raclette') project = models.Project.query.get('raclette')
transactions = project.get_transactions_to_settle_bill() transactions = project.get_transactions_to_settle_bill()
members = defaultdict(int) members = defaultdict(int)
# There should not be any zero-amount transfer after rounding # There should not be any zero-amount transfer after rounding
@ -805,6 +805,7 @@ class BudgetTestCase(TestCase):
class APITestCase(TestCase): class APITestCase(TestCase):
"""Tests the API""" """Tests the API"""
def api_create(self, name, id=None, password=None, contact=None): def api_create(self, name, id=None, password=None, contact=None):
@ -833,7 +834,7 @@ class APITestCase(TestCase):
def assertStatus(self, expected, resp, url=""): def assertStatus(self, expected, resp, url=""):
return self.assertEqual(expected, resp.status_code, 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): def test_basic_auth(self):
# create a project # create a project
@ -850,15 +851,15 @@ class APITestCase(TestCase):
for resource in ("/raclette/members", "/raclette/bills"): for resource in ("/raclette/members", "/raclette/bills"):
url = "/api/projects" + resource url = "/api/projects" + resource
self.assertStatus(401, getattr(self.app, verb)(url), self.assertStatus(401, getattr(self.app, verb)(url),
verb + resource) verb + resource)
for verb in ('get', 'delete', 'put'): for verb in ('get', 'delete', 'put'):
for resource in ("/raclette", "/raclette/members/1", for resource in ("/raclette", "/raclette/members/1",
"/raclette/bills/1"): "/raclette/bills/1"):
url = "/api/projects" + resource url = "/api/projects" + resource
self.assertStatus(401, getattr(self.app, verb)(url), self.assertStatus(401, getattr(self.app, verb)(url),
verb + resource) verb + resource)
def test_project(self): def test_project(self):
# wrong email should return an error # wrong email should return an error
@ -885,7 +886,7 @@ class APITestCase(TestCase):
# get information about it # get information about it
resp = self.app.get("/api/projects/raclette", resp = self.app.get("/api/projects/raclette",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertTrue(200, resp.status_code) self.assertTrue(200, resp.status_code)
expected = { expected = {
@ -904,12 +905,12 @@ class APITestCase(TestCase):
"contact_email": "yeah@notmyidea.org", "contact_email": "yeah@notmyidea.org",
"password": "raclette", "password": "raclette",
"name": "The raclette party", "name": "The raclette party",
}, headers=self.get_auth("raclette")) }, headers=self.get_auth("raclette"))
self.assertEqual(200, resp.status_code) self.assertEqual(200, resp.status_code)
resp = self.app.get("/api/projects/raclette", resp = self.app.get("/api/projects/raclette",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertEqual(200, resp.status_code) self.assertEqual(200, resp.status_code)
expected = { expected = {
@ -925,13 +926,13 @@ class APITestCase(TestCase):
# delete should work # delete should work
resp = self.app.delete("/api/projects/raclette", resp = self.app.delete("/api/projects/raclette",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertEqual(200, resp.status_code) self.assertEqual(200, resp.status_code)
# get should return a 401 on an unknown resource # get should return a 401 on an unknown resource
resp = self.app.get("/api/projects/raclette", resp = self.app.get("/api/projects/raclette",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertEqual(401, resp.status_code) self.assertEqual(401, resp.status_code)
def test_member(self): def test_member(self):
@ -940,15 +941,15 @@ class APITestCase(TestCase):
# get the list of members (should be empty) # get the list of members (should be empty)
req = self.app.get("/api/projects/raclette/members", req = self.app.get("/api/projects/raclette/members",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual('[]', req.data.decode('utf-8')) self.assertEqual('[]', req.data.decode('utf-8'))
# add a member # add a member
req = self.app.post("/api/projects/raclette/members", data={ req = self.app.post("/api/projects/raclette/members", data={
"name": "Alexis" "name": "Alexis"
}, headers=self.get_auth("raclette")) }, headers=self.get_auth("raclette"))
# the id of the new member should be returned # the id of the new member should be returned
self.assertStatus(201, req) self.assertStatus(201, req)
@ -956,21 +957,21 @@ class APITestCase(TestCase):
# the list of members should contain one member # the list of members should contain one member
req = self.app.get("/api/projects/raclette/members", req = self.app.get("/api/projects/raclette/members",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) 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)
# edit this member # edit this member
req = self.app.put("/api/projects/raclette/members/1", data={ req = self.app.put("/api/projects/raclette/members/1", data={
"name": "Fred" "name": "Fred"
}, headers=self.get_auth("raclette")) }, headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
# get should return the new name # get should return the new name
req = self.app.get("/api/projects/raclette/members/1", req = self.app.get("/api/projects/raclette/members/1",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual("Fred", json.loads(req.data.decode('utf-8'))["name"]) self.assertEqual("Fred", json.loads(req.data.decode('utf-8'))["name"])
@ -978,14 +979,14 @@ class APITestCase(TestCase):
# delete a member # delete a member
req = self.app.delete("/api/projects/raclette/members/1", req = self.app.delete("/api/projects/raclette/members/1",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
# the list of members should be empty # the list of members should be empty
# get the list of members (should be empty) # get the list of members (should be empty)
req = self.app.get("/api/projects/raclette/members", req = self.app.get("/api/projects/raclette/members",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual('[]', req.data.decode('utf-8')) self.assertEqual('[]', req.data.decode('utf-8'))
@ -1001,7 +1002,7 @@ class APITestCase(TestCase):
# get the list of bills (should be empty) # get the list of bills (should be empty)
req = self.app.get("/api/projects/raclette/bills", req = self.app.get("/api/projects/raclette/bills",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual("[]", req.data.decode('utf-8')) self.assertEqual("[]", req.data.decode('utf-8'))
@ -1013,7 +1014,7 @@ class APITestCase(TestCase):
'payer': "1", 'payer': "1",
'payed_for': ["1", "2"], 'payed_for': ["1", "2"],
'amount': '25', 'amount': '25',
}, headers=self.get_auth("raclette")) }, headers=self.get_auth("raclette"))
# should return the id # should return the id
self.assertStatus(201, req) self.assertStatus(201, req)
@ -1021,7 +1022,7 @@ class APITestCase(TestCase):
# get this bill details # get this bill details
req = self.app.get("/api/projects/raclette/bills/1", req = self.app.get("/api/projects/raclette/bills/1",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
# compare with the added info # compare with the added info
self.assertStatus(200, req) self.assertStatus(200, req)
@ -1039,7 +1040,7 @@ class APITestCase(TestCase):
# the list of bills should lenght 1 # the list of bills should lenght 1
req = self.app.get("/api/projects/raclette/bills", req = self.app.get("/api/projects/raclette/bills",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) 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'))))
@ -1050,7 +1051,7 @@ class APITestCase(TestCase):
'payer': "1", 'payer': "1",
'payed_for': ["1", "2"], 'payed_for': ["1", "2"],
'amount': '25', 'amount': '25',
}, headers=self.get_auth("raclette")) }, headers=self.get_auth("raclette"))
self.assertStatus(400, req) self.assertStatus(400, req)
self.assertEqual('{"date": ["This field is required."]}', req.data.decode('utf-8')) self.assertEqual('{"date": ["This field is required."]}', req.data.decode('utf-8'))
@ -1062,11 +1063,11 @@ class APITestCase(TestCase):
'payer': "2", 'payer': "2",
'payed_for': ["1", "2"], 'payed_for': ["1", "2"],
'amount': '25', 'amount': '25',
}, headers=self.get_auth("raclette")) }, headers=self.get_auth("raclette"))
# check its fields # check its fields
req = self.app.get("/api/projects/raclette/bills/1", req = self.app.get("/api/projects/raclette/bills/1",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
expected = { expected = {
"what": "beer", "what": "beer",
@ -1082,17 +1083,17 @@ class APITestCase(TestCase):
# delete a bill # delete a bill
req = self.app.delete("/api/projects/raclette/bills/1", req = self.app.delete("/api/projects/raclette/bills/1",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
# getting it should return a 404 # getting it should return a 404
req = self.app.get("/api/projects/raclette/bills/1", req = self.app.get("/api/projects/raclette/bills/1",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(404, req) self.assertStatus(404, req)
def test_username_xss(self): def test_username_xss(self):
# create a project # create a project
#self.api_create("raclette") # self.api_create("raclette")
self.post_project("raclette") self.post_project("raclette")
self.login("raclette") self.login("raclette")
@ -1118,11 +1119,11 @@ class APITestCase(TestCase):
'payer': "1", 'payer': "1",
'payed_for': ["1", "2"], 'payed_for': ["1", "2"],
'amount': '25', 'amount': '25',
}, headers=self.get_auth("raclette")) }, headers=self.get_auth("raclette"))
# get this bill details # get this bill details
req = self.app.get("/api/projects/raclette/bills/1", req = self.app.get("/api/projects/raclette/bills/1",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
# compare with the added info # compare with the added info
self.assertStatus(200, req) self.assertStatus(200, req)
@ -1139,7 +1140,7 @@ class APITestCase(TestCase):
# getting it should return a 404 # getting it should return a 404
req = self.app.get("/api/projects/raclette", req = self.app.get("/api/projects/raclette",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
expected = { expected = {
"active_members": [ "active_members": [
@ -1162,7 +1163,9 @@ class APITestCase(TestCase):
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual(expected, json.loads(req.data.decode('utf-8'))) self.assertEqual(expected, json.loads(req.data.decode('utf-8')))
class ServerTestCase(APITestCase): class ServerTestCase(APITestCase):
def setUp(self): def setUp(self):
run.configure() run.configure()
super(ServerTestCase, self).setUp() super(ServerTestCase, self).setUp()

View file

@ -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,9 +116,10 @@ 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(
if isinstance(dic[h], unicode) else str(dic[h]).encode('utf8') [dic[h].encode('utf8')
for h in dict_to_convert[0].keys()]) if isinstance(dic[h], unicode) else str(dic[h]).encode('utf8')
for h in dict_to_convert[0].keys()])
except (KeyError, IndexError): except (KeyError, IndexError):
csv_data = [] csv_data = []
writer = csv.writer(csv_file) writer = csv.writer(csv_file)
@ -123,4 +130,4 @@ def list_of_dicts2csv(dict_to_convert):
return csv_file return csv_file
# base64 encoding that works with both py2 and py3 and yield no warning # 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

View file

@ -15,8 +15,7 @@ from flask import (
) )
from flask_mail import Mail, Message from flask_mail import Mail, Message
from flask_babel import get_locale, gettext as _ from flask_babel import get_locale, gettext as _
from werkzeug.security import generate_password_hash, \ from werkzeug.security import generate_password_hash, check_password_hash
check_password_hash
from smtplib import SMTPRecipientsRefused from smtplib import SMTPRecipientsRefused
import werkzeug import werkzeug
from sqlalchemy import orm from sqlalchemy import orm
@ -75,14 +74,14 @@ def pull_project(endpoint, values):
project = Project.query.get(project_id) project = Project.query.get(project_id)
if not project: if not project:
raise Redirect303(url_for(".create_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: if project.id in session and session[project.id] == project.password:
# add project into kwargs and call the original function # add project into kwargs and call the original function
g.project = project g.project = project
else: else:
# redirect to authentication page # redirect to authentication page
raise Redirect303( raise Redirect303(
url_for(".authenticate", project_id=project_id)) url_for(".authenticate", project_id=project_id))
@main.route("/admin", methods=["GET", "POST"]) @main.route("/admin", methods=["GET", "POST"])
@ -110,7 +109,7 @@ def authenticate(project_id=None):
form.id.data = request.args['project_id'] form.id.data = request.args['project_id']
project_id = form.id.data project_id = form.id.data
if project_id is None: 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") msg = _("You need to enter a project identifier")
form.errors["id"] = [msg] form.errors["id"] = [msg]
return render_template("authenticate.html", form=form) return render_template("authenticate.html", form=form)
@ -150,7 +149,7 @@ def authenticate(project_id=None):
return redirect(url_for(".list_bills")) return redirect(url_for(".list_bills"))
return render_template("authenticate.html", form=form, return render_template("authenticate.html", form=form,
create_project=create_project) create_project=create_project)
@main.route("/") @main.route("/")
@ -196,14 +195,14 @@ def create_project():
g.project = project g.project = project
message_title = _("You have just created '%(project)s' " 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" % message_body = render_template("reminder_mail.%s" %
get_locale().language) get_locale().language)
msg = Message(message_title, msg = Message(message_title,
body=message_body, body=message_body,
recipients=[project.contact_email]) recipients=[project.contact_email])
try: try:
mail.send(msg) mail.send(msg)
except SMTPRecipientsRefused: except SMTPRecipientsRefused:
@ -214,7 +213,7 @@ def create_project():
# redirect the user to the next step (invite) # redirect the user to the next step (invite)
flash(_("%(msg_compl)sThe project identifier is %(project)s", 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 redirect(url_for(".invite", project_id=project.id))
return render_template("create_project.html", form=form) return render_template("create_project.html", form=form)
@ -231,8 +230,8 @@ def remind_password():
# send the password reminder # send the password reminder
password_reminder = "password_reminder.%s" % get_locale().language password_reminder = "password_reminder.%s" % get_locale().language
mail.send(Message("password recovery", mail.send(Message("password recovery",
body=render_template(password_reminder, project=project), body=render_template(password_reminder, project=project),
recipients=[project.contact_email])) recipients=[project.contact_email]))
flash(_("a mail has been sent to you with the password")) flash(_("a mail has been sent to you with the password"))
return render_template("password_reminder.html", form=form) return render_template("password_reminder.html", form=form)
@ -270,7 +269,7 @@ def edit_project():
attachment_filename="%s-%s.%s" % attachment_filename="%s-%s.%s" %
(g.project.id, export_type, export_format), (g.project.id, export_type, export_format),
as_attachment=True as_attachment=True
) )
else: else:
edit_form.name.data = g.project.name edit_form.name.data = g.project.name
edit_form.password.data = g.project.password edit_form.password.data = g.project.password
@ -311,7 +310,7 @@ def demo():
project_id='demo')) project_id='demo'))
if not project and is_demo_project_activated: if not project and is_demo_project_activated:
project = Project(id="demo", name=u"demonstration", password="demo", 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.add(project)
db.session.commit() db.session.commit()
session[project.id] = project.password session[project.id] = project.password
@ -329,14 +328,14 @@ def invite():
# send the email # send the email
message_body = render_template("invitation_mail.%s" % message_body = render_template("invitation_mail.%s" %
get_locale().language) get_locale().language)
message_title = _("You have been invited to share your " 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, msg = Message(message_title,
body=message_body, body=message_body,
recipients=[email.strip() recipients=[email.strip()
for email in form.emails.data.split(",")]) for email in form.emails.data.split(",")])
mail.send(msg) mail.send(msg)
flash(_("Your invitations have been sent")) flash(_("Your invitations have been sent"))
return redirect(url_for(".list_bills")) return redirect(url_for(".list_bills"))
@ -354,11 +353,11 @@ def list_bills():
bills = g.project.get_bills().options(orm.subqueryload(Bill.owers)) bills = g.project.get_bills().options(orm.subqueryload(Bill.owers))
return render_template("list_bills.html", return render_template("list_bills.html",
bills=bills, member_form=MemberForm(g.project), bills=bills, member_form=MemberForm(g.project),
bill_form=bill_form, bill_form=bill_form,
add_bill=request.values.get('add_bill', False), add_bill=request.values.get('add_bill', False),
current_view="list_bills", current_view="list_bills",
) )
@main.route("/<project_id>/members/add", methods=["GET", "POST"]) @main.route("/<project_id>/members/add", methods=["GET", "POST"])
@ -378,7 +377,7 @@ def add_member():
@main.route("/<project_id>/members/<member_id>/reactivate", methods=["POST"]) @main.route("/<project_id>/members/<member_id>/reactivate", methods=["POST"])
def reactivate(member_id): def reactivate(member_id):
person = Person.query.filter(Person.id == 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: if person:
person[0].activated = True person[0].activated = True
db.session.commit() db.session.commit()

View file

@ -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
--------------------------- ---------------------------