From 7b28517d1f8966f010ec681fab783d9af771f64d Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Wed, 27 Jul 2022 16:20:49 +0200 Subject: add first filters to the browse view --- fietsboek/static/fietsboek.js | 15 +++++++ fietsboek/templates/browse.jinja2 | 79 +++++++++++++++++++++++++++++++++ fietsboek/views/browse.py | 93 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 186 insertions(+), 1 deletion(-) 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 %}

{{ _("page.browse.title") }}

+
+
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + + km +
+
+ +
+ < {{ _("page.browse.filter.length") }} < +
+ +
+
+ + + km +
+
+ +
+
+ + +
+
+ +
+ < {{ _("page.browse.filter.date") }} < +
+ +
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+
+
{% if tracks %} {% for track in tracks %}
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, -- cgit v1.2.3