mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-04-28 17:32:38 +02:00
implement search and filter of bills using server side filtering
This commit is contained in:
parent
56bee93346
commit
421d5d2438
2 changed files with 103 additions and 10 deletions
|
@ -94,17 +94,25 @@
|
||||||
<div class="d-flex flex-wrap w-100 pt-2 mt-2" id="bill-toolbar">
|
<div class="d-flex flex-wrap w-100 pt-2 mt-2" id="bill-toolbar">
|
||||||
{% if bills.pages > 1 %}
|
{% if bills.pages > 1 %}
|
||||||
<ul class="pagination mr-2 mb-0 pb-2 flex-wrap" id="pagination-top">
|
<ul class="pagination mr-2 mb-0 pb-2 flex-wrap" id="pagination-top">
|
||||||
<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>
|
<li class="page-item {% if bills.page == 1 %}disabled{% endif %}"><a class="page-link" href="{{ url_for('main.list_bills', page=bills.prev_num) }}{% if search_query %}&search={{ search_query }}{% endif %}{% if filter_date %}&date={{ filter_date }}{% endif %}{% if filter_amount %}&amount={{ filter_amount }}{% endif %}{% if filter_payer %}&payer={{ filter_payer }}{% endif %}{% if filter_for %}&for={{ filter_for }}{% endif %}{% if filter_what %}&what={{ filter_what }}{% endif %}">« {{ _("Newer bills") }}</a></li>
|
||||||
{%- for page in bills.iter_pages() %}
|
{%- for page in bills.iter_pages() %}
|
||||||
{% if page %}
|
{% 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>
|
<li class="page-item {% if page == bills.page %}active{% endif %}"><a class="page-link" href="{{ url_for('main.list_bills', page=page) }}{% if search_query %}&search={{ search_query }}{% endif %}{% if filter_date %}&date={{ filter_date }}{% endif %}{% if filter_amount %}&amount={{ filter_amount }}{% endif %}{% if filter_payer %}&payer={{ filter_payer }}{% endif %}{% if filter_for %}&for={{ filter_for }}{% endif %}{% if filter_what %}&what={{ filter_what }}{% endif %}">{{ page }}</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled"><span class="ellipsis page-link">…</span></li>
|
<li class="page-item disabled"><span class="ellipsis page-link">…</span></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{%- endfor %}
|
{%- 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>
|
<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) }}{% if search_query %}&search={{ search_query }}{% endif %}{% if filter_date %}&date={{ filter_date }}{% endif %}{% if filter_amount %}&amount={{ filter_amount }}{% endif %}{% if filter_payer %}&payer={{ filter_payer }}{% endif %}{% if filter_for %}&for={{ filter_for }}{% endif %}{% if filter_what %}&what={{ filter_what }}{% endif %}">{{ _("Older bills") }} »</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap align-items-center mr-2 mb-2">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-toggle="collapse" data-target="#filterCollapse" aria-expanded="false" aria-controls="filterCollapse">
|
||||||
|
{{ _("Filter") }}
|
||||||
|
{% if search_active %}<span class="badge badge-info ml-1">{{ _("Active") }}</span>{% endif %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span id="new-bill" class="ml-auto pb-2" {% if not g.project.members %} data-toggle="tooltip" title="{{_('You should start by adding participants')}}" {% endif %}>
|
<span id="new-bill" class="ml-auto pb-2" {% 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 {% if not g.project.members %} disabled {% endif %}" data-toggle="modal" data-keyboard="true" data-target="#bill-form" autofocus>
|
<a href="{{ url_for('.add_bill') }}" class="btn btn-primary {% if not g.project.members %} disabled {% endif %}" data-toggle="modal" data-keyboard="true" data-target="#bill-form" autofocus>
|
||||||
<i class="icon icon-white before-text">{{ static_include("images/plus.svg") | safe }}</i>
|
<i class="icon icon-white before-text">{{ static_include("images/plus.svg") | safe }}</i>
|
||||||
|
@ -113,6 +121,40 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse {% if search_active %}show{% endif %} mb-3" id="filterCollapse">
|
||||||
|
<div class="card card-body">
|
||||||
|
<form method="GET" action="{{ url_for('main.list_bills') }}" class="form-inline">
|
||||||
|
<div class="form-row w-100">
|
||||||
|
<div class="col-md-4 mb-2">
|
||||||
|
<label for="search" class="sr-only">{{ _("Search") }}</label>
|
||||||
|
<input type="text" class="form-control form-control-sm w-100" id="search" name="search" placeholder="{{ _('Search in For what?') }}" value="{{ search_query }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-2">
|
||||||
|
<label for="date" class="sr-only">{{ _("Date") }}</label>
|
||||||
|
<input type="date" class="form-control form-control-sm w-100" id="date" name="date" value="{{ filter_date }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-2">
|
||||||
|
<label for="amount" class="sr-only">{{ _("Amount") }}</label>
|
||||||
|
<input type="text" class="form-control form-control-sm w-100" id="amount" name="amount" placeholder="{{ _('Amount') }}" value="{{ filter_amount }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-2">
|
||||||
|
<label for="payer" class="sr-only">{{ _("Payer") }}</label>
|
||||||
|
<select class="form-control form-control-sm w-100" id="payer" name="payer">
|
||||||
|
<option value="">{{ _('Any payer') }}</option>
|
||||||
|
{% for member in g.project.active_members %}
|
||||||
|
<option value="{{ member.id }}" {% if filter_payer|string == member.id|string %}selected{% endif %}>{{ member.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8 mb-2 text-right">
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary">{{ _("Apply filters") }}</button>
|
||||||
|
<a href="{{ url_for('main.list_bills') }}" class="btn btn-sm btn-outline-secondary ml-2">{{ _("Clear filters") }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% 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>
|
||||||
|
@ -125,6 +167,7 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for (weights, bill) in bills.items %}
|
{% for (weights, bill) in bills.items %}
|
||||||
|
{% if bill %}
|
||||||
<tr owers="{{bill.owers|join(',','id')}}" payer="{{bill.payer.id}}">
|
<tr owers="{{bill.owers|join(',','id')}}" payer="{{bill.payer.id}}">
|
||||||
<td>
|
<td>
|
||||||
<span data-toggle="tooltip" data-placement="top"
|
<span data-toggle="tooltip" data-placement="top"
|
||||||
|
@ -158,6 +201,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -60,7 +60,8 @@ 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, billowers, db
|
||||||
|
from sqlalchemy import func
|
||||||
from ihatemoney.utils import (
|
from ihatemoney.utils import (
|
||||||
Redirect303,
|
Redirect303,
|
||||||
csv2list_of_dicts,
|
csv2list_of_dicts,
|
||||||
|
@ -670,12 +671,55 @@ def list_bills():
|
||||||
):
|
):
|
||||||
bill_form.payed_for.data = session["last_selected_payed_for"][g.project.id]
|
bill_form.payed_for.data = session["last_selected_payed_for"][g.project.id]
|
||||||
|
|
||||||
# Each item will be a (weight_sum, Bill) tuple.
|
# get filter values
|
||||||
# TODO: improve this awkward result using column_property:
|
search_query = request.args.get('search', '')
|
||||||
# https://docs.sqlalchemy.org/en/14/orm/mapped_sql_expr.html.
|
filter_date = request.args.get('date', '')
|
||||||
weighted_bills = g.project.get_bill_weights_ordered().paginate(
|
filter_amount = request.args.get('amount', '')
|
||||||
per_page=100, error_out=True
|
filter_payer = request.args.get('payer', '')
|
||||||
)
|
|
||||||
|
filters_active = any([search_query, filter_date, filter_amount, filter_payer])
|
||||||
|
|
||||||
|
# if no filters active, use standard query
|
||||||
|
if not filters_active:
|
||||||
|
weighted_bills = g.project.get_bill_weights_ordered().paginate(
|
||||||
|
per_page=100, error_out=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# start with all bills
|
||||||
|
query = Bill.query.filter(Bill.payer_id == Person.id).filter(Person.project_id == g.project.id)
|
||||||
|
|
||||||
|
# apply filters if there are any
|
||||||
|
if search_query:
|
||||||
|
query = query.filter(Bill.what.ilike(f'%{search_query}%'))
|
||||||
|
|
||||||
|
if filter_date:
|
||||||
|
try:
|
||||||
|
date_obj = datetime.datetime.strptime(filter_date, '%Y-%m-%d').date()
|
||||||
|
query = query.filter(Bill.date == date_obj)
|
||||||
|
except ValueError:
|
||||||
|
logging.error(f"invalid date format provided: {filter_date}. expected format: YYYY-MM-DD")
|
||||||
|
|
||||||
|
|
||||||
|
if filter_amount:
|
||||||
|
try:
|
||||||
|
amount = float(filter_amount)
|
||||||
|
query = query.filter(Bill.amount == amount)
|
||||||
|
except ValueError:
|
||||||
|
logging.error(f"invalid amount format provided: {filter_amount}. amount must be a number")
|
||||||
|
|
||||||
|
if filter_payer:
|
||||||
|
query = query.filter(Bill.payer_id == filter_payer)
|
||||||
|
|
||||||
|
filtered_bill_ids = [bill.id for bill in query.all()]
|
||||||
|
|
||||||
|
# Now get the weighted bills with these IDs
|
||||||
|
if filtered_bill_ids:
|
||||||
|
bills_query = g.project.get_bill_weights().filter(Bill.id.in_(filtered_bill_ids))
|
||||||
|
bills_query = g.project.order_bills(bills_query)
|
||||||
|
weighted_bills = bills_query.paginate(per_page=100, error_out=True)
|
||||||
|
else:
|
||||||
|
# no bills match the filters
|
||||||
|
weighted_bills = db.session.query(func.sum(Person.weight), Bill).filter(Bill.id == None).paginate(per_page=100, error_out=True)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"list_bills.html",
|
"list_bills.html",
|
||||||
|
@ -685,6 +729,11 @@ def list_bills():
|
||||||
csrf_form=csrf_form,
|
csrf_form=csrf_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",
|
||||||
|
search_query=search_query,
|
||||||
|
filter_date=filter_date,
|
||||||
|
filter_amount=filter_amount,
|
||||||
|
filter_payer=filter_payer,
|
||||||
|
search_active=filters_active,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue