mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-05-06 05:01:48 +02:00
Implemented a solution for 'undo delete'.
This commit is contained in:
parent
8e6643302f
commit
c9e6a7ec3c
4 changed files with 296 additions and 4 deletions
|
@ -204,6 +204,9 @@ msgstr ""
|
|||
msgid "The bill has been deleted"
|
||||
msgstr ""
|
||||
|
||||
msgid "UNDO"
|
||||
msgstr ""
|
||||
|
||||
msgid "The bill has been modified"
|
||||
msgstr ""
|
||||
|
||||
|
|
185
ihatemoney/templates/list_bills_post_delete.html
Normal file
185
ihatemoney/templates/list_bills_post_delete.html
Normal file
|
@ -0,0 +1,185 @@
|
|||
{% extends "sidebar_table_layout.html" %}
|
||||
|
||||
{% block title %} - {{ g.project.name }}{% endblock %}
|
||||
{% block js %}
|
||||
{% if add_bill %} $('#new-bill > a').click(); {% endif %}
|
||||
|
||||
// ask for confirmation before removing an user
|
||||
$('.action.delete').each(function(){
|
||||
var link = $(this).find('button');
|
||||
link.click(function(){
|
||||
if ($(this).hasClass("confirm")){
|
||||
return true;
|
||||
}
|
||||
$(this).html("{{_("you sure?")}}");
|
||||
$(this).addClass("confirm");
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
var highlight_owers = function(){
|
||||
var ower_ids = $(this).attr("owers").split(',');
|
||||
var payer_id = $(this).attr("payer");
|
||||
$.each(ower_ids, function(i, val){
|
||||
$('#bal-member-'+val).addClass("ower_line");
|
||||
});
|
||||
$("#bal-member-"+payer_id).addClass("payer_line");
|
||||
};
|
||||
|
||||
var unhighlight_owers = function(){
|
||||
$('[id^="bal-member-"]').removeClass("ower_line payer_line");
|
||||
};
|
||||
|
||||
$('#bill_table tbody tr').hover(highlight_owers, unhighlight_owers);
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<div class="sidebar_content">
|
||||
<form id="add-member-form" action="{{ url_for(".add_member") }}" method="post">
|
||||
{{ forms.add_member(member_form) }}
|
||||
</form>
|
||||
|
||||
<div id="table_overflow">
|
||||
<table class="balance table">
|
||||
{% set balance = g.project.balance %}
|
||||
{% for member in g.project.members | sort(attribute='name') if member.activated or balance[member.id]|round(2) != 0 %}
|
||||
<tr id="bal-member-{{ member.id }}" action={% if member.activated %}delete{% else %}reactivate{% endif %}>
|
||||
<td class="balance-name">{{ member.name }}
|
||||
<span class="light{% if not g.project.uses_weights %} extra-info{% endif %}">(x{{ member.weight|minimal_round(1) }})</span>
|
||||
</td>
|
||||
{% if member.activated %}
|
||||
<td>
|
||||
<form class="action delete" action="{{ url_for(".remove_member", member_id=member.id) }}" method="POST">
|
||||
<button type="submit">{{ _("deactivate") }}</button></form>
|
||||
<form class="action edit" action="{{ url_for(".edit_member", member_id=member.id) }}" method="GET">
|
||||
<button type="submit">{{ _("edit") }}</button></form>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>
|
||||
<form class="action reactivate" action="{{ url_for(".reactivate", member_id=member.id) }}" method="POST">
|
||||
<button type="submit">{{ _("reactivate") }}</button></form></td>
|
||||
{% endif %}
|
||||
<td class="balance-value {% if balance[member.id]|round(2) > 0 %}positive{% elif balance[member.id]|round(2) < 0 %}negative{% endif %}">
|
||||
{% if balance[member.id]|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="identifier">
|
||||
<a class="btn btn-secondary btn-block" href="{{ url_for('.invite') }}">
|
||||
<i class="icon icon-white paper-plane">{{ static_include("images/paper-plane.svg") | safe }}</i>
|
||||
{{ _("Invite people") }}
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<span id="undo" class="float-left">
|
||||
<a href="{{ url_for('.undo_delete_bill') }}" method="get" class="btn btn-primary float-left">
|
||||
{{ _("Undo delete") }}
|
||||
</a>
|
||||
</span>
|
||||
<span id="new-bill" class="float-right" {% if not g.project.members %} data-toggle="tooltip" title="{{_('You should start by adding participants')}}" {% endif %}>
|
||||
<a href="{{ url_for('.add_bill') }}" class="btn btn-primary float-right {% if not g.project.members %} disabled {% endif %}" data-toggle="modal" data-keyboard="false" data-target="#bill-form">
|
||||
<i class="icon icon-white plus">{{ static_include("images/plus.svg") | safe }}</i>
|
||||
{{ _("Add a new bill") }}
|
||||
</a>
|
||||
</span>
|
||||
<div id="bill-form" class="modal fade show" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">{{ _('Add a bill') }}</h3>
|
||||
<a href="#" class="close" data-dismiss="modal">×</a>
|
||||
</div>
|
||||
<form action="{{ url_for(".add_bill") }}" method="post" class="modal-body container">
|
||||
{{ forms.add_bill(bill_form, title=False) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if bills.pages > 1 %}
|
||||
<span id="previous-page" class="float-left">
|
||||
<a class="btn btn-outline-secondary float-left {% if bills.page == 1 %}disabled{% endif %}" href="{{ url_for('main.list_bills', page=bills.prev_num) }}">« {{ _("Newer bills") }}</a>
|
||||
</span>
|
||||
|
||||
<span id="next-page" class="float-left">
|
||||
<a class="btn btn-outline-secondary float-left {% if bills.page == bills.pages %}disabled{% endif %}" href="{{ url_for('main.list_bills', page=bills.next_num) }}">{{ _("Older bills") }} »</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if bills.total > 0 %}
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<table id="bill_table" class="col table table-striped table-hover table-responsive-sm">
|
||||
<thead><tr><th>{{ _("When?") }}</th><th>{{ _("Who paid?") }}</<th><th>{{ _("For what?") }}</th><th>{{ _("For whom?") }}</th><th>{{ _("How much?") }}</th><th>{{ _("Actions") }}</th></tr></thead>
|
||||
<tbody>
|
||||
{% for bill in bills.items %}
|
||||
<tr owers="{{bill.owers|join(',','id')}}" payer="{{bill.payer.id}}">
|
||||
<td>
|
||||
<span data-toggle="tooltip" data-placement="top"
|
||||
title="{{ _('Added on %(date)s', date=bill.creation_date if bill.creation_date else bill.date) }}">
|
||||
{{ bill.date }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ bill.payer }}</td>
|
||||
<td>{{ bill.what }}</td>
|
||||
<td>{% if bill.owers|length == g.project.members|length -%}
|
||||
{{ _("Everyone") }}
|
||||
{%- elif bill.owers|length > g.project.members|length / 2 + 1 -%}
|
||||
{{ _("Everyone but %(excluded)s", excluded=g.project.members|reject('in', bill.owers)|join(', ', 'name')) }}
|
||||
{%- else -%}
|
||||
{{ bill.owers|join(', ', 'name') }}
|
||||
{%- endif %}</td>
|
||||
<td>{{ "%0.2f"|format(bill.amount) }} ({{ "%0.2f"|format(bill.pay_each()) }} {{ _("each") }})</td>
|
||||
<td class="bill-actions">
|
||||
<a class="edit" href="{{ url_for(".edit_bill", bill_id=bill.id) }}" title="{{ _("edit") }}">{{ _('edit') }}</a>
|
||||
<a class="delete" href="{{ url_for(".delete_bill", bill_id=bill.id) }}" title="{{ _("delete") }}">{{ _('delete') }}</a>
|
||||
{% if bill.external_link %}
|
||||
<a class="see" href="{{ bill.external_link }}" ref="noopener" target="_blank" title="{{ _("see") }}">{{ _('see') }} </a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if bills.pages > 1 %}
|
||||
<ul class="pagination">
|
||||
<li class="page-item {% if bills.page == 1 %}disabled{% endif %}"><a class="page-link" href="{{ url_for('main.list_bills', page=bills.prev_num) }}">« Newer bills</a></li>
|
||||
{%- for page in bills.iter_pages() %}
|
||||
{% if page %}
|
||||
<li class="page-item {% if page == bills.page %}active{% endif %}"><a class="page-link" href="{{ url_for('main.list_bills', page=page) }}">{{ page }}</a></li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="ellipsis page-link">…</span></li>
|
||||
{% endif %}
|
||||
{%- endfor %}
|
||||
<li class="page-item {% if bills.page == bills.pages %}disabled{% endif %}"><a class="page-link" href="{{ url_for('main.list_bills', page=bills.next_num) }}">Older bills »</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="py-3 d-flex justify-content-center empty-bill">
|
||||
<div class="card d-inline-flex p-2">
|
||||
<div class="card-body text-center text-muted">
|
||||
<i class="icon icon-white hand-holding-heart">{{ static_include("images/hand-holding-heart.svg") | safe }}</i>
|
||||
<h3>{{ _('No bills')}}</h3>
|
||||
<p>
|
||||
{{ _("Nothing to list yet.")}}<br />
|
||||
{{ _("You probably want to") }}
|
||||
{%- if g.project.members %} <a href="{{ url_for('.add_bill') }}" data-toggle="modal" data-target="#bill-form">
|
||||
{{- _("add a bill") -}}
|
||||
</a> ?
|
||||
{% else %} <a href="{{ url_for('.add_member') }}">
|
||||
{{- _('add participants') -}}
|
||||
</a> ?
|
||||
{%- endif -%}
|
||||
</p>
|
||||
</div>
|
||||
</div></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -673,6 +673,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
|||
balance = models.Project.query.get("raclette").balance
|
||||
self.assertEqual(set(balance.values()), set([19.0, -19.0]))
|
||||
|
||||
|
||||
# Bill with negative amount
|
||||
self.client.post(
|
||||
"/raclette/add",
|
||||
|
@ -787,6 +788,61 @@ class BudgetTestCase(IhatemoneyTestCase):
|
|||
self.client.post("/raclette/members/add", data={"name": "fred"})
|
||||
self.client.post("/raclette/members/add", data={"name": "tata"})
|
||||
|
||||
# create bills, then delete and undo
|
||||
self.client.post(
|
||||
"/raclette/add",
|
||||
data={
|
||||
"date": "2020-04-22",
|
||||
"what": "Glover Apts. Rent April",
|
||||
"payer": 1,
|
||||
"payed_for": [1, 2, 3],
|
||||
"amount": "852.00",
|
||||
},
|
||||
)
|
||||
|
||||
balance = models.Project.query.get("raclette").balance
|
||||
self.assertEqual(set(balance.values()), set([568.00, -284.00, -284.00]))
|
||||
|
||||
self.client.post(
|
||||
"/raclette/add",
|
||||
data={
|
||||
"data": "2020-05-22",
|
||||
"what": "Glover Apts. Rent May",
|
||||
"payer": 2,
|
||||
"payed_for": [1, 2, 3],
|
||||
"amount": "852.00",
|
||||
},
|
||||
)
|
||||
|
||||
balance = models.Project.query.get("raclette").balance
|
||||
self.assertEqual(set(balance.values()), set([284.00, 284.00, -568.00]))
|
||||
|
||||
bill = models.Bill.query.one()
|
||||
self.assertEqual(bill.amount, 852.00)
|
||||
|
||||
# delete the first bill
|
||||
self.client.get("/raclette/delete/%s" % bill.id)
|
||||
self.assertEqual(1, len(models.Bill.query.all()), "bill deletion")
|
||||
|
||||
# check balance
|
||||
balance = models.Project.query.get("raclette").balance
|
||||
self.assertEqual(set(balance.values()), set([-284.00, 568.00, -284.00]))
|
||||
|
||||
# undo delete
|
||||
self.client.get("/raclette/undo")
|
||||
|
||||
# recheck balance
|
||||
balance = models.Project.query.get("raclette").balance
|
||||
self.assertEqual(set(balance.values()), set([284.00, 284.00, -568.00]))
|
||||
|
||||
# delete both bills
|
||||
bill = models.Bill.query.one()
|
||||
self.client.get("/raclette/delete/%s" % bill.id)
|
||||
bill = models.Bill.query.one()
|
||||
self.client.get("/raclette/delete/%s" % bill.id)
|
||||
balance = models.Project.query.get("raclette").balance
|
||||
self.assertEqual(set(balance.values()), set([0, 0, 0]))
|
||||
|
||||
# create bills
|
||||
self.client.post(
|
||||
"/raclette/add",
|
||||
|
|
|
@ -12,6 +12,7 @@ from datetime import datetime
|
|||
from functools import wraps
|
||||
import json
|
||||
import os
|
||||
import copy
|
||||
from smtplib import SMTPRecipientsRefused
|
||||
|
||||
from dateutil.parser import parse
|
||||
|
@ -64,6 +65,8 @@ main = Blueprint("main", __name__)
|
|||
|
||||
login_throttler = LoginThrottler(max_attempts=3, delay=1)
|
||||
|
||||
ghost_billform = None
|
||||
|
||||
|
||||
def requires_admin(bypass=None):
|
||||
"""Require admin permissions for @requires_admin decorated endpoints.
|
||||
|
@ -213,8 +216,8 @@ def authenticate(project_id=None):
|
|||
|
||||
project = Project.query.get(project_id)
|
||||
if not project:
|
||||
# If the user try to connect to an unexisting project, we will
|
||||
# propose him a link to the creation form.
|
||||
# If the user tries to connect to an unexisting project, we will
|
||||
# provide them with a link to the creation form.
|
||||
return render_template(
|
||||
"authenticate.html", form=form, create_project=project_id
|
||||
)
|
||||
|
@ -620,7 +623,7 @@ def add_member():
|
|||
if form.validate():
|
||||
member = form.save(g.project, Person())
|
||||
db.session.commit()
|
||||
flash(_("%(member)s had been added", member=member.name))
|
||||
flash(_("%(member)s has been added", member=member.name))
|
||||
return redirect(url_for(".list_bills"))
|
||||
|
||||
return render_template("add_member.html", form=form)
|
||||
|
@ -699,17 +702,62 @@ def add_bill():
|
|||
return render_template("add_bill.html", form=form)
|
||||
|
||||
|
||||
@main.route("/<project_id>/delete/<int:bill_id>")
|
||||
@main.route("/<project_id>/delete/<int:bill_id>", methods=["GET", "POST"])
|
||||
def delete_bill(bill_id):
|
||||
global ghost_billform
|
||||
ghost_billform = get_billform_for(g.project)
|
||||
# fixme: everyone is able to delete a bill
|
||||
bill = Bill.query.get(g.project, bill_id)
|
||||
if not bill:
|
||||
return redirect(url_for(".list_bills"))
|
||||
|
||||
# save the deleted bill, so that it can be restored if the
|
||||
# user chooses to undo this action
|
||||
ghost_billform.fill(bill)
|
||||
|
||||
db.session.delete(bill)
|
||||
db.session.commit()
|
||||
flash(_("The bill has been deleted"))
|
||||
|
||||
return redirect(url_for(".post_delete"))
|
||||
|
||||
|
||||
@main.route("/<project_id>/post_delete", methods=["GET", "POST"])
|
||||
def post_delete():
|
||||
# this functions identically to list_bills, however
|
||||
# the undo action button is added
|
||||
bill_form = get_billform_for(g.project)
|
||||
# set the last selected payer as default choice if exists
|
||||
if "last_selected_payer" in session:
|
||||
bill_form.payer.data = session["last_selected_payer"]
|
||||
# Preload the "owers" relationship for all bills
|
||||
bills = (
|
||||
g.project.get_bills()
|
||||
.options(orm.subqueryload(Bill.owers))
|
||||
.paginate(per_page=100, error_out=True)
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"list_bills_post_delete.html",
|
||||
bills=bills,
|
||||
member_form=MemberForm(g.project),
|
||||
bill_form=bill_form,
|
||||
add_bill=request.values.get("add_bill", False),
|
||||
current_view="list_bills_post_delete",
|
||||
)
|
||||
|
||||
|
||||
@main.route("/<project_id>/undo", methods=["GET", "POST"])
|
||||
def undo_delete_bill():
|
||||
global ghost_billform
|
||||
args = {}
|
||||
|
||||
bill = Bill()
|
||||
db.session.add(ghost_billform.save(bill, g.project))
|
||||
db.session.commit()
|
||||
|
||||
flash(_("Restored bill"))
|
||||
|
||||
return redirect(url_for(".list_bills"))
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue