geo-activity-playground 0.36.2__py3-none-any.whl → 0.37.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 (25) hide show
  1. geo_activity_playground/core/meta_search.py +117 -0
  2. geo_activity_playground/core/summary_stats.py +30 -0
  3. geo_activity_playground/core/test_meta_search.py +91 -0
  4. geo_activity_playground/core/test_summary_stats.py +108 -0
  5. geo_activity_playground/webui/app.py +9 -5
  6. geo_activity_playground/webui/eddington_blueprint.py +8 -3
  7. geo_activity_playground/webui/{equipment/controller.py → equipment_blueprint.py} +19 -41
  8. geo_activity_playground/webui/heatmap/blueprint.py +7 -29
  9. geo_activity_playground/webui/heatmap/heatmap_controller.py +45 -103
  10. geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +5 -37
  11. geo_activity_playground/webui/search_blueprint.py +6 -69
  12. geo_activity_playground/webui/search_util.py +31 -0
  13. geo_activity_playground/webui/summary_blueprint.py +15 -11
  14. geo_activity_playground/webui/templates/eddington/index.html.j2 +5 -0
  15. geo_activity_playground/webui/{equipment/templates → templates}/equipment/index.html.j2 +3 -5
  16. geo_activity_playground/webui/templates/search/index.html.j2 +30 -87
  17. geo_activity_playground/webui/templates/search_form.html.j2 +82 -0
  18. geo_activity_playground/webui/templates/summary/index.html.j2 +5 -1
  19. {geo_activity_playground-0.36.2.dist-info → geo_activity_playground-0.37.0.dist-info}/METADATA +1 -1
  20. {geo_activity_playground-0.36.2.dist-info → geo_activity_playground-0.37.0.dist-info}/RECORD +23 -19
  21. geo_activity_playground/webui/equipment/__init__.py +0 -0
  22. geo_activity_playground/webui/equipment/blueprint.py +0 -16
  23. {geo_activity_playground-0.36.2.dist-info → geo_activity_playground-0.37.0.dist-info}/LICENSE +0 -0
  24. {geo_activity_playground-0.36.2.dist-info → geo_activity_playground-0.37.0.dist-info}/WHEEL +0 -0
  25. {geo_activity_playground-0.36.2.dist-info → geo_activity_playground-0.37.0.dist-info}/entry_points.txt +0 -0
@@ -11,6 +11,8 @@ from PIL import ImageDraw
11
11
 
12
12
  from geo_activity_playground.core.activities import ActivityRepository
13
13
  from geo_activity_playground.core.config import Config
14
+ from geo_activity_playground.core.meta_search import apply_search_query
15
+ from geo_activity_playground.core.meta_search import SearchQuery
14
16
  from geo_activity_playground.core.raster_map import convert_to_grayscale
15
17
  from geo_activity_playground.core.raster_map import GeoBounds
16
18
  from geo_activity_playground.core.raster_map import get_sensible_zoom_level
@@ -48,12 +50,7 @@ class HeatmapController:
48
50
  "activities_per_tile"
49
51
  ]
50
52
 
51
- def render(
52
- self,
53
- kinds: list[int],
54
- date_start: Optional[datetime.date],
55
- date_end: Optional[datetime.date],
56
- ) -> dict:
53
+ def render(self, query: SearchQuery) -> dict:
57
54
  zoom = 14
58
55
  tiles = self.tile_histories[zoom]
59
56
  medians = tiles.median(skipna=True)
@@ -62,19 +59,6 @@ class HeatmapController:
62
59
  )
63
60
  cluster_state = self.tile_evolution_states[zoom]
64
61
 
65
- available_kinds = sorted(self._repository.meta["kind"].unique())
66
-
67
- if not kinds:
68
- kinds = list(range(len(available_kinds)))
69
-
70
- extra_args = []
71
- if date_start is not None:
72
- extra_args.append(f"date-start={date_start.isoformat()}")
73
- if date_end is not None:
74
- extra_args.append(f"date-end={date_end.isoformat()}")
75
- for kind in kinds:
76
- extra_args.append(f"kind={kind}")
77
-
78
62
  values = {
79
63
  "center": {
80
64
  "latitude": median_lat,
@@ -87,14 +71,9 @@ class HeatmapController:
87
71
  else {}
88
72
  ),
89
73
  },
90
- "kinds": kinds,
91
- "available_kinds": available_kinds,
92
- "extra_args": "&".join(extra_args),
74
+ "extra_args": query.to_url_str(),
75
+ "query": query.to_jinja(),
93
76
  }
94
- if date_start is not None:
95
- values["date_start"] = date_start.date().isoformat()
96
- if date_end is not None:
97
- values["date_end"] = date_end.date().isoformat()
98
77
 
99
78
  return values
100
79
 
@@ -103,16 +82,12 @@ class HeatmapController:
103
82
  x: int,
104
83
  y: int,
105
84
  z: int,
106
- kind: str,
107
- date_start: Optional[datetime.date],
108
- date_end: Optional[datetime.date],
85
+ query: SearchQuery,
109
86
  ) -> np.ndarray:
110
87
  tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
111
88
  tile_counts = np.zeros(tile_pixels, dtype=np.int32)
112
- if date_start is None and date_end is None:
113
- tile_count_cache_path = pathlib.Path(
114
- f"Cache/Heatmap/{kind}/{z}/{x}/{y}.npy"
115
- )
89
+ if not query.active:
90
+ tile_count_cache_path = pathlib.Path(f"Cache/Heatmap/{z}/{x}/{y}.npy")
116
91
  if tile_count_cache_path.exists():
117
92
  try:
118
93
  tile_counts = np.load(tile_count_cache_path)
@@ -124,52 +99,41 @@ class HeatmapController:
124
99
  tile_counts = np.zeros(tile_pixels, dtype=np.int32)
125
100
  tile_count_cache_path.parent.mkdir(parents=True, exist_ok=True)
126
101
  activity_ids = self.activities_per_tile[z].get((x, y), set())
127
- activity_ids_kind = set()
128
- for activity_id in activity_ids:
129
- activity = self._repository.get_activity_by_id(activity_id)
130
- if activity["kind"] == kind:
131
- activity_ids_kind.add(activity_id)
132
- if activity_ids_kind:
133
- with work_tracker(
134
- tile_count_cache_path.with_suffix(".json")
135
- ) as parsed_activities:
136
- if parsed_activities - activity_ids_kind:
137
- logger.warning(
138
- f"Resetting heatmap cache for {kind=}/{x=}/{y=}/{z=} because activities have been removed."
102
+
103
+ with work_tracker(
104
+ tile_count_cache_path.with_suffix(".json")
105
+ ) as parsed_activities:
106
+ if parsed_activities - activity_ids:
107
+ logger.warning(
108
+ f"Resetting heatmap cache for {x=}/{y=}/{z=} because activities have been removed."
109
+ )
110
+ tile_counts = np.zeros(tile_pixels, dtype=np.int32)
111
+ parsed_activities.clear()
112
+ for activity_id in activity_ids:
113
+ if activity_id in parsed_activities:
114
+ continue
115
+ parsed_activities.add(activity_id)
116
+ time_series = self._repository.get_time_series(activity_id)
117
+ for _, group in time_series.groupby("segment_id"):
118
+ xy_pixels = (
119
+ np.array([group["x"] * 2**z - x, group["y"] * 2**z - y]).T
120
+ * OSM_TILE_SIZE
139
121
  )
140
- tile_counts = np.zeros(tile_pixels, dtype=np.int32)
141
- parsed_activities.clear()
142
- for activity_id in activity_ids_kind:
143
- if activity_id in parsed_activities:
144
- continue
145
- parsed_activities.add(activity_id)
146
- time_series = self._repository.get_time_series(activity_id)
147
- for _, group in time_series.groupby("segment_id"):
148
- xy_pixels = (
149
- np.array(
150
- [group["x"] * 2**z - x, group["y"] * 2**z - y]
151
- ).T
152
- * OSM_TILE_SIZE
153
- )
154
- im = Image.new("L", tile_pixels)
155
- draw = ImageDraw.Draw(im)
156
- pixels = list(map(int, xy_pixels.flatten()))
157
- draw.line(pixels, fill=1, width=max(3, 6 * (z - 17)))
158
- aim = np.array(im)
159
- tile_counts += aim
160
- tmp_path = tile_count_cache_path.with_suffix(".tmp.npy")
161
- np.save(tmp_path, tile_counts)
162
- tile_count_cache_path.unlink(missing_ok=True)
163
- tmp_path.rename(tile_count_cache_path)
122
+ im = Image.new("L", tile_pixels)
123
+ draw = ImageDraw.Draw(im)
124
+ pixels = list(map(int, xy_pixels.flatten()))
125
+ draw.line(pixels, fill=1, width=max(3, 6 * (z - 17)))
126
+ aim = np.array(im)
127
+ tile_counts += aim
128
+ tmp_path = tile_count_cache_path.with_suffix(".tmp.npy")
129
+ np.save(tmp_path, tile_counts)
130
+ tile_count_cache_path.unlink(missing_ok=True)
131
+ tmp_path.rename(tile_count_cache_path)
164
132
  else:
133
+ activities = apply_search_query(self._repository.meta, query)
165
134
  activity_ids = self.activities_per_tile[z].get((x, y), set())
166
135
  for activity_id in activity_ids:
167
- activity = self._repository.get_activity_by_id(activity_id)
168
- if not activity["kind"] == kind:
169
- continue
170
- if date_start is not None and activity["start"] < date_start:
171
- continue
172
- if date_end is not None and date_end < activity["start"]:
136
+ if activity_id not in activities["id"]:
173
137
  continue
174
138
  time_series = self._repository.get_time_series(activity_id)
175
139
  for _, group in time_series.groupby("segment_id"):
@@ -190,16 +154,11 @@ class HeatmapController:
190
154
  x: int,
191
155
  y: int,
192
156
  z: int,
193
- kinds_ids: list[int],
194
- date_start: Optional[datetime.date],
195
- date_end: Optional[datetime.date],
157
+ query: SearchQuery,
196
158
  ) -> np.ndarray:
197
159
  tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
198
160
  tile_counts = np.zeros(tile_pixels)
199
- available_kinds = sorted(self._repository.meta["kind"].unique())
200
- for kind_id in kinds_ids:
201
- kind = available_kinds[kind_id]
202
- tile_counts += self._get_counts(x, y, z, kind, date_start, date_end)
161
+ tile_counts += self._get_counts(x, y, z, query)
203
162
 
204
163
  tile_counts = np.sqrt(tile_counts) / 5
205
164
  tile_counts[tile_counts > 1.0] = 1.0
@@ -217,32 +176,17 @@ class HeatmapController:
217
176
  ] + data_color[:, :, c]
218
177
  return map_tile
219
178
 
220
- def render_tile(
221
- self,
222
- x: int,
223
- y: int,
224
- z: int,
225
- kind_ids: list[int],
226
- date_start: Optional[datetime.date],
227
- date_end: Optional[datetime.date],
228
- ) -> bytes:
179
+ def render_tile(self, x: int, y: int, z: int, query: SearchQuery) -> bytes:
229
180
  f = io.BytesIO()
230
181
  pl.imsave(
231
182
  f,
232
- self._render_tile_image(x, y, z, kind_ids, date_start, date_end),
183
+ self._render_tile_image(x, y, z, query),
233
184
  format="png",
234
185
  )
235
186
  return bytes(f.getbuffer())
236
187
 
237
188
  def download_heatmap(
238
- self,
239
- north: float,
240
- east: float,
241
- south: float,
242
- west: float,
243
- kind_ids: list[int],
244
- date_start: Optional[datetime.date],
245
- date_end: Optional[datetime.date],
189
+ self, north: float, east: float, south: float, west: float, query: SearchQuery
246
190
  ) -> bytes:
247
191
  geo_bounds = GeoBounds(south, west, north, east)
248
192
  tile_bounds = get_sensible_zoom_level(geo_bounds, (4000, 4000))
@@ -265,9 +209,7 @@ class HeatmapController:
265
209
  i * OSM_TILE_SIZE : (i + 1) * OSM_TILE_SIZE,
266
210
  j * OSM_TILE_SIZE : (j + 1) * OSM_TILE_SIZE,
267
211
  :,
268
- ] = self._render_tile_image(
269
- x, y, tile_bounds.zoom, kind_ids, date_start, date_end
270
- )
212
+ ] = self._render_tile_image(x, y, tile_bounds.zoom, query)
271
213
 
272
214
  f = io.BytesIO()
273
215
  pl.imsave(f, background, format="png")
@@ -1,44 +1,12 @@
1
1
  {% extends "page.html.j2" %}
2
+ {% from "search_form.html.j2" import search_form %}
2
3
 
3
4
  {% block container %}
4
- <div class="row mb-3">
5
- <div class="col">
6
- <h1>Heatmap</h1>
7
- </div>
8
- </div>
9
-
10
- <form action="" method="GET">
11
-
12
- <div class="row mb-3">
13
- <label class="col-sm-2 col-form-label">Kinds</label>
5
+ <h1 class="mb-3">Heatmap</h1>
14
6
 
15
- <div class="col-sm-10">
16
- {% for kind in available_kinds %}
17
- <div class="form-check form-check-inline form-switch ">
18
- <input class="form-check-input" type="checkbox" role="switch" id="kind{{ loop.index0 }}" name="kind"
19
- value="{{ loop.index0 }}" {{ 'checked' if loop.index0 in kinds else '' }} />
20
- <label class="form-check-label" for="kind{{ loop.index0 }}">{{ kind }}</label>
21
- </div>
22
- {% endfor %}
23
- </div>
24
- </div>
25
-
26
- <div class="row mb-3">
27
- <label class="col-sm-2 col-form-label">Date range</label>
28
- <div class="col-sm-5">
29
- <input type="date" id="date-start" name="date-start" value="{{ date_start }}" />
30
- <label for="date-start" class="form-label">Start date</label>
31
- </div>
32
- <div class="col-sm-5">
33
- <input type="date" id="date-end" name="date-end" value="{{ date_end }}" />
34
- <label for="date-start" class="form-label">End date</label>
35
- </div>
36
- </div>
37
-
38
- <div class="mb-3">
39
- <button type="submit" class="btn btn-primary">Filter heatmap</button>
40
- </div>
41
- </form>
7
+ <div class="mb-3">
8
+ {{ search_form(query, equipments_avail, kinds_avail) }}
9
+ </div>
42
10
 
43
11
  <div class="row mb-3">
44
12
  <div class="col">
@@ -8,6 +8,9 @@ from flask import request
8
8
  from flask import Response
9
9
 
10
10
  from ..core.activities import ActivityRepository
11
+ from geo_activity_playground.core.meta_search import apply_search_query
12
+ from geo_activity_playground.core.meta_search import SearchQuery
13
+ from geo_activity_playground.webui.search_util import search_query_from_form
11
14
 
12
15
 
13
16
  def reduce_or(selections):
@@ -23,79 +26,13 @@ def make_search_blueprint(repository: ActivityRepository) -> Blueprint:
23
26
 
24
27
  @blueprint.route("/")
25
28
  def index():
26
- kinds_avail = repository.meta["kind"].unique()
27
- equipments_avail = repository.meta["equipment"].unique()
28
-
29
- print(request.args)
30
-
31
- activities = repository.meta
32
-
33
- if equipments := request.args.getlist("equipment"):
34
- selection = reduce_or(
35
- activities["equipment"] == equipment for equipment in equipments
36
- )
37
- activities = activities.loc[selection]
38
-
39
- if kinds := request.args.getlist("kind"):
40
- selection = reduce_or(activities["kind"] == kind for kind in kinds)
41
- activities = activities.loc[selection]
42
-
43
- name_exact = bool(request.args.get("name_exact", False))
44
- name_casing = bool(request.args.get("name_casing", False))
45
- if name := request.args.get("name", ""):
46
- if name_casing:
47
- haystack = activities["name"]
48
- needle = name
49
- else:
50
- haystack = activities["name"].str.lower()
51
- needle = name.lower()
52
- if name_exact:
53
- selection = haystack == needle
54
- else:
55
- selection = [needle in an for an in haystack]
56
- activities = activities.loc[selection]
57
-
58
- begin = request.args.get("begin", "")
59
- end = request.args.get("end", "")
60
-
61
- if begin:
62
- try:
63
- begin_dt = dateutil.parser.parse(begin)
64
- except ValueError:
65
- flash(
66
- f"Cannot parse date `{begin}`, please use a different format.",
67
- category="danger",
68
- )
69
- else:
70
- selection = begin_dt <= activities["start"]
71
- activities = activities.loc[selection]
72
-
73
- if end:
74
- try:
75
- end_dt = dateutil.parser.parse(end)
76
- except ValueError:
77
- flash(
78
- f"Cannot parse date `{end}`, please use a different format.",
79
- category="danger",
80
- )
81
- else:
82
- selection = activities["start"] < end_dt
83
- activities = activities.loc[selection]
84
-
85
- activities = activities.sort_values("start", ascending=False)
29
+ query = search_query_from_form(request.args)
30
+ activities = apply_search_query(repository.meta, query)
86
31
 
87
32
  return render_template(
88
33
  "search/index.html.j2",
89
34
  activities=list(activities.iterrows()),
90
- equipments=request.args.getlist("equipment"),
91
- equipments_avail=sorted(equipments_avail),
92
- kinds=request.args.getlist("kind"),
93
- kinds_avail=sorted(kinds_avail),
94
- name=name,
95
- name_exact=name_exact,
96
- name_casing=name_casing,
97
- begin=begin,
98
- end=end,
35
+ query=query.to_jinja(),
99
36
  )
100
37
 
101
38
  return blueprint
@@ -0,0 +1,31 @@
1
+ import datetime
2
+ from typing import Optional
3
+
4
+ import dateutil.parser
5
+ from werkzeug.datastructures import MultiDict
6
+
7
+ from geo_activity_playground.core.meta_search import SearchQuery
8
+
9
+
10
+ def search_query_from_form(args: MultiDict) -> SearchQuery:
11
+ query = SearchQuery(
12
+ equipment=args.getlist("equipment"),
13
+ kind=args.getlist("kind"),
14
+ name=args.get("name", None),
15
+ name_case_sensitive=_parse_bool(args.get("name_case_sensitive", "false")),
16
+ start_begin=_parse_date_or_none(args.get("start_begin", None)),
17
+ start_end=_parse_date_or_none(args.get("start_end", None)),
18
+ )
19
+
20
+ return query
21
+
22
+
23
+ def _parse_date_or_none(s: Optional[str]) -> Optional[datetime.date]:
24
+ if not s:
25
+ return None
26
+ else:
27
+ return dateutil.parser.parse(s).date()
28
+
29
+
30
+ def _parse_bool(s: str) -> bool:
31
+ return s == "true"
@@ -5,11 +5,14 @@ import altair as alt
5
5
  import pandas as pd
6
6
  from flask import Blueprint
7
7
  from flask import render_template
8
+ from flask import request
8
9
 
9
10
  from geo_activity_playground.core.activities import ActivityRepository
10
11
  from geo_activity_playground.core.activities import make_geojson_from_time_series
11
12
  from geo_activity_playground.core.config import Config
13
+ from geo_activity_playground.core.meta_search import apply_search_query
12
14
  from geo_activity_playground.webui.plot_util import make_kind_scale
15
+ from geo_activity_playground.webui.search_util import search_query_from_form
13
16
 
14
17
 
15
18
  def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Blueprint:
@@ -17,8 +20,11 @@ def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Bl
17
20
 
18
21
  @blueprint.route("/")
19
22
  def index():
23
+ query = search_query_from_form(request.args)
24
+ activities = apply_search_query(repository.meta, query)
25
+
20
26
  kind_scale = make_kind_scale(repository.meta, config)
21
- df = embellished_activities(repository.meta)
27
+ df = embellished_activities(activities)
22
28
  # df = df.loc[df["consider_for_achievements"]]
23
29
 
24
30
  year_kind_total = (
@@ -46,8 +52,9 @@ def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Bl
46
52
  repository.get_time_series(activity_id)
47
53
  ),
48
54
  )
49
- for activity_id, reasons in nominate_activities(repository.meta).items()
55
+ for activity_id, reasons in nominate_activities(df).items()
50
56
  ],
57
+ query=query.to_jinja(),
51
58
  )
52
59
 
53
60
  return blueprint
@@ -56,20 +63,18 @@ def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Bl
56
63
  def nominate_activities(meta: pd.DataFrame) -> dict[int, list[str]]:
57
64
  nominations: dict[int, list[str]] = collections.defaultdict(list)
58
65
 
59
- subset = meta.loc[meta["consider_for_achievements"]]
60
-
61
- i = subset["distance_km"].idxmax()
66
+ i = meta["distance_km"].idxmax()
62
67
  nominations[i].append(f"Greatest distance: {meta.loc[i].distance_km:.1f} km")
63
68
 
64
- i = subset["elapsed_time"].idxmax()
69
+ i = meta["elapsed_time"].idxmax()
65
70
  nominations[i].append(f"Longest elapsed time: {meta.loc[i].elapsed_time}")
66
71
 
67
- if "calories" in subset.columns and not pd.isna(subset["calories"]).all():
68
- i = subset["calories"].idxmax()
72
+ if "calories" in meta.columns and not pd.isna(meta["calories"]).all():
73
+ i = meta["calories"].idxmax()
69
74
  nominations[i].append(f"Most calories burnt: {meta.loc[i].calories:.0f} kcal")
70
75
 
71
- if "steps" in subset.columns and not pd.isna(subset["steps"]).all():
72
- i = subset["steps"].idxmax()
76
+ if "steps" in meta.columns and not pd.isna(meta["steps"]).all():
77
+ i = meta["steps"].idxmax()
73
78
  nominations[i].append(f"Most steps: {meta.loc[i].steps:.0f}")
74
79
 
75
80
  for kind, group in meta.groupby("kind"):
@@ -108,7 +113,6 @@ def embellished_activities(meta: pd.DataFrame) -> pd.DataFrame:
108
113
  df["hours"] = [
109
114
  elapsed_time.total_seconds() / 3600 for elapsed_time in df["elapsed_time"]
110
115
  ]
111
- del df["elapsed_time"]
112
116
  return df
113
117
 
114
118
 
@@ -1,8 +1,13 @@
1
1
  {% extends "page.html.j2" %}
2
+ {% from "search_form.html.j2" import search_form %}
2
3
 
3
4
  {% block container %}
4
5
  <h1 class="mb-3">Eddington Number</h1>
5
6
 
7
+ <div class="mb-3">
8
+ {{ search_form(query, equipments_avail, kinds_avail) }}
9
+ </div>
10
+
6
11
  <div class="row mb-3">
7
12
  <div class="col-md-4">
8
13
  <h2>Definition</h2>
@@ -13,7 +13,6 @@
13
13
  <thead>
14
14
  <tr>
15
15
  <th>Equipment</th>
16
- <th>Primarily used for</th>
17
16
  <th style="text-align: right;">Distance / km</th>
18
17
  <th style="text-align: right;">First use</th>
19
18
  <th style="text-align: right;">Last use</th>
@@ -23,10 +22,9 @@
23
22
  {% for equipment in equipment_summary %}
24
23
  <tr>
25
24
  <td>{{ equipment.equipment }}</td>
26
- <td>{{ equipment.primarily_used_for }}</td>
27
- <td style="text-align: right;">{{ equipment.total_distance_km|int }}</td>
28
- <td style="text-align: right;">{{ equipment.first_use.date() }}</td>
29
- <td style="text-align: right;">{{ equipment.last_use.date() }}</td>
25
+ <td style="text-align: right;">{{ equipment.total_distance_km }}</td>
26
+ <td style="text-align: right;">{{ equipment.first_use }}</td>
27
+ <td style="text-align: right;">{{ equipment.last_use }}</td>
30
28
  </tr>
31
29
  {% endfor %}
32
30
  </tbody>
@@ -1,95 +1,38 @@
1
1
  {% extends "page.html.j2" %}
2
+ {% from "search_form.html.j2" import search_form %}
2
3
 
3
4
  {% block container %}
4
5
 
5
6
  <h1 class="row mb-3">Activities Overview & Search</h1>
6
7
 
7
- <div class="row mb-3">
8
- <div class="col-md-2">
9
- <form>
10
- <div class="mb-3">
11
- <label for="name" class="form-label">Name</label>
12
- <input type="text" class="form-control" id="name" name="name" value="{{ name }}">
13
- <div class="form-check">
14
- <input class="form-check-input" type="checkbox" name="name_exact" value="true" id="name_exact" {% if
15
- name_exact %} checked {% endif %}>
16
- <label class="form-check-label" for="name_exact">
17
- Exact match
18
- </label>
19
- </div>
20
- <div class="form-check">
21
- <input class="form-check-input" type="checkbox" name="name_casing" value="true" id="name_casing" {%
22
- if name_casing %} checked {% endif %}>
23
- <label class="form-check-label" for="name_casing">
24
- Case sensitive
25
- </label>
26
- </div>
27
- </div>
28
-
29
- <div class="mb-3">
30
- <label for="begin" class="form-label">After</label>
31
- <input type="text" class="form-control" id="begin" name="begin" value="{{ begin }}">
32
- <label for="end" class="form-label">Until</label>
33
- <input type="text" class="form-control" id="end" name="end" value="{{ end }}">
34
- </div>
35
-
36
- <div class="mb-3">
37
- <label for="" class="form-label">Kind</label>
38
- {% for kind in kinds_avail %}
39
- <div class="form-check">
40
- <input class="form-check-input" type="checkbox" name="kind" value="{{ kind }}" id="kind_{{ kind }}"
41
- {% if kind in kinds %} checked {% endif %}>
42
- <label class="form-check-label" for="kind_{{ kind }}">
43
- {{ kind }}
44
- </label>
45
- </div>
46
- {% endfor %}
47
- </div>
48
-
49
- <div class="mb-3">
50
- <label for="" class="form-label">Equipment</label>
51
- {% for equipment in equipments_avail %}
52
- <div class="form-check">
53
- <input class="form-check-input" type="checkbox" name="equipment" value="{{ equipment }}"
54
- id="equipment_{{ equipment }}" {% if equipment in equipments %} checked {% endif %}>
55
- <label class="form-check-label" for="equipment_{{ equipment }}">
56
- {{ equipment }}
57
- </label>
58
- </div>
59
- {% endfor %}
60
- </div>
61
-
62
- <button type="submit" class="btn btn-primary">Search</button>
63
- </form>
64
- </div>
65
-
66
- <div class="col-md-10">
67
- <table class="table table-sort table-arrows">
68
- <thead>
69
- <tr>
70
- <th>Name</th>
71
- <th>Start</th>
72
- <th>Kind</th>
73
- <th class="numeric-sort">Distance</th>
74
- <th>Elapsed time</th>
75
- </tr>
76
- </thead>
77
- <tbody>
78
- {% for index, activity in activities %}
79
- <tr>
80
- <td><a href="{{ url_for('activity.show', id=activity['id']) }}">{{ activity['name'] }}</a></td>
81
- <td>
82
- {% if activity['start'] is defined %}
83
- {{ activity['start']|dt }}
84
- {% endif %}
85
- </td>
86
- <td>{{ activity['kind'] }}</td>
87
- <td>{{ '%.1f' % activity["distance_km"] }} km</td>
88
- <td>{{ activity.elapsed_time|td }}</td>
89
- </tr>
90
- {% endfor %}
91
- </tbody>
92
- </table>
93
- </div>
8
+ <div class="mb-3">
9
+ {{ search_form(query, equipments_avail, kinds_avail) }}
94
10
  </div>
11
+
12
+ <table class="table table-sort table-arrows">
13
+ <thead>
14
+ <tr>
15
+ <th>Name</th>
16
+ <th>Start</th>
17
+ <th>Kind</th>
18
+ <th class="numeric-sort">Distance</th>
19
+ <th>Elapsed time</th>
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ {% for index, activity in activities %}
24
+ <tr>
25
+ <td><a href="{{ url_for('activity.show', id=activity['id']) }}">{{ activity['name'] }}</a></td>
26
+ <td>
27
+ {% if activity['start'] is defined %}
28
+ {{ activity['start']|dt }}
29
+ {% endif %}
30
+ </td>
31
+ <td>{{ activity['kind'] }}</td>
32
+ <td>{{ '%.1f' % activity["distance_km"] }} km</td>
33
+ <td>{{ activity.elapsed_time|td }}</td>
34
+ </tr>
35
+ {% endfor %}
36
+ </tbody>
37
+ </table>
95
38
  {% endblock %}