aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fietsboek/static/fietsboek.js15
-rw-r--r--fietsboek/templates/browse.jinja279
-rw-r--r--fietsboek/views/browse.py93
3 files changed, 186 insertions, 1 deletions
diff --git a/fietsboek/static/fietsboek.js b/fietsboek/static/fietsboek.js
index 265032e..bf402a8 100644
--- a/fietsboek/static/fietsboek.js
+++ b/fietsboek/static/fietsboek.js
@@ -290,6 +290,21 @@ addHandler(".archive-checkbox", "change", () => {
document.querySelector("#archiveDownloadButton").disabled = (checked.length == 0);
});
+/**
+ * Handler to clear the input when a .button-clear-input is pressed.
+ *
+ * The button must be in an input-group with the input.
+ *
+ * @param event - The triggering event.
+ */
+function clearInputButtonClicked(event) {
+ const input = event.target.closest(".input-group").querySelector("input");
+ input.value = "";
+}
+
+addHandler(".button-clear-input", "click", clearInputButtonClicked);
+
+
document.addEventListener('DOMContentLoaded', function() {
window.fietsboekImageIndex = 0;
diff --git a/fietsboek/templates/browse.jinja2 b/fietsboek/templates/browse.jinja2
index 2732984..b152b1e 100644
--- a/fietsboek/templates/browse.jinja2
+++ b/fietsboek/templates/browse.jinja2
@@ -2,6 +2,85 @@
{% block content %}
<div class="container">
<h1>{{ _("page.browse.title") }}</h1>
+ <div class="mb-3">
+ <form id="browseFilter">
+ <div class="row g-3 mb-3">
+ <div class="col-12">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="search-terms" type="text" class="form-control" placeholder="{{ _("page.browse.filter.search_terms") }}" value="{{ request.params.get('search-terms', '') }}">
+ </div>
+ </div>
+ </div>
+
+ <div class="collapse row g-3 mb-3" id="advancedSearch">
+ <div class="col-md-6">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="tags" type="text" class="form-control" placeholder="{{ _("page.browse.filter.tags") }}" value="{{ request.params.get('tags', '') }}">
+ </div>
+ </div>
+
+ <div class="col-md-6">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="tagged-person" type="text" class="form-control" placeholder="{{ _("page.browse.filter.tagged_person") }}" value="{{ request.params.get('tagged-person', '') }}">
+ </div>
+ </div>
+
+ <div class="col-md-4">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="min-length" type="number" class="form-control" placeholder="{{ _("page.browse.filter.length_minimum") }}" value="{{ request.params.get('min-length', '') }}">
+ <span class="input-group-text">km</span>
+ </div>
+ </div>
+
+ <div class="col-md-4 fs-5 d-flex align-items-center justify-content-center">
+ &lt; {{ _("page.browse.filter.length") }} &lt;
+ </div>
+
+ <div class="col-md-4">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="max-length" type="number" class="form-control" placeholder="{{ _("page.browse.filter.length_maximum") }}" value="{{ request.params.get('max-length', '') }}">
+ <span class="input-group-text">km</span>
+ </div>
+ </div>
+
+ <div class="col-md-4">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="min-date" type="date" class="form-control" value="{{ request.params.get('min-date', '') }}">
+ </div>
+ </div>
+
+ <div class="col-md-4 fs-5 d-flex align-items-center justify-content-center">
+ &lt; {{ _("page.browse.filter.date") }} &lt;
+ </div>
+
+ <div class="col-md-4">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="max-date" type="date" class="form-control" value="{{ request.params.get('max-date', '') }}">
+ </div>
+ </div>
+ </div>
+
+ <div class="row g-3 mb-3">
+ <div class="col-md-6">
+ <button type="submit" class="btn btn-primary">
+ {{ _("page.browse.filters.apply") }}
+ </button>
+ </div>
+ <div class="col-md-6">
+ <button class="btn btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#advancedSearch" aria-expanded="false" aria-controls="advancedSearch">
+ {{ _("page.browse.filters.expand_advanced") }}
+ </button>
+ </div>
+ </div>
+ </form>
+ </div>
{% if tracks %}
{% for track in tracks %}
<div class="card mb-3">
diff --git a/fietsboek/views/browse.py b/fietsboek/views/browse.py
index 17dd45a..83a58b3 100644
--- a/fietsboek/views/browse.py
+++ b/fietsboek/views/browse.py
@@ -1,9 +1,10 @@
"""Views for browsing all tracks."""
+import datetime
from io import RawIOBase
from zipfile import ZipFile, ZIP_DEFLATED
from pyramid.view import view_config
-from pyramid.httpexceptions import HTTPForbidden, HTTPNotFound
+from pyramid.httpexceptions import HTTPForbidden, HTTPNotFound, HTTPBadRequest
from pyramid.response import Response
from sqlalchemy import select
@@ -32,6 +33,94 @@ class Stream(RawIOBase):
return b"".join(buf)
+def _get_int(request, name):
+ try:
+ return int(request.params.get(name))
+ except ValueError:
+ raise HTTPBadRequest(f'Invalid integer in {name!r}')
+
+def _get_date(request, name):
+ try:
+ return datetime.date.fromisoformat(request.params.get(name))
+ except ValueError:
+ raise HTTPBadRequest(f'Invalid date in {name!r}')
+
+
+class TrackFilters:
+ """A filter that applies user-given filters to a track."""
+ # TODO: We should also do some of those in SQL, if possible.
+
+ def __init__(self, filters):
+ self._filters = filters
+
+ def apply(self, track):
+ """Apply the filters to the track.
+
+ :param track: The track.
+ :type track: fietsboek.models.track.Track
+ :return: Whether the track matches the filters.
+ :rtype: bool
+ """
+ return all(f(track) for f in self._filters)
+
+ @classmethod
+ def parse(cls, request):
+ """Parse the filters from the given request.
+
+ :raises HTTPBadRequest: If the filters are malformed.
+ :param request: The request.
+ :type request: pyramid.request.Request
+ :return: The parsed filter.
+ :rtype: TrackFilters
+ """
+ filters = []
+ if request.params.get('search-terms'):
+ term = request.params.get('search-terms').strip()
+ filters.append(lambda track: term.lower() in track.title.lower())
+
+ if request.params.get('tags'):
+ tags = [tag.strip() for tag in request.params.get('tags').split('&&')]
+ tags = list(filter(bool, tags))
+
+ def has_tags(track):
+ lower_tags = {tag.lower() for tag in track.text_tags()}
+ return all(tag.lower() in lower_tags for tag in tags)
+
+ filters.append(has_tags)
+
+ if request.params.get('tagged-person'):
+ names = [name.strip() for name in request.params.get('tagged-person').split('&&')]
+ names = list(filter(bool, names))
+
+ def has_people(track):
+ peoples_names = [person.name for person in track.tagged_people]
+ peoples_names.append(track.owner.name)
+ peoples_names = set(map(str.lower, peoples_names))
+ print(peoples_names)
+ return all(name.lower() in peoples_names for name in names)
+
+ filters.append(has_people)
+
+ if request.params.get('min-length'):
+ # Value is given in km, so convert it to m
+ min_length = _get_int(request, "min-length") * 1000
+ filters.append(lambda track: track.length >= min_length)
+
+ if request.params.get('max-length'):
+ max_length = _get_int(request, "max-length") * 1000
+ filters.append(lambda track: track.length <= max_length)
+
+ if request.params.get('min-date'):
+ min_date = _get_date(request, "min-date")
+ filters.append(lambda track: track.date.date() >= min_date)
+
+ if request.params.get('max-date'):
+ max_date = _get_date(request, "max-date")
+ filters.append(lambda track: track.date.date() <= max_date)
+
+ return TrackFilters(filters)
+
+
def visible_tracks(dbsession, user):
"""Returns all visible tracks for the given user.
@@ -64,7 +153,9 @@ def browse(request):
:return: The HTTP response.
:rtype: pyramid.response.Response
"""
+ filters = TrackFilters.parse(request)
tracks = visible_tracks(request.dbsession, request.identity)
+ tracks = [track for track in tracks if filters.apply(track)]
return {
'tracks': tracks,
'mps_to_kph': util.mps_to_kph,