geo-activity-playground 0.42.0__py3-none-any.whl → 0.43.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. geo_activity_playground/alembic/script.py.mako +0 -6
  2. geo_activity_playground/alembic/versions/da2cba03b71d_add_photos.py +40 -0
  3. geo_activity_playground/alembic/versions/script.py.mako +6 -0
  4. geo_activity_playground/core/activities.py +3 -1
  5. geo_activity_playground/core/datamodel.py +48 -22
  6. geo_activity_playground/core/enrichment.py +4 -2
  7. geo_activity_playground/core/meta_search.py +78 -34
  8. geo_activity_playground/core/missing_values.py +5 -2
  9. geo_activity_playground/core/paths.py +2 -0
  10. geo_activity_playground/webui/app.py +21 -7
  11. geo_activity_playground/webui/blueprints/photo_blueprint.py +198 -0
  12. geo_activity_playground/webui/search_util.py +23 -7
  13. geo_activity_playground/webui/templates/activity/show.html.j2 +43 -10
  14. geo_activity_playground/webui/templates/eddington/distance.html.j2 +1 -2
  15. geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +1 -2
  16. geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +18 -15
  17. geo_activity_playground/webui/templates/heatmap/index.html.j2 +1 -2
  18. geo_activity_playground/webui/templates/page.html.j2 +8 -0
  19. geo_activity_playground/webui/templates/photo/map.html.j2 +45 -0
  20. geo_activity_playground/webui/templates/photo/new.html.j2 +13 -0
  21. geo_activity_playground/webui/templates/search/index.html.j2 +1 -2
  22. geo_activity_playground/webui/templates/search_form.html.j2 +47 -22
  23. geo_activity_playground/webui/templates/summary/index.html.j2 +12 -10
  24. {geo_activity_playground-0.42.0.dist-info → geo_activity_playground-0.43.0.dist-info}/METADATA +2 -1
  25. {geo_activity_playground-0.42.0.dist-info → geo_activity_playground-0.43.0.dist-info}/RECORD +28 -24
  26. {geo_activity_playground-0.42.0.dist-info → geo_activity_playground-0.43.0.dist-info}/LICENSE +0 -0
  27. {geo_activity_playground-0.42.0.dist-info → geo_activity_playground-0.43.0.dist-info}/WHEEL +0 -0
  28. {geo_activity_playground-0.42.0.dist-info → geo_activity_playground-0.43.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,198 @@
1
+ import datetime
2
+ import pathlib
3
+ import uuid
4
+
5
+ import dateutil.parser
6
+ import exifread
7
+ import geojson
8
+ import sqlalchemy
9
+ from flask import Blueprint
10
+ from flask import redirect
11
+ from flask import render_template
12
+ from flask import request
13
+ from flask import Response
14
+ from flask import url_for
15
+ from PIL import Image
16
+ from PIL import ImageOps
17
+
18
+ from ...core.config import ConfigAccessor
19
+ from ...core.datamodel import Activity
20
+ from ...core.datamodel import DB
21
+ from ...core.datamodel import Photo
22
+ from ...core.paths import PHOTOS_DIR
23
+ from ..authenticator import Authenticator
24
+ from ..authenticator import needs_authentication
25
+ from ..flasher import Flasher
26
+ from ..flasher import FlashTypes
27
+
28
+
29
+ def ratio_to_decimal(numbers: list[exifread.utils.Ratio]) -> float:
30
+ deg, min, sec = numbers.values
31
+ return deg.decimal() + min.decimal() / 60 + sec.decimal() / 3600
32
+
33
+
34
+ def get_metadata_from_image(path: pathlib.Path) -> dict:
35
+ with open(path, "rb") as f:
36
+ tags = exifread.process_file(f)
37
+ metadata = {}
38
+ try:
39
+ metadata["latitude"] = ratio_to_decimal(tags["GPS GPSLatitude"])
40
+ metadata["longitude"] = ratio_to_decimal(tags["GPS GPSLongitude"])
41
+ except KeyError:
42
+ pass
43
+ try:
44
+ metadata["time"] = datetime.datetime.strptime(
45
+ str(tags["EXIF DateTimeOriginal"]), "%Y:%m:%d %H:%M:%S"
46
+ )
47
+ except KeyError:
48
+ pass
49
+
50
+ return metadata
51
+
52
+
53
+ def make_photo_blueprint(
54
+ config_accessor: ConfigAccessor, authenticator: Authenticator, flasher: Flasher
55
+ ) -> Blueprint:
56
+ blueprint = Blueprint("photo", __name__, template_folder="templates")
57
+
58
+ @blueprint.route("/get/<int:id>/<int:size>.webp")
59
+ def get(id: int, size: int) -> Response:
60
+ assert size < 5000
61
+ photo = DB.session.get_one(Photo, id)
62
+
63
+ original_path = PHOTOS_DIR() / "original" / photo.path
64
+ small_path = PHOTOS_DIR() / f"size-{size}" / photo.path.with_suffix(".webp")
65
+
66
+ if not small_path.exists():
67
+ with Image.open(original_path) as im:
68
+ target_size = (size, size)
69
+ im = ImageOps.contain(im, target_size)
70
+ small_path.parent.mkdir(exist_ok=True)
71
+ im.save(small_path)
72
+
73
+ with open(small_path, "rb") as f:
74
+ return Response(f.read(), mimetype="image/webp")
75
+
76
+ @blueprint.route("/map")
77
+ def map() -> str:
78
+ return render_template("photo/map.html.j2")
79
+
80
+ @blueprint.route("/map-for-all/photos.geojson")
81
+ def map_for_all() -> Response:
82
+ photos = DB.session.scalars(sqlalchemy.select(Photo)).all()
83
+ fc = geojson.FeatureCollection(
84
+ features=[
85
+ geojson.Feature(
86
+ geometry=geojson.Point((photo.longitude, photo.latitude)),
87
+ properties={
88
+ "photo_id": photo.id,
89
+ "url_marker": url_for(".get", id=photo.id, size=128),
90
+ "url_popup": url_for(".get", id=photo.id, size=512),
91
+ "url_full": url_for(".get", id=photo.id, size=4096),
92
+ },
93
+ )
94
+ for photo in photos
95
+ ]
96
+ )
97
+ return Response(
98
+ geojson.dumps(fc, sort_keys=True, indent=2, ensure_ascii=False),
99
+ mimetype="application/json",
100
+ )
101
+
102
+ @blueprint.route("/map-for-activity/<int:activity_id>/photos.geojson")
103
+ def map_for_activity(activity_id: int) -> Response:
104
+ activity = DB.session.get_one(Activity, activity_id)
105
+ fc = geojson.FeatureCollection(
106
+ features=[
107
+ geojson.Feature(
108
+ geometry=geojson.Point((photo.longitude, photo.latitude)),
109
+ properties={
110
+ "photo_id": photo.id,
111
+ "url_marker": url_for(".get", id=photo.id, size=128),
112
+ "url_popup": url_for(".get", id=photo.id, size=512),
113
+ "url_full": url_for(".get", id=photo.id, size=4096),
114
+ },
115
+ )
116
+ for photo in activity.photos
117
+ ]
118
+ )
119
+ return Response(
120
+ geojson.dumps(fc, sort_keys=True, indent=2, ensure_ascii=False),
121
+ mimetype="application/json",
122
+ )
123
+
124
+ @blueprint.route("/new", methods=["GET", "POST"])
125
+ @needs_authentication(authenticator)
126
+ def new() -> Response:
127
+ if request.method == "POST":
128
+ # check if the post request has the file part
129
+ if "file" not in request.files:
130
+ flasher.flash_message(
131
+ "No file could be found. Did you select a file?", FlashTypes.WARNING
132
+ )
133
+ return redirect(url_for(".new"))
134
+
135
+ file = request.files["file"]
136
+ # If the user does not select a file, the browser submits an
137
+ # empty file without a filename.
138
+ if file.filename == "":
139
+ flasher.flash_message("No selected file.", FlashTypes.WARNING)
140
+ return redirect(url_for(".new"))
141
+ if not file:
142
+ flasher.flash_message("Empty file uploaded.", FlashTypes.WARNING)
143
+ return redirect(url_for(".new"))
144
+
145
+ filename = str(uuid.uuid4()) + pathlib.Path(file.filename).suffix
146
+ path = PHOTOS_DIR() / "original" / filename
147
+ path.parent.mkdir(exist_ok=True)
148
+ file.save(path)
149
+ metadata = get_metadata_from_image(path)
150
+
151
+ if "time" not in metadata:
152
+ flasher.flash_message(
153
+ "Your image doesn't have the EXIF attribute 'EXIF DateTimeOriginal' and hence cannot be dated.",
154
+ FlashTypes.DANGER,
155
+ )
156
+ return redirect(url_for(".new"))
157
+ time: datetime.datetime = metadata["time"]
158
+
159
+ activity = DB.session.scalar(
160
+ sqlalchemy.select(Activity)
161
+ .where(
162
+ Activity.start.is_not(None),
163
+ Activity.elapsed_time.is_not(None),
164
+ Activity.start <= time,
165
+ )
166
+ .order_by(Activity.start.desc())
167
+ .limit(1)
168
+ )
169
+ if activity is None or activity.start + activity.elapsed_time < time:
170
+ flasher.flash_message(
171
+ f"Your image is from {time} but no activity could be found. Please first upload an activity or fix the time in the photo",
172
+ FlashTypes.DANGER,
173
+ )
174
+ print(activity)
175
+
176
+ if "latitude" not in metadata:
177
+ time_series = activity.time_series
178
+ print(time_series)
179
+ row = time_series.loc[time_series["time"] >= time].iloc[0]
180
+ metadata["latitude"] = row["latitude"]
181
+ metadata["longitude"] = row["longitude"]
182
+
183
+ photo = Photo(
184
+ filename=filename,
185
+ time=time,
186
+ latitude=metadata["latitude"],
187
+ longitude=metadata["longitude"],
188
+ activity=activity,
189
+ )
190
+
191
+ DB.session.add(photo)
192
+ DB.session.commit()
193
+
194
+ return redirect(f"/activity/{activity.id}")
195
+ else:
196
+ return render_template("photo/new.html.j2")
197
+
198
+ return blueprint
@@ -1,3 +1,5 @@
1
+ from typing import Optional
2
+
1
3
  from werkzeug.datastructures import MultiDict
2
4
 
3
5
  from ..core.config import ConfigAccessor
@@ -7,13 +9,20 @@ from .authenticator import Authenticator
7
9
 
8
10
 
9
11
  def search_query_from_form(args: MultiDict) -> SearchQuery:
10
- query = SearchQuery(
11
- equipment=args.getlist("equipment"),
12
- kind=args.getlist("kind"),
13
- name=args.get("name", None),
14
- name_case_sensitive=_parse_bool(args.get("name_case_sensitive", "false")),
15
- start_begin=_parse_date_or_none(args.get("start_begin", None)),
16
- start_end=_parse_date_or_none(args.get("start_end", None)),
12
+ query = SearchQuery.from_primitives(
13
+ {
14
+ "equipment": map(int, args.getlist("equipment")),
15
+ "kind": map(int, args.getlist("kind")),
16
+ "tag": map(int, args.getlist("tag")),
17
+ "name": args.get("name", None),
18
+ "name_case_sensitive": _parse_bool(
19
+ args.get("name_case_sensitive", "false")
20
+ ),
21
+ "start_begin": args.get("start_begin", None),
22
+ "start_end": args.get("start_end", None),
23
+ "distance_km_min": _optional_float(args.get("distance_km_min", None)),
24
+ "distance_km_max": _optional_float(args.get("distance_km_max", None)),
25
+ }
17
26
  )
18
27
 
19
28
  return query
@@ -23,6 +32,13 @@ def _parse_bool(s: str) -> bool:
23
32
  return s == "true"
24
33
 
25
34
 
35
+ def _optional_float(s: str) -> Optional[float]:
36
+ if s:
37
+ return float(s)
38
+ else:
39
+ return None
40
+
41
+
26
42
  class SearchQueryHistory:
27
43
  def __init__(
28
44
  self, config_accessor: ConfigAccessor, authenticator: Authenticator
@@ -10,14 +10,16 @@
10
10
  <div class="row mb-3">
11
11
  <div class="col-sm-12 col-md-4">
12
12
  <dl>
13
- <dt>Name</dt>
14
- <dd>{{ activity.name }}</dd>
15
-
16
13
  {% if activity.kind %}
17
14
  <dt>Kind</dt>
18
15
  <dd>{{ activity.kind.name }}</dd>
19
16
  {% endif %}
20
17
 
18
+ {% if activity.equipment %}
19
+ <dt>Equipment</dt>
20
+ <dd>{{ activity.equipment.name }}</dd>
21
+ {% endif %}
22
+
21
23
  {% if activity.tags %}
22
24
  <dt>Tags</dt>
23
25
  <dd>
@@ -61,11 +63,6 @@
61
63
  <dd>{{ activity.elevation_gain|round(0)|int }} m</dd>
62
64
  {% endif %}
63
65
 
64
- {% if activity.equipment %}
65
- <dt>Equipment</dt>
66
- <dd>{{ activity.equipment.name }}</dd>
67
- {% endif %}
68
-
69
66
  {% if new_tiles[14] %}
70
67
  <dt>New Explorer Tiles</dt>
71
68
  <dd>{{ new_tiles[14] }}</dd>
@@ -100,6 +97,30 @@
100
97
  style: function (feature) { return { color: feature.properties.color } }
101
98
  }).addTo(map)
102
99
  map.fitBounds(geojson.getBounds());
100
+
101
+
102
+
103
+ fetch("{{ url_for('photo.map_for_activity', activity_id = activity.id) }}")
104
+ .then(function (response) {
105
+ return response.json();
106
+ })
107
+ .then(function (data) {
108
+ L.geoJSON(data, {
109
+ pointToLayer: function (feature, latlng) {
110
+ return L.marker(latlng, {
111
+ icon: new L.Icon({
112
+ iconSize: [32, 32],
113
+ iconAnchor: [16, 16],
114
+ popupAnchor: [16, 0],
115
+ iconUrl: feature.properties.url_marker,
116
+ })
117
+ });
118
+ },
119
+ onEachFeature: function (feature, layer) {
120
+ layer.bindPopup(`<a href="${feature.properties.url_full}" target="_blank"><img src="${feature.properties.url_popup}" /></a>`)
121
+ }
122
+ }).addTo(map);
123
+ });
103
124
  </script>
104
125
 
105
126
 
@@ -135,8 +156,19 @@
135
156
  </div>
136
157
  </div>
137
158
 
138
-
159
+ {% if activity.photos %}
160
+ <h2 class="mb-3">Photos</h2>
139
161
  <div class="row mb-3">
162
+ {% for photo in activity.photos %}
163
+ <div class="col-md-3">
164
+ <img src="{{ url_for('photo.get', id=photo.id, size=512) }}" width="100%" />
165
+ </div>
166
+ {% endfor %}
167
+ </div>
168
+ {% endif %}
169
+
170
+
171
+ <div class=" row mb-3">
140
172
  <div class="col">
141
173
  <h2>Distance & speed</h2>
142
174
  </div>
@@ -212,7 +244,8 @@
212
244
 
213
245
  {% if new_tiles_geojson %}
214
246
  <h2>New explorer tiles</h2>
215
- <p>With this activity you have explored new explorer tiles. The following maps show the new tiles on the respective zoom
247
+ <p>With this activity you have explored new explorer tiles. The following maps show the new tiles on the respective
248
+ zoom
216
249
  levels.</p>
217
250
  <script>
218
251
  function add_map(id, geojson) {
@@ -1,11 +1,10 @@
1
1
  {% extends "page.html.j2" %}
2
- {% from "search_form.html.j2" import search_form %}
3
2
 
4
3
  {% block container %}
5
4
  <h1 class="mb-3">Eddington Number</h1>
6
5
 
7
6
  <div class="mb-3">
8
- {{ search_form(query, equipments_avail, kinds_avail, search_query_favorites, search_query_last, request_url) }}
7
+ {% include "search_form.html.j2" %}
9
8
  </div>
10
9
 
11
10
  <div class="row mb-3">
@@ -1,11 +1,10 @@
1
1
  {% extends "page.html.j2" %}
2
- {% from "search_form.html.j2" import search_form %}
3
2
 
4
3
  {% block container %}
5
4
  <h1 class="mb-3">Eddington Number for Elevation Gain</h1>
6
5
 
7
6
  <div class="mb-3">
8
- {{ search_form(query, equipments_avail, kinds_avail, search_query_favorites, search_query_last, request_url) }}
7
+ {% include "search_form.html.j2" %}
9
8
  </div>
10
9
 
11
10
  <div class="row mb-3">
@@ -1,11 +1,10 @@
1
1
  {% extends "page.html.j2" %}
2
- {% from "search_form.html.j2" import search_form %}
3
2
 
4
3
  {% block container %}
5
4
  <h1 class="mb-3">Eddington Number for Elevation Gain</h1>
6
5
 
7
6
  <div class="mb-3">
8
- {{ search_form(query, equipments_avail, kinds_avail, search_query_favorites, search_query_last, request_url) }}
7
+ {% include "search_form.html.j2" %}
9
8
  </div>
10
9
 
11
10
  <div class="row mb-3">
@@ -13,20 +12,21 @@
13
12
  <form method="GET">
14
13
  <div class="mb-3">
15
14
  <label class="form-label">Divisor</label>
16
- <select class="form-select" aria-label="Divisor" name="elevation_eddington_divisor" onchange="this.form.submit()">
15
+ <select class="form-select" aria-label="Divisor" name="elevation_eddington_divisor"
16
+ onchange="this.form.submit()">
17
17
  {% for dv in divisor_values_avail %}
18
18
  <option {% if dv==elevation_eddington_divisor %} selected {% endif %}>{{ dv }}</option>
19
19
  {% endfor %}
20
20
  {% for key, value in query.items() %}
21
- {% if value and key != "active" %}
22
- {% if value is sequence %}
23
- {% for v in value %}
24
- <input type="hidden" name="{{ key }}" value="{{ v }}"/>
25
- {% endfor %}
26
- {% else %}
27
- <input type="hidden" name="{{ key }}" value="{{ value }}"/>
28
- {% endif %}
29
- {% endif %}
21
+ {% if value and key != "active" %}
22
+ {% if value is sequence %}
23
+ {% for v in value %}
24
+ <input type="hidden" name="{{ key }}" value="{{ v }}" />
25
+ {% endfor %}
26
+ {% else %}
27
+ <input type="hidden" name="{{ key }}" value="{{ value }}" />
28
+ {% endif %}
29
+ {% endif %}
30
30
  {% endfor %}
31
31
  </select>
32
32
  </div>
@@ -37,9 +37,11 @@
37
37
  <p>Your Eddington number with divisor {{ elevation_eddington_divisor }} is <b>{{ eddington_number }}</b>.</p>
38
38
 
39
39
  <p>That means that on {{ (eddington_number / elevation_eddington_divisor) | int }} separate days you
40
- have recorded activities with an elevation gain of more than {{ eddington_number }} m. Going high is one thing, going often is
40
+ have recorded activities with an elevation gain of more than {{ eddington_number }} m. Going high is one
41
+ thing, going often is
41
42
  another. But going high often is hard. Also if you increment the Eddington number, all days with less
42
- elevation gain will not count any more. It becomes increasingly hard to increment the Eddington number because you
43
+ elevation gain will not count any more. It becomes increasingly hard to increment the Eddington number
44
+ because you
43
45
  don't only need to achieve a higher count, but all flatter activities don't count towards the bigger number.
44
46
  </p>
45
47
  </div>
@@ -74,7 +76,8 @@
74
76
  <div class="col">
75
77
  <h2>Plot</h2>
76
78
 
77
- <p>In a graphical representation, the Eddington number is the elevation gain where the red line intersects with the
79
+ <p>In a graphical representation, the Eddington number is the elevation gain where the red line intersects with
80
+ the
78
81
  blue area.</p>
79
82
 
80
83
  {{ vega_direct("logarithmic_plot", logarithmic_plot) }}
@@ -1,11 +1,10 @@
1
1
  {% extends "page.html.j2" %}
2
- {% from "search_form.html.j2" import search_form %}
3
2
 
4
3
  {% block container %}
5
4
  <h1 class="mb-3">Heatmap</h1>
6
5
 
7
6
  <div class="mb-3">
8
- {{ search_form(query, equipments_avail, kinds_avail, search_query_favorites, search_query_last, request_url) }}
7
+ {% include "search_form.html.j2" %}
9
8
  </div>
10
9
 
11
10
  <div class="row mb-3">
@@ -123,6 +123,12 @@
123
123
  </li>
124
124
  {% endif %}
125
125
 
126
+ {% if photo_count > 0 %}
127
+ <li class="nav-item">
128
+ <a class="nav-link" aria-current="page" href="{{ url_for('photo.map') }}">Photo Map</a>
129
+ </li>
130
+ {% endif %}
131
+
126
132
  <li class="nav-item dropdown">
127
133
  <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
128
134
  aria-expanded="false">
@@ -131,6 +137,8 @@
131
137
  <ul class="dropdown-menu">
132
138
  <li><a class="dropdown-item" href="{{ url_for('upload.index') }}">Upload Activities</a>
133
139
  </li>
140
+ <li><a class="dropdown-item" href="{{ url_for('photo.new') }}">Upload Photos</a>
141
+ </li>
134
142
  <li><a class="dropdown-item" href="{{ url_for('upload.reload') }}">Scan New
135
143
  Activities</a>
136
144
  </li>
@@ -0,0 +1,45 @@
1
+ {% extends "page.html.j2" %}
2
+
3
+ {% block container %}
4
+ <h1>Photo Map</h1>
5
+
6
+ <div id="photo-map" style="height: 800px;" class="mb-3"></div>
7
+ <script>
8
+ var map = L.map('photo-map', {
9
+ fullscreenControl: true
10
+ });
11
+ L.tileLayer('/tile/color/{z}/{x}/{y}.png', {
12
+ maxZoom: 19,
13
+ attribution: '{{ map_tile_attribution|safe }}'
14
+ }).addTo(map);
15
+
16
+ fetch("{{ url_for('photo.map_for_all') }}")
17
+ .then(function (response) {
18
+ return response.json();
19
+ })
20
+ .then(function (data) {
21
+ let layer = L.geoJSON(data, {
22
+ pointToLayer: function (feature, latlng) {
23
+ return L.marker(latlng, {
24
+ icon: new L.Icon({
25
+ iconSize: [32, 32],
26
+ iconAnchor: [16, 16],
27
+ popupAnchor: [16, 0],
28
+ iconUrl: feature.properties.url_marker,
29
+ })
30
+ });
31
+ },
32
+ onEachFeature: function (feature, layer) {
33
+ layer.bindPopup(`<a href="${feature.properties.url_full}" target="_blank"><img src="${feature.properties.url_popup}" /></a>`)
34
+ }
35
+ })
36
+
37
+ let group = L.markerClusterGroup()
38
+ group.addLayer(layer)
39
+
40
+ group.addTo(map);
41
+ map.fitBounds(group.getBounds());
42
+ });
43
+ </script>
44
+
45
+ {% endblock %}
@@ -0,0 +1,13 @@
1
+ {% extends "page.html.j2" %}
2
+
3
+ {% block container %}
4
+ <h1>Upload Photo</h1>
5
+
6
+ <form method="POST" enctype="multipart/form-data">
7
+ <div class="mb-3">
8
+ <label for="file" class="form-label">Photo file</label>
9
+ <input type="file" name="file" id="file" class="form-control">
10
+ </div>
11
+ <button type="submit" class="btn btn-primary">Upload</button>
12
+ </form>
13
+ {% endblock %}
@@ -1,12 +1,11 @@
1
1
  {% extends "page.html.j2" %}
2
- {% from "search_form.html.j2" import search_form %}
3
2
 
4
3
  {% block container %}
5
4
 
6
5
  <h1 class="row mb-3">Activities Overview & Search</h1>
7
6
 
8
7
  <div class="mb-3">
9
- {{ search_form(query, equipments_avail, kinds_avail, search_query_favorites, search_query_last, request_url) }}
8
+ {% include "search_form.html.j2" %}
10
9
  </div>
11
10
 
12
11
  <table class="table table-sort table-arrows">
@@ -1,4 +1,14 @@
1
- {% macro search_form(query, equipments_avail, kinds_avail, search_query_favorites, search_query_last, request_url) %}
1
+ {% macro _show_search_query(description, url_parameters, is_favorite, request_url) %}
2
+ <li>
3
+ <a href="?{{ url_parameters }}">{{ description }}</a>
4
+ {% if is_favorite %}
5
+ <a href="{{ url_for('search.delete_search_query') }}?{{ url_parameters }}&redirect={{ request_url }}">🗑️</a>
6
+ {% else %}
7
+ <a href="{{ url_for('search.save_search_query') }}?{{ url_parameters }}&redirect={{ request_url }}">💾</a>
8
+ {% endif %}
9
+ </li>
10
+ {% endmacro %}
11
+
2
12
  <div class="accordion" id="search_form_accordion">
3
13
  <div class="accordion-item">
4
14
  <h2 class="accordion-header">
@@ -41,16 +51,27 @@
41
51
  value="{{ query.start_end }}">
42
52
  </div>
43
53
 
54
+ <div class="col-6">
55
+ <label for="distance_km_min" class="form-label">Distance minimum</label>
56
+ <input type="number" class="form-control" id="distance_km_min"
57
+ name="distance_km_min" value="{{ query.distance_km_min }}">
58
+ </div>
59
+ <div class="col-6">
60
+ <label for="distance_km_max" class="form-label">Distance maximum</label>
61
+ <input type="number" class="form-control" id="distance_km_max"
62
+ name="distance_km_max" value="{{ query.distance_km_max }}">
63
+ </div>
64
+
44
65
  <div class="col-12">
45
66
  <label for="" class="form-label">Kind</label>
46
67
  <div class="form-control">
47
68
  {% for kind in kinds_avail %}
48
69
  <div class="form-check form-check-inline">
49
70
  <input class="form-check-input" type="checkbox" name="kind"
50
- value="{{ kind }}" id="kind_{{ kind }}" {% if kind in query.kind %}
51
- checked {% endif %}>
52
- <label class="form-check-label" for="kind_{{ kind }}">
53
- {{ kind }}
71
+ value="{{ kind.id }}" id="kind_{{ kind.id }}" {% if kind.id in
72
+ query.kind %} checked {% endif %}>
73
+ <label class="form-check-label" for="kind_{{ kind.id }}">
74
+ {{ kind.name }}
54
75
  </label>
55
76
  </div>
56
77
  {% endfor %}
@@ -63,10 +84,26 @@
63
84
  {% for equipment in equipments_avail %}
64
85
  <div class="form-check form-check-inline">
65
86
  <input class="form-check-input" type="checkbox" name="equipment"
66
- value="{{ equipment }}" id="equipment_{{ equipment }}" {% if equipment
67
- in query.equipment %} checked {% endif %}>
68
- <label class="form-check-label" for="equipment_{{ equipment }}">
69
- {{ equipment }}
87
+ value="{{ equipment.id }}" id="equipment_{{ equipment.id }}" {% if
88
+ equipment.id in query.equipment %} checked {% endif %}>
89
+ <label class="form-check-label" for="equipment_{{ equipment.id }}">
90
+ {{ equipment.name }}
91
+ </label>
92
+ </div>
93
+ {% endfor %}
94
+ </div>
95
+ </div>
96
+
97
+ <div class="col-12">
98
+ <label for="" class="form-label">Tags</label>
99
+ <div class="form-control">
100
+ {% for tag in tags_avail %}
101
+ <div class="form-check form-check-inline">
102
+ <input class="form-check-input" type="checkbox" name="tag"
103
+ value="{{ tag.id }}" id="tag_{{ tag.id }}" {% if tag.id in query.tag %}
104
+ checked {% endif %}>
105
+ <label class="form-check-label" for="tag_{{ tag.id }}">
106
+ {{ tag.tag }}
70
107
  </label>
71
108
  </div>
72
109
  {% endfor %}
@@ -101,16 +138,4 @@
101
138
  </div>
102
139
  </div>
103
140
  </div>
104
- </div>
105
- {% endmacro %}
106
-
107
- {% macro _show_search_query(description, url_parameters, is_favorite, request_url) %}
108
- <li>
109
- <a href="?{{ url_parameters }}">{{ description }}</a>
110
- {% if is_favorite %}
111
- <a href="{{ url_for('search.delete_search_query') }}?{{ url_parameters }}&redirect={{ request_url }}">🗑️</a>
112
- {% else %}
113
- <a href="{{ url_for('search.save_search_query') }}?{{ url_parameters }}&redirect={{ request_url }}">💾</a>
114
- {% endif %}
115
- </li>
116
- {% endmacro %}
141
+ </div>