mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-04-28 17:32:38 +02:00
FIXUP: some more work
This commit is contained in:
parent
14cc9b96d3
commit
f968c9870c
5 changed files with 156 additions and 42 deletions
|
@ -135,7 +135,8 @@ class EditProjectForm(FlaskForm):
|
||||||
_("New private code"),
|
_("New private code"),
|
||||||
description=_("Enter a new code if you want to change it"),
|
description=_("Enter a new code if you want to change it"),
|
||||||
)
|
)
|
||||||
contact_email = StringField(_("Email"), validators=[DataRequired(), Email()])
|
contact_email = StringField(_("Email"), validators=[
|
||||||
|
DataRequired(), Email()])
|
||||||
project_history = BooleanField(_("Enable project history"))
|
project_history = BooleanField(_("Enable project history"))
|
||||||
ip_recording = BooleanField(_("Use IP tracking for project history"))
|
ip_recording = BooleanField(_("Use IP tracking for project history"))
|
||||||
currency_helper = CurrencyConverter()
|
currency_helper = CurrencyConverter()
|
||||||
|
@ -228,7 +229,8 @@ class ImportProjectForm(FlaskForm):
|
||||||
"File",
|
"File",
|
||||||
validators=[
|
validators=[
|
||||||
FileRequired(),
|
FileRequired(),
|
||||||
FileAllowed(["json", "JSON", "csv", "CSV"], "Incorrect file format"),
|
FileAllowed(["json", "JSON", "csv", "CSV"],
|
||||||
|
"Incorrect file format"),
|
||||||
],
|
],
|
||||||
description=_("Compatible with Cospend"),
|
description=_("Compatible with Cospend"),
|
||||||
)
|
)
|
||||||
|
@ -349,9 +351,11 @@ class ResetPasswordForm(FlaskForm):
|
||||||
|
|
||||||
|
|
||||||
class BillForm(FlaskForm):
|
class BillForm(FlaskForm):
|
||||||
date = DateField(_("When?"), validators=[DataRequired()], default=datetime.now)
|
date = DateField(_("When?"), validators=[
|
||||||
|
DataRequired()], default=datetime.now)
|
||||||
what = StringField(_("What?"), validators=[DataRequired()])
|
what = StringField(_("What?"), validators=[DataRequired()])
|
||||||
payer = SelectField(_("Who paid?"), validators=[DataRequired()], coerce=int)
|
payer = SelectField(_("Who paid?"), validators=[
|
||||||
|
DataRequired()], coerce=int)
|
||||||
amount = CalculatorStringField(_("How much?"), validators=[DataRequired()])
|
amount = CalculatorStringField(_("How much?"), validators=[DataRequired()])
|
||||||
currency_helper = CurrencyConverter()
|
currency_helper = CurrencyConverter()
|
||||||
original_currency = SelectField(_("Currency"), validators=[DataRequired()])
|
original_currency = SelectField(_("Currency"), validators=[DataRequired()])
|
||||||
|
@ -373,8 +377,30 @@ class BillForm(FlaskForm):
|
||||||
submit = SubmitField(_("Submit"))
|
submit = SubmitField(_("Submit"))
|
||||||
submit2 = SubmitField(_("Submit and add a new one"))
|
submit2 = SubmitField(_("Submit and add a new one"))
|
||||||
|
|
||||||
|
def parse_hashtags(self, project, what):
|
||||||
|
"""Handles the hashtags which can be optionally specified in the 'what'
|
||||||
|
field, using the `grocery #hash #otherhash` syntax.
|
||||||
|
|
||||||
|
Returns: the new "what" field (with hashtags stripped-out) and the list
|
||||||
|
of tags.
|
||||||
|
"""
|
||||||
|
|
||||||
|
hashtags = findall(r"#(\w+)", what)
|
||||||
|
|
||||||
|
if not hashtags:
|
||||||
|
return what, []
|
||||||
|
|
||||||
|
for tag in hashtags:
|
||||||
|
what = what.replace(f"#{tag}", "")
|
||||||
|
|
||||||
|
return what, hashtags
|
||||||
|
|
||||||
def export(self, project):
|
def export(self, project):
|
||||||
return Bill(
|
"""This is triggered on bill creation.
|
||||||
|
"""
|
||||||
|
what, hashtags = self.parse_hashtags(project, self.what.data)
|
||||||
|
|
||||||
|
bill = Bill(
|
||||||
amount=float(self.amount.data),
|
amount=float(self.amount.data),
|
||||||
date=self.date.data,
|
date=self.date.data,
|
||||||
external_link=self.external_link.data,
|
external_link=self.external_link.data,
|
||||||
|
@ -382,20 +408,17 @@ class BillForm(FlaskForm):
|
||||||
owers=Person.query.get_by_ids(self.payed_for.data, project),
|
owers=Person.query.get_by_ids(self.payed_for.data, project),
|
||||||
payer_id=self.payer.data,
|
payer_id=self.payer.data,
|
||||||
project_default_currency=project.default_currency,
|
project_default_currency=project.default_currency,
|
||||||
what=self.what.data,
|
what=what,
|
||||||
bill_type=self.bill_type.data,
|
bill_type=self.bill_type.data,
|
||||||
)
|
)
|
||||||
|
bill.set_tags(hashtags, project)
|
||||||
|
return bill
|
||||||
|
|
||||||
def save(self, bill, project):
|
def save(self, bill, project):
|
||||||
|
what, hashtags = self.parse_hashtags(project, self.what.data)
|
||||||
bill.payer_id = self.payer.data
|
bill.payer_id = self.payer.data
|
||||||
bill.amount = self.amount.data
|
bill.amount = self.amount.data
|
||||||
# Get the list of tags from the 'what' field
|
bill.what = what
|
||||||
hashtags = findall(r"#(\w+)", self.what.data)
|
|
||||||
if hashtags:
|
|
||||||
bill.tags = [Tag(name=tag) for tag in hashtags]
|
|
||||||
for tag in hashtags:
|
|
||||||
self.what.data = self.what.data.replace(f"#{tag}", "")
|
|
||||||
bill.what = self.what.data
|
|
||||||
bill.bill_type = BillType(self.bill_type.data)
|
bill.bill_type = BillType(self.bill_type.data)
|
||||||
bill.external_link = self.external_link.data
|
bill.external_link = self.external_link.data
|
||||||
bill.date = self.date.data
|
bill.date = self.date.data
|
||||||
|
@ -404,19 +427,22 @@ class BillForm(FlaskForm):
|
||||||
bill.converted_amount = self.currency_helper.exchange_currency(
|
bill.converted_amount = self.currency_helper.exchange_currency(
|
||||||
bill.amount, bill.original_currency, project.default_currency
|
bill.amount, bill.original_currency, project.default_currency
|
||||||
)
|
)
|
||||||
|
bill.set_tags(hashtags, project)
|
||||||
return bill
|
return bill
|
||||||
|
|
||||||
def fill(self, bill, project):
|
def fill(self, bill, project):
|
||||||
self.payer.data = bill.payer_id
|
self.payer.data = bill.payer_id
|
||||||
self.amount.data = bill.amount
|
self.amount.data = bill.amount
|
||||||
self.what.data = bill.what
|
hashtags = ' '.join([f'#{tag.name}' for tag in bill.tags])
|
||||||
|
self.what.data = bill.what.strip() + f' {hashtags}'
|
||||||
self.bill_type.data = bill.bill_type
|
self.bill_type.data = bill.bill_type
|
||||||
self.external_link.data = bill.external_link
|
self.external_link.data = bill.external_link
|
||||||
self.original_currency.data = bill.original_currency
|
self.original_currency.data = bill.original_currency
|
||||||
self.date.data = bill.date
|
self.date.data = bill.date
|
||||||
self.payed_for.data = [int(ower.id) for ower in bill.owers]
|
self.payed_for.data = [int(ower.id) for ower in bill.owers]
|
||||||
|
|
||||||
self.original_currency.label = Label("original_currency", _("Currency"))
|
self.original_currency.label = Label(
|
||||||
|
"original_currency", _("Currency"))
|
||||||
self.original_currency.description = _(
|
self.original_currency.description = _(
|
||||||
"Project default: %(currency)s",
|
"Project default: %(currency)s",
|
||||||
currency=render_localized_currency(
|
currency=render_localized_currency(
|
||||||
|
@ -445,10 +471,13 @@ class BillForm(FlaskForm):
|
||||||
|
|
||||||
|
|
||||||
class MemberForm(FlaskForm):
|
class MemberForm(FlaskForm):
|
||||||
name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter])
|
name = StringField(_("Name"), validators=[
|
||||||
|
DataRequired()], filters=[strip_filter])
|
||||||
|
|
||||||
weight_validators = [NumberRange(min=0.1, message=_("Weights should be positive"))]
|
weight_validators = [NumberRange(
|
||||||
weight = CommaDecimalField(_("Weight"), default=1, validators=weight_validators)
|
min=0.1, message=_("Weights should be positive"))]
|
||||||
|
weight = CommaDecimalField(
|
||||||
|
_("Weight"), default=1, validators=weight_validators)
|
||||||
submit = SubmitField(_("Add"))
|
submit = SubmitField(_("Add"))
|
||||||
|
|
||||||
def __init__(self, project, edit=False, *args, **kwargs):
|
def __init__(self, project, edit=False, *args, **kwargs):
|
||||||
|
@ -467,7 +496,8 @@ class MemberForm(FlaskForm):
|
||||||
Person.activated,
|
Person.activated,
|
||||||
).all()
|
).all()
|
||||||
): # NOQA
|
): # NOQA
|
||||||
raise ValidationError(_("This project already have this participant"))
|
raise ValidationError(
|
||||||
|
_("This project already have this participant"))
|
||||||
|
|
||||||
def save(self, project, person):
|
def save(self, project, person):
|
||||||
# if the user is already bound to the project, just reactivate him
|
# if the user is already bound to the project, just reactivate him
|
||||||
|
|
|
@ -125,7 +125,8 @@ class Project(db.Model):
|
||||||
|
|
||||||
balance spent paid
|
balance spent paid
|
||||||
"""
|
"""
|
||||||
balances, should_pay, should_receive = (defaultdict(int) for time in (1, 2, 3))
|
balances, should_pay, should_receive = (
|
||||||
|
defaultdict(int) for time in (1, 2, 3))
|
||||||
for bill in self.get_bills_unordered().all():
|
for bill in self.get_bills_unordered().all():
|
||||||
total_weight = sum(ower.weight for ower in bill.owers)
|
total_weight = sum(ower.weight for ower in bill.owers)
|
||||||
|
|
||||||
|
@ -181,11 +182,28 @@ class Project(db.Model):
|
||||||
:rtype dict:
|
:rtype dict:
|
||||||
"""
|
"""
|
||||||
monthly = defaultdict(lambda: defaultdict(float))
|
monthly = defaultdict(lambda: defaultdict(float))
|
||||||
|
|
||||||
for bill in self.get_bills_unordered().all():
|
for bill in self.get_bills_unordered().all():
|
||||||
if bill.bill_type == BillType.EXPENSE:
|
if bill.bill_type == BillType.EXPENSE:
|
||||||
monthly[bill.date.year][bill.date.month] += bill.converted_amount
|
monthly[bill.date.year][bill.date.month] += bill.converted_amount
|
||||||
return monthly
|
return monthly
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tags_monthly_stats(self):
|
||||||
|
"""
|
||||||
|
|
||||||
|
:return: a dict of years mapping to a dict of months mapping to the amount
|
||||||
|
:rtype dict:
|
||||||
|
"""
|
||||||
|
tags_monthly = defaultdict(
|
||||||
|
lambda: defaultdict(lambda: defaultdict(float)))
|
||||||
|
|
||||||
|
for bill in self.get_bills_unordered().all():
|
||||||
|
if bill.bill_type == BillType.EXPENSE:
|
||||||
|
for tag in bill.tags:
|
||||||
|
tags_monthly[bill.date.year][bill.date.month][tag.name] += bill.converted_amount
|
||||||
|
return tags_monthly
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uses_weights(self):
|
def uses_weights(self):
|
||||||
return len([i for i in self.members if i.weight != 1]) > 0
|
return len([i for i in self.members if i.weight != 1]) > 0
|
||||||
|
@ -322,7 +340,8 @@ class Project(db.Model):
|
||||||
year=newest_date.year, month=newest_date.month, day=1
|
year=newest_date.year, month=newest_date.month, day=1
|
||||||
)
|
)
|
||||||
# Infinite iterator towards the past
|
# Infinite iterator towards the past
|
||||||
all_months = (newest_month - relativedelta(months=i) for i in itertools.count())
|
all_months = (newest_month - relativedelta(months=i)
|
||||||
|
for i in itertools.count())
|
||||||
# Stop when reaching one month before the first date
|
# Stop when reaching one month before the first date
|
||||||
months = itertools.takewhile(
|
months = itertools.takewhile(
|
||||||
lambda x: x > oldest_date - relativedelta(months=1), all_months
|
lambda x: x > oldest_date - relativedelta(months=1), all_months
|
||||||
|
@ -497,7 +516,8 @@ class Project(db.Model):
|
||||||
)
|
)
|
||||||
loads_kwargs["max_age"] = max_age
|
loads_kwargs["max_age"] = max_age
|
||||||
else:
|
else:
|
||||||
project = Project.query.get(project_id) if project_id is not None else None
|
project = Project.query.get(
|
||||||
|
project_id) if project_id is not None else None
|
||||||
password = project.password if project is not None else ""
|
password = project.password if project is not None else ""
|
||||||
serializer = URLSafeSerializer(
|
serializer = URLSafeSerializer(
|
||||||
current_app.config["SECRET_KEY"] + password, salt=token_type
|
current_app.config["SECRET_KEY"] + password, salt=token_type
|
||||||
|
@ -643,13 +663,28 @@ class Person(db.Model):
|
||||||
# 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 = db.Table(
|
||||||
"billowers",
|
"billowers",
|
||||||
db.Column("bill_id", db.Integer, db.ForeignKey("bill.id"), primary_key=True),
|
db.Column("bill_id", db.Integer, db.ForeignKey(
|
||||||
db.Column("person_id", db.Integer, db.ForeignKey("person.id"), primary_key=True),
|
"bill.id"), primary_key=True),
|
||||||
|
db.Column("person_id", db.Integer, db.ForeignKey(
|
||||||
|
"person.id"), primary_key=True),
|
||||||
sqlite_autoincrement=True,
|
sqlite_autoincrement=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Tag(db.Model):
|
class Tag(db.Model):
|
||||||
|
class TagQuery(BaseQuery):
|
||||||
|
def get_or_create(self, name, project):
|
||||||
|
exists = (
|
||||||
|
Tag.query.filter(Tag.name == name)
|
||||||
|
.filter(Tag.project_id == project.id)
|
||||||
|
.one_or_none()
|
||||||
|
)
|
||||||
|
if exists:
|
||||||
|
return exists
|
||||||
|
return Tag(name=name, project_id=project.id)
|
||||||
|
|
||||||
|
query_class = TagQuery
|
||||||
|
|
||||||
__versionned__ = {}
|
__versionned__ = {}
|
||||||
|
|
||||||
__table_args__ = {"sqlite_autoincrement": True}
|
__table_args__ = {"sqlite_autoincrement": True}
|
||||||
|
@ -662,11 +697,15 @@ class Tag(db.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
# We need to manually define a join table for m2m relations
|
# We need to manually define a join table for m2m relations
|
||||||
billtags = db.Table(
|
billtags = db.Table(
|
||||||
"billtags",
|
"billtags",
|
||||||
db.Column("bill_id", db.Integer, db.ForeignKey("bill.id"), primary_key=True),
|
db.Column("bill_id", db.Integer, db.ForeignKey(
|
||||||
|
"bill.id"), primary_key=True),
|
||||||
db.Column("tag_id", db.Integer, db.ForeignKey("tag.id"), primary_key=True),
|
db.Column("tag_id", db.Integer, db.ForeignKey("tag.id"), primary_key=True),
|
||||||
sqlite_autoincrement=True,
|
sqlite_autoincrement=True,
|
||||||
)
|
)
|
||||||
|
@ -776,15 +815,24 @@ class Bill(db.Model):
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.what
|
|
||||||
|
|
||||||
def pay_each(self):
|
def pay_each(self):
|
||||||
"""Warning: this is slow, if you need to compute this for many bills, do
|
"""Warning: this is slow, if you need to compute this for many bills, do
|
||||||
it differently (see balance_full function)
|
it differently (see balance_full function)
|
||||||
"""
|
"""
|
||||||
return self.pay_each_default(self.converted_amount)
|
return self.pay_each_default(self.converted_amount)
|
||||||
|
|
||||||
|
def set_tags(self, tags, project):
|
||||||
|
object_tags = []
|
||||||
|
for tag_name in tags:
|
||||||
|
tag = Tag.query.get_or_create(name=tag_name, project=project)
|
||||||
|
db.session.add(tag)
|
||||||
|
object_tags.append(tag)
|
||||||
|
self.tags = object_tags
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.what
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
f"<Bill of {self.amount} from {self.payer} for "
|
f"<Bill of {self.amount} from {self.payer} for "
|
||||||
|
|
|
@ -116,12 +116,18 @@
|
||||||
{% if bills.total > 0 %}
|
{% if bills.total > 0 %}
|
||||||
<table id="bill_table" class="col table table-striped table-hover table-responsive-sm">
|
<table id="bill_table" class="col table table-striped table-hover table-responsive-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>{{ _("When?") }}
|
<tr><th>{{ _("When?") }}</th>
|
||||||
</th><th>{{ _("Who paid?") }}
|
<th>{{ _("Who paid?") }}</th>
|
||||||
</th><th>{{ _("For what?") }}
|
<th>{{ _("For what?") }}</th>
|
||||||
</th><th>{{ _("For whom?") }}
|
<th>{{ _("For whom?") }}</th>
|
||||||
</th><th>{{ _("How much?") }}
|
<th>{{ _("How much?") }}</th>
|
||||||
</th><th>{{ _("Actions") }}</th></tr>
|
<th data-toggle="tooltip"
|
||||||
|
data-placement="top"
|
||||||
|
title="{{ _('You can add tags to your bills by appending a #hashtag') }}">
|
||||||
|
{{ _("Tags") }}
|
||||||
|
</th>
|
||||||
|
<th>{{ _("Actions") }}</th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for (weights, bill) in bills.items %}
|
{% for (weights, bill) in bills.items %}
|
||||||
|
@ -147,6 +153,11 @@
|
||||||
{{ weighted_bill_amount(bill, weights) }}
|
{{ weighted_bill_amount(bill, weights) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for tag in bill.tags %}
|
||||||
|
#{{ tag.name }}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
<td class="bill-actions d-flex align-items-center">
|
<td class="bill-actions d-flex align-items-center">
|
||||||
<a class="edit" href="{{ url_for(".edit_bill", bill_id=bill.id) }}" title="{{ _("edit") }}">{{ _('edit') }}</a>
|
<a class="edit" href="{{ url_for(".edit_bill", bill_id=bill.id) }}" title="{{ _("edit") }}">{{ _('edit') }}</a>
|
||||||
<form class="delete-bill" action="{{ url_for(".delete_bill", bill_id=bill.id) }}" method="POST">
|
<form class="delete-bill" action="{{ url_for(".delete_bill", bill_id=bill.id) }}" method="POST">
|
||||||
|
|
|
@ -23,12 +23,28 @@
|
||||||
</table>
|
</table>
|
||||||
<h2>{{ _("Expenses by Month") }}</h2>
|
<h2>{{ _("Expenses by Month") }}</h2>
|
||||||
<table id="monthly_stats" class="table table-striped">
|
<table id="monthly_stats" class="table table-striped">
|
||||||
<thead><tr><th>{{ _("Period") }}</th><th>{{ _("Spent") }}</th></tr></thead>
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ _("Period") }}</th>
|
||||||
|
<th>{{ _("Spent") }}</th>
|
||||||
|
{% for tag in tags %}
|
||||||
|
<th>#{{ tag.name }}</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for month in months %}
|
{% for month in months %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ month|dateformat("MMMM yyyy") }}</td>
|
<td>{{ month|dateformat("MMMM yyyy") }}</td>
|
||||||
<td>{{ monthly_stats[month.year][month.month]|currency }}</td>
|
<td>{{ monthly_stats[month.year][month.month]|currency }}</td>
|
||||||
|
{% for tag in tags %}
|
||||||
|
{% if tag.name in tags_monthly_stats[month.year][month.month] %}
|
||||||
|
<td>{{ tags_monthly_stats[month.year][month.month][tag.name]|currency }}</td>
|
||||||
|
{% else %}
|
||||||
|
<td> - </td>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -58,7 +58,7 @@ from ihatemoney.forms import (
|
||||||
get_billform_for,
|
get_billform_for,
|
||||||
)
|
)
|
||||||
from ihatemoney.history import get_history, get_history_queries, purge_history
|
from ihatemoney.history import get_history, get_history_queries, purge_history
|
||||||
from ihatemoney.models import Bill, BillType, LoggingMode, Person, Project, db
|
from ihatemoney.models import Bill, BillType, LoggingMode, Person, Project, Tag, db
|
||||||
from ihatemoney.utils import (
|
from ihatemoney.utils import (
|
||||||
Redirect303,
|
Redirect303,
|
||||||
csv2list_of_dicts,
|
csv2list_of_dicts,
|
||||||
|
@ -154,7 +154,8 @@ def pull_project(endpoint, values):
|
||||||
project_id = entered_project_id.lower()
|
project_id = entered_project_id.lower()
|
||||||
project = Project.query.get(project_id)
|
project = Project.query.get(project_id)
|
||||||
if not project:
|
if not project:
|
||||||
raise Redirect303(url_for(".create_project", project_id=project_id))
|
raise Redirect303(
|
||||||
|
url_for(".create_project", project_id=project_id))
|
||||||
|
|
||||||
is_admin = session.get("is_admin")
|
is_admin = session.get("is_admin")
|
||||||
is_invitation = endpoint == "main.join_project"
|
is_invitation = endpoint == "main.join_project"
|
||||||
|
@ -368,7 +369,8 @@ def remind_password():
|
||||||
# send a link to reset the password
|
# send a link to reset the password
|
||||||
remind_message = Message(
|
remind_message = Message(
|
||||||
"password recovery",
|
"password recovery",
|
||||||
body=render_localized_template("password_reminder", project=project),
|
body=render_localized_template(
|
||||||
|
"password_reminder", project=project),
|
||||||
recipients=[project.contact_email],
|
recipients=[project.contact_email],
|
||||||
)
|
)
|
||||||
success = send_email(remind_message)
|
success = send_email(remind_message)
|
||||||
|
@ -611,7 +613,8 @@ def invite():
|
||||||
msg = Message(
|
msg = Message(
|
||||||
message_title,
|
message_title,
|
||||||
body=message_body,
|
body=message_body,
|
||||||
recipients=[email.strip() for email in form.emails.data.split(",")],
|
recipients=[email.strip()
|
||||||
|
for email in form.emails.data.split(",")],
|
||||||
)
|
)
|
||||||
success = send_email(msg)
|
success = send_email(msg)
|
||||||
if success:
|
if success:
|
||||||
|
@ -632,7 +635,8 @@ def invite():
|
||||||
token=g.project.generate_token(),
|
token=g.project.generate_token(),
|
||||||
_external=True,
|
_external=True,
|
||||||
)
|
)
|
||||||
invite_link = urlunparse(urlparse(invite_link)._replace(scheme="ihatemoney"))
|
invite_link = urlunparse(
|
||||||
|
urlparse(invite_link)._replace(scheme="ihatemoney"))
|
||||||
qr = qrcode.QRCode(image_factory=qrcode.image.svg.SvgPathImage)
|
qr = qrcode.QRCode(image_factory=qrcode.image.svg.SvgPathImage)
|
||||||
qr.add_data(invite_link)
|
qr.add_data(invite_link)
|
||||||
qr.make(fit=True)
|
qr.make(fit=True)
|
||||||
|
@ -914,7 +918,8 @@ def strip_ip_addresses():
|
||||||
form = DestructiveActionProjectForm(id=g.project.id)
|
form = DestructiveActionProjectForm(id=g.project.id)
|
||||||
if not form.validate():
|
if not form.validate():
|
||||||
flash(
|
flash(
|
||||||
format_form_errors(form, _("Error deleting recorded IP addresses")),
|
format_form_errors(
|
||||||
|
form, _("Error deleting recorded IP addresses")),
|
||||||
category="danger",
|
category="danger",
|
||||||
)
|
)
|
||||||
return redirect(url_for(".history"))
|
return redirect(url_for(".history"))
|
||||||
|
@ -933,18 +938,22 @@ def statistics():
|
||||||
"""Compute what each participant has paid and spent and display it"""
|
"""Compute what each participant has paid and spent and display it"""
|
||||||
# Determine range of months between which there are bills
|
# Determine range of months between which there are bills
|
||||||
months = g.project.active_months_range()
|
months = g.project.active_months_range()
|
||||||
|
tags = Tag.query.filter(Tag.project_id == g.project.id)
|
||||||
return render_template(
|
return render_template(
|
||||||
"statistics.html",
|
"statistics.html",
|
||||||
members_stats=g.project.members_stats,
|
members_stats=g.project.members_stats,
|
||||||
monthly_stats=g.project.monthly_stats,
|
monthly_stats=g.project.monthly_stats,
|
||||||
|
tags_monthly_stats=g.project.tags_monthly_stats,
|
||||||
months=months,
|
months=months,
|
||||||
|
tags=tags,
|
||||||
current_view="statistics",
|
current_view="statistics",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_etag(project_id, last_modified):
|
def build_etag(project_id, last_modified):
|
||||||
return hashlib.md5(
|
return hashlib.md5(
|
||||||
(current_app.config["SECRET_KEY"] + project_id + last_modified).encode()
|
(current_app.config["SECRET_KEY"] +
|
||||||
|
project_id + last_modified).encode()
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue