Implement filters in server side code

This commit is contained in:
MilouH 2025-04-02 22:56:56 +02:00
parent 56bee93346
commit 5368399158
2 changed files with 145 additions and 47 deletions

View file

@ -9,7 +9,7 @@
{% block title %} - {{ g.project.name }}{% endblock %}
{% block js %}
{% if add_bill %} $('#new-bill > a').click(); {% endif %}
{% if add_bill %} $('#new-bill > a').click(); {% endif %}
// focus on first field when adding a bill
$("#bill-form").on('shown.bs.modal', function(){
@ -78,51 +78,100 @@
{% endblock %}
{% block content %}
<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">&times;</a>
</div>
<form action="{{ url_for(".add_bill") }}" method="post" class="modal-body container">
{{ forms.add_bill(bill_form, title=False) }}
</form>
<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">&times;</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>
<div class="d-flex flex-wrap w-100 pt-2 mt-2" id="bill-toolbar">
{% if bills.pages > 1 %}
<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) }}">&laquo; {{ _("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") }} &raquo;</a></li>
</ul>
{% 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>
<i class="icon icon-white before-text">{{ static_include("images/plus.svg") | safe }}</i>
{{ _("Add a new bill") }}
</a>
</span>
{% if bills.pages > 1 %}
<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) }}">&laquo; {{ _("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") }} &raquo;</a></li>
</ul>
{% 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") }}
</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 %}>
<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>
{{ _("Add a new bill") }}
</a>
</span>
</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-2 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="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-2 mb-2">
<label for="date_from" class="sr-only">{{ _("Start date") }}</label>
<input type="date" class="form-control form-control-sm w-100" id="date_from" name="date_from" value="{{ filter_date_from }}">
</div>
<div class="col-md-2 mb-2">
<label for="date_to" class="sr-only">{{ _("End date") }}</label>
<input type="date" class="form-control form-control-sm w-100" id="date_to" name="date_to" value="{{ filter_date_to }}">
</div>
<div class="col-md-1 mb-2">
<label for="amount_min" class="sr-only">{{ _("Amount min") }}</label>
<input type="text" class="form-control form-control-sm w-100" id="amount_min" name="amount_min" placeholder="{{ _('Min cost') }}" value="{{ filter_amount_min }}">
</div>
<div class="col-md-1 mb-2">
<label for="amount_max" class="sr-only">{{ _("Amount max") }}</label>
<input type="text" class="form-control form-control-sm w-100" id="amount_max" name="amount_max" placeholder="{{ _('Max cost') }}" value="{{ filter_amount_max }}">
</div>
<div class="col-md-8 mb-2 text-left">
<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 %}
<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>
<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 (weights, bill) in bills.items %}
<tr owers="{{bill.owers|join(',','id')}}" payer="{{bill.payer.id}}">
@ -162,7 +211,7 @@
</tbody>
</table>
{% else %}
<div class="py-3 d-flex justify-content-center empty-bill">
<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 billimg">{{ static_include("images/bill.svg") | safe }}</i>

View file

@ -36,6 +36,7 @@ from flask_babel import gettext as _
from flask_mail import Message
import qrcode
import qrcode.image.svg
from sqlalchemy import func
from sqlalchemy_continuum import Operation
from werkzeug.exceptions import NotFound
from werkzeug.security import check_password_hash
@ -670,12 +671,53 @@ def list_bills():
):
bill_form.payed_for.data = session["last_selected_payed_for"][g.project.id]
# Each item will be a (weight_sum, Bill) tuple.
# TODO: improve this awkward result using column_property:
# https://docs.sqlalchemy.org/en/14/orm/mapped_sql_expr.html.
weighted_bills = g.project.get_bill_weights_ordered().paginate(
per_page=100, error_out=True
)
# get filter values
search_query = request.args.get('search', '')
filter_date_from = request.args.get('date_from', '')
filter_date_to = request.args.get('date_to', '')
filter_amount_min = request.args.get('amount_min', '')
filter_amount_max = request.args.get('amount_max', '')
filter_payer = request.args.get('payer', '')
filters_active = any([search_query, filter_date_to, filter_date_from, filter_amount_min, filter_amount_max, filter_payer])
# if no filters active, use standard query
if not filters_active:
# Each item will be a (weight_sum, Bill) tuple.
# TODO: improve this awkward result using column_property:
# https://docs.sqlalchemy.org/en/14/orm/mapped_sql_expr.html.
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_from:
date_obj = datetime.datetime.strptime(filter_date_from, '%Y-%m-%d').date()
query = query.filter(Bill.date >= date_obj)
if filter_date_to:
date_obj = datetime.datetime.strptime(filter_date_to, '%Y-%m-%d').date()
query = query.filter(Bill.date <= date_obj)
if filter_amount_min:
query = query.filter(Bill.amount >= float(filter_amount_min))
if filter_amount_max:
query = query.filter(Bill.amount <= float(filter_amount_max))
if filter_payer:
query = query.filter(Bill.payer_id == filter_payer)
filtered_bill_ids = [bill.id for bill in query.all()]
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)
return render_template(
"list_bills.html",
@ -685,6 +727,13 @@ def list_bills():
csrf_form=csrf_form,
add_bill=request.values.get("add_bill", False),
current_view="list_bills",
search_query=search_query,
filter_date_to=filter_date_to,
filter_date_from=filter_date_from,
filter_amount_min=filter_amount_min,
filter_amount_max=filter_amount_max,
filter_payer=filter_payer,
search_active=filters_active,
)
@ -1044,4 +1093,4 @@ def favicon():
os.path.join(main.root_path, "static"),
"favicon.ico",
mimetype="image/vnd.microsoft.icon",
)
)