Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change settle endpoint to use POST instead of GET #1303

Merged
merged 6 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions ihatemoney/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
BooleanField,
DateField,
DecimalField,
HiddenField,
IntegerField,
Label,
PasswordField,
SelectField,
Expand Down Expand Up @@ -437,6 +439,22 @@ def validate_original_currency(self, field):
raise ValidationError(msg)


class HiddenCommaDecimalField(HiddenField, CommaDecimalField):
pass


class HiddenIntegerField(HiddenField, IntegerField):
pass


class SettlementForm(FlaskForm):
"""Used internally for validation, not directly visible to users"""

amount = HiddenCommaDecimalField("Amount", validators=[DataRequired()])
sender_id = HiddenIntegerField("Sender", validators=[DataRequired()])
receiver_id = HiddenIntegerField("Receiver", validators=[DataRequired()])


class MemberForm(FlaskForm):
name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter])

Expand Down
4 changes: 4 additions & 0 deletions ihatemoney/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,10 @@ def remove_member(self, member_id):
db.session.commit()
return person

def has_member(self, member_id):
person = Person.query.get(member_id, self)
return person is not None

def remove_project(self):
# We can't import at top level without circular dependencies
from ihatemoney.history import purge_history
Expand Down
74 changes: 45 additions & 29 deletions ihatemoney/templates/settle_bills.html
Original file line number Diff line number Diff line change
@@ -1,33 +1,49 @@
{% extends "sidebar_table_layout.html" %}

{% block sidebar %}
<div id="table_overflow">
{{ balance_table(show_weight=False) }}
</div>
{% endblock %}


{% block content %}
<table id="bill_table" class="split_bills table table-striped">
<thead><tr><th>{{ _("Who pays?") }}</th><th>{{ _("To whom?") }}</th><th>{{ _("How much?") }}</th><th>{{ _("Settled?") }}</th></tr></thead>
<tbody>
{% for bill in bills %}
<tr receiver={{bill.receiver.id}}>
<td>{{ bill.ower }}</td>
<td>{{ bill.receiver }}</td>
<td>{{ bill.amount|currency }}</td>
<td>
<span id="settle-bill" class="ml-auto pb-2">
<a href="{{ url_for('.settle', amount = bill.amount, ower_id = bill.ower.id, payer_id = bill.receiver.id) }}" class="btn btn-primary">
<div data-toggle="tooltip" title='{{ _("Click here to record that the money transfer has been done") }}'>
{{ ("Settle") }}
</div>
</a>
</span>
</td>
{% extends "sidebar_table_layout.html" %} {% block sidebar %}
<div id="table_overflow">{{ balance_table(show_weight=False) }}</div>
{% endblock %} {% block content %}
<table id="bill_table" class="split_bills table table-striped">
<thead>
<tr>
<th>{{ _("Who pays?") }}</th>
<th>{{ _("To whom?") }}</th>
<th>{{ _("How much?") }}</th>
<th>{{ _("Settled?") }}</th>
</tr>
</thead>
<tbody>
{% for transaction in transactions %}
<tr receiver="{{transaction.receiver.id}}">
<td>{{ transaction.ower }}</td>
<td>{{ transaction.receiver }}</td>
<td>{{ transaction.amount|currency }}</td>
<td>
<span id="settle-bill" class="ml-auto pb-2">
<form class="" action="{{ url_for(".add_settlement_bill") }}" method="POST">
{{ settlement_form.csrf_token }}
{{ settlement_form.amount(value=transaction.amount) }}
{{ settlement_form.sender_id(value=transaction.ower.id) }}
{{ settlement_form.receiver_id(value=transaction.receiver.id) }}
<button class="btn btn-primary" type="submit" title="{{ _("Settle") }}">
<div
data-toggle="tooltip"
title='{{ _("Click here to record that the money transfer has been done") }}'
>
{{ _("Settle") }}
</div>
</button>
</form>
<a
href="{{ url_for('.add_settlement_bill', amount = transaction.amount, sender_id = transaction.ower.id, receiver_id = transaction.receiver.id) }}"
class="btn btn-primary"
>
{{ ("Settle") }}
</div>
</a>
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</tbody>
</table>

{% endblock %}
112 changes: 102 additions & 10 deletions ihatemoney/tests/budget_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1358,23 +1358,25 @@ def test_settle_button(self):
count = 0
for t in transactions:
count += 1
self.client.get(
"/raclette/settle"
+ "/"
+ str(t["amount"])
+ "/"
+ str(t["ower"].id)
+ "/"
+ str(t["receiver"].id)
self.client.post(
"/raclette/settle",
data={
"amount": t["amount"],
"sender_id": t["ower"].id,
"receiver_id": t["receiver"].id,
},
)
temp_transactions = project.get_transactions_to_settle_bill()
# test if the one has disappeared
assert len(temp_transactions) == len(transactions) - count

# test if theres a new one with bill_type reimbursement
# test if there is a new one with bill_type reimbursement
bill = project.get_newest_bill()
assert bill.bill_type == models.BillType.REIMBURSEMENT
return

# There should be no more settlement to do at the end
transactions = project.get_transactions_to_settle_bill()
assert len(transactions) == 0

def test_settle_zero(self):
self.post_project("raclette")
Expand Down Expand Up @@ -1463,6 +1465,78 @@ def test_access_other_projects(self):
# Create and log in as another project
self.post_project("tartiflette")

# Add a participant in this second project
self.client.post("/tartiflette/members/add", data={"name": "pirate"})
pirate = models.Person.query.filter(models.Person.id == 5).one()
assert pirate.name == "pirate"

# Try to add a new bill to another project
resp = self.client.post(
"/raclette/add",
data={
"date": "2017-01-01",
"what": "fromage frelaté",
"payer": 2,
"payed_for": [2, 3, 4],
"bill_type": "Expense",
"amount": "100.0",
},
)
# Ensure it has not been created
raclette = self.get_project("raclette")
assert raclette.get_bills().count() == 1

# Try to add a new bill in our project that references members of another project.
# First with invalid payed_for IDs.
resp = self.client.post(
"/tartiflette/add",
data={
"date": "2017-01-01",
"what": "soupe",
"payer": 5,
"payed_for": [3],
"bill_type": "Expense",
"amount": "5000.0",
},
)
# Ensure it has not been created
piratebill = models.Bill.query.filter(models.Bill.what == "soupe").one_or_none()
assert piratebill is None, "piratebill 1 should not exist"

# Then with invalid payer ID
self.client.post(
"/tartiflette/add",
data={
"date": "2017-02-01",
"what": "pain",
"payer": 3,
"payed_for": [5],
"bill_type": "Expense",
"amount": "5000.0",
},
)
# Ensure it has not been created
piratebill = models.Bill.query.filter(models.Bill.what == "pain").one_or_none()
assert piratebill is None, "piratebill 2 should not exist"

# Make sure we can actually create valid bills
self.client.post(
"/tartiflette/add",
data={
"date": "2017-03-01",
"what": "baguette",
"payer": 5,
"payed_for": [5],
"bill_type": "Expense",
"amount": "5.0",
},
)
# Ensure it has been created
okbill = models.Bill.query.filter(models.Bill.what == "baguette").one_or_none()
assert okbill is not None, "Bill baguette should exist"
assert okbill.what == "baguette"

# Now try to access and modify existing bills
modified_bill = {
"date": "2018-12-31",
"what": "roblochon",
Expand Down Expand Up @@ -1556,6 +1630,24 @@ def test_access_other_projects(self):
member = models.Person.query.filter(models.Person.id == 1).one_or_none()
assert member is None

# test new settle endpoint to add bills with wrong ids
self.client.post("/exit")
self.client.post(
"/authenticate", data={"id": "tartiflette", "password": "tartiflette"}
)
self.client.post(
"/tartiflette/settle",
data={
"sender_id": 4,
"receiver_id": 5,
"amount": "42.0",
},
)
piratebill = models.Bill.query.filter(
models.Bill.bill_type == models.BillType.REIMBURSEMENT
).one_or_none()
assert piratebill is None, "piratebill 3 should not exist"

@pytest.mark.skip(reason="Currency conversion is broken")
def test_currency_switch(self):
# A project should be editable
Expand Down
4 changes: 3 additions & 1 deletion ihatemoney/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,9 @@ def format_form_errors(form, prefix):
)
else:
error_list = "</li><li>".join(
str(error) for (field, errors) in form.errors.items() for error in errors
f"<strong>{field}</strong> {error}"
for (field, errors) in form.errors.items()
for error in errors
)
errors = f"<ul><li>{error_list}</li></ul>"
# I18N: Form error with a list of errors
Expand Down
41 changes: 32 additions & 9 deletions ihatemoney/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
ProjectForm,
ProjectFormWithCaptcha,
ResetPasswordForm,
SettlementForm,
get_billform_for,
)
from ihatemoney.history import get_history, get_history_queries, purge_history
Expand Down Expand Up @@ -852,24 +853,46 @@ def change_lang(lang):
@main.route("/<project_id>/settle_bills")
def settle_bill():
"""Compute the sum each one have to pay to each other and display it"""
bills = g.project.get_transactions_to_settle_bill()
return render_template("settle_bills.html", bills=bills, current_view="settle_bill")
transactions = g.project.get_transactions_to_settle_bill()
settlement_form = SettlementForm()
return render_template(
"settle_bills.html",
transactions=transactions,
settlement_form=settlement_form,
current_view="settle_bill",
)


@main.route("/<project_id>/settle", methods=["POST"])
def add_settlement_bill():
"""Create a bill to register a settlement"""
form = SettlementForm(id=g.project.id)
if not form.validate():
flash(
format_form_errors(form, _("Error creating settlement bill")),
category="danger",
)
return redirect(url_for(".settle_bill"))

# Ensure that the sender and receiver ID are valid and part of this project
receiver_id = form.receiver_id.data
sender_id = form.sender_id.data

if not g.project.has_member(sender_id):
return redirect(url_for(".settle_bill"))

@main.route("/<project_id>/settle/<amount>/<int:ower_id>/<int:payer_id>")
def settle(amount, ower_id, payer_id):
new_reinbursement = Bill(
amount=float(amount),
settlement = Bill(
amount=form.amount.data,
date=datetime.datetime.today(),
owers=[Person.query.get(payer_id)],
payer_id=ower_id,
owers=[Person.query.get(receiver_id, g.project)],
payer_id=sender_id,
project_default_currency=g.project.default_currency,
bill_type=BillType.REIMBURSEMENT,
what=_("Settlement"),
)
session.update()

db.session.add(new_reinbursement)
db.session.add(settlement)
db.session.commit()

flash(_("Settlement bill has been successfully added"), category="success")
Expand Down