geo-activity-playground 0.37.0__py3-none-any.whl → 0.38.1__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/core/activities.py +12 -0
  2. geo_activity_playground/core/config.py +6 -2
  3. geo_activity_playground/core/enrichment.py +9 -0
  4. geo_activity_playground/core/meta_search.py +49 -9
  5. geo_activity_playground/core/summary_stats.py +1 -1
  6. geo_activity_playground/core/test_meta_search.py +9 -0
  7. geo_activity_playground/webui/activity/controller.py +20 -0
  8. geo_activity_playground/webui/activity/templates/activity/day.html.j2 +3 -10
  9. geo_activity_playground/webui/activity/templates/activity/name.html.j2 +2 -0
  10. geo_activity_playground/webui/activity/templates/activity/show.html.j2 +17 -0
  11. geo_activity_playground/webui/app.py +18 -5
  12. geo_activity_playground/webui/eddington_blueprint.py +162 -58
  13. geo_activity_playground/webui/equipment_blueprint.py +12 -3
  14. geo_activity_playground/webui/explorer/blueprint.py +4 -0
  15. geo_activity_playground/webui/heatmap/blueprint.py +3 -0
  16. geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +1 -1
  17. geo_activity_playground/webui/search_blueprint.py +34 -2
  18. geo_activity_playground/webui/search_util.py +42 -9
  19. geo_activity_playground/webui/summary_blueprint.py +31 -34
  20. geo_activity_playground/webui/templates/eddington/index.html.j2 +69 -2
  21. geo_activity_playground/webui/templates/search/index.html.j2 +8 -4
  22. geo_activity_playground/webui/templates/search_form.html.j2 +91 -57
  23. geo_activity_playground/webui/templates/summary/index.html.j2 +1 -1
  24. {geo_activity_playground-0.37.0.dist-info → geo_activity_playground-0.38.1.dist-info}/METADATA +6 -6
  25. {geo_activity_playground-0.37.0.dist-info → geo_activity_playground-0.38.1.dist-info}/RECORD +28 -28
  26. {geo_activity_playground-0.37.0.dist-info → geo_activity_playground-0.38.1.dist-info}/LICENSE +0 -0
  27. {geo_activity_playground-0.37.0.dist-info → geo_activity_playground-0.38.1.dist-info}/WHEEL +0 -0
  28. {geo_activity_playground-0.37.0.dist-info → geo_activity_playground-0.38.1.dist-info}/entry_points.txt +0 -0
@@ -6,6 +6,7 @@ from flask import render_template
6
6
  from geo_activity_playground.core.activities import ActivityRepository
7
7
  from geo_activity_playground.core.config import Config
8
8
  from geo_activity_playground.core.summary_stats import get_equipment_use_table
9
+ from geo_activity_playground.webui.plot_util import make_kind_scale
9
10
 
10
11
 
11
12
  def make_equipment_blueprint(
@@ -20,7 +21,7 @@ def make_equipment_blueprint(
20
21
  )
21
22
 
22
23
  equipment_variables = {}
23
- for equipment in repository.meta["equipment"].unique():
24
+ for equipment in equipment_summary["equipment"]:
24
25
  selection = repository.meta.loc[repository.meta["equipment"] == equipment]
25
26
  total_distances = pd.DataFrame(
26
27
  {
@@ -55,6 +56,11 @@ def make_equipment_blueprint(
55
56
  .encode(
56
57
  alt.X("year(start):O", title="Year"),
57
58
  alt.Y("sum(distance_km)", title="Distance / km"),
59
+ alt.Color(
60
+ "kind",
61
+ scale=make_kind_scale(repository.meta, config),
62
+ title="Kind",
63
+ ),
58
64
  )
59
65
  .to_json(format="vega")
60
66
  )
@@ -67,7 +73,10 @@ def make_equipment_blueprint(
67
73
  )
68
74
  .mark_bar()
69
75
  .encode(
70
- alt.X("kind", title="Kind"),
76
+ alt.X(
77
+ "kind",
78
+ title="Kind",
79
+ ),
71
80
  alt.Y("sum(distance_km)", title="Distance / km"),
72
81
  )
73
82
  .to_json(format="vega")
@@ -84,7 +93,7 @@ def make_equipment_blueprint(
84
93
 
85
94
  variables = {
86
95
  "equipment_variables": equipment_variables,
87
- "equipment_summary": equipment_summary,
96
+ "equipment_summary": equipment_summary.to_dict(orient="records"),
88
97
  }
89
98
 
90
99
  return render_template("equipment/index.html.j2", **variables)
@@ -4,11 +4,14 @@ from flask import render_template
4
4
  from flask import Response
5
5
  from flask import url_for
6
6
 
7
+ from geo_activity_playground.webui.authenticator import Authenticator
8
+ from geo_activity_playground.webui.authenticator import needs_authentication
7
9
  from geo_activity_playground.webui.explorer.controller import ExplorerController
8
10
 
9
11
 
10
12
  def make_explorer_blueprint(
11
13
  explorer_controller: ExplorerController,
14
+ authenticator: Authenticator,
12
15
  ) -> Blueprint:
13
16
  blueprint = Blueprint("explorer", __name__, template_folder="templates")
14
17
 
@@ -19,6 +22,7 @@ def make_explorer_blueprint(
19
22
  )
20
23
 
21
24
  @blueprint.route("/enable-zoom-level/<zoom>")
25
+ @needs_authentication(authenticator)
22
26
  def enable_zoom_level(zoom: str):
23
27
  explorer_controller.enable_zoom_level(int(zoom))
24
28
  return redirect(url_for(".map", zoom=zoom))
@@ -9,12 +9,14 @@ from geo_activity_playground.core.config import Config
9
9
  from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
10
10
  from geo_activity_playground.webui.heatmap.heatmap_controller import HeatmapController
11
11
  from geo_activity_playground.webui.search_util import search_query_from_form
12
+ from geo_activity_playground.webui.search_util import SearchQueryHistory
12
13
 
13
14
 
14
15
  def make_heatmap_blueprint(
15
16
  repository: ActivityRepository,
16
17
  tile_visit_accessor: TileVisitAccessor,
17
18
  config: Config,
19
+ search_query_history: SearchQueryHistory,
18
20
  ) -> Blueprint:
19
21
  heatmap_controller = HeatmapController(repository, tile_visit_accessor, config)
20
22
  blueprint = Blueprint("heatmap", __name__, template_folder="templates")
@@ -22,6 +24,7 @@ def make_heatmap_blueprint(
22
24
  @blueprint.route("/")
23
25
  def index():
24
26
  query = search_query_from_form(request.args)
27
+ search_query_history.register_query(query)
25
28
  return render_template(
26
29
  "heatmap/index.html.j2", **heatmap_controller.render(query)
27
30
  )
@@ -5,7 +5,7 @@
5
5
  <h1 class="mb-3">Heatmap</h1>
6
6
 
7
7
  <div class="mb-3">
8
- {{ search_form(query, equipments_avail, kinds_avail) }}
8
+ {{ search_form(query, equipments_avail, kinds_avail, search_query_favorites, search_query_last, request_url) }}
9
9
  </div>
10
10
 
11
11
  <div class="row mb-3">
@@ -1,16 +1,22 @@
1
+ import urllib.parse
1
2
  from functools import reduce
2
3
 
3
4
  import dateutil.parser
4
5
  from flask import Blueprint
5
6
  from flask import flash
7
+ from flask import redirect
6
8
  from flask import render_template
7
9
  from flask import request
8
10
  from flask import Response
9
11
 
10
12
  from ..core.activities import ActivityRepository
13
+ from geo_activity_playground.core.config import ConfigAccessor
11
14
  from geo_activity_playground.core.meta_search import apply_search_query
12
15
  from geo_activity_playground.core.meta_search import SearchQuery
16
+ from geo_activity_playground.webui.authenticator import Authenticator
17
+ from geo_activity_playground.webui.authenticator import needs_authentication
13
18
  from geo_activity_playground.webui.search_util import search_query_from_form
19
+ from geo_activity_playground.webui.search_util import SearchQueryHistory
14
20
 
15
21
 
16
22
  def reduce_or(selections):
@@ -21,18 +27,44 @@ def reduce_and(selections):
21
27
  return reduce(lambda a, b: a & b, selections)
22
28
 
23
29
 
24
- def make_search_blueprint(repository: ActivityRepository) -> Blueprint:
30
+ def make_search_blueprint(
31
+ repository: ActivityRepository,
32
+ search_query_history: SearchQueryHistory,
33
+ authenticator: Authenticator,
34
+ config_accessor: ConfigAccessor,
35
+ ) -> Blueprint:
25
36
  blueprint = Blueprint("search", __name__, template_folder="templates")
26
37
 
27
38
  @blueprint.route("/")
28
39
  def index():
29
40
  query = search_query_from_form(request.args)
41
+ search_query_history.register_query(query)
30
42
  activities = apply_search_query(repository.meta, query)
31
43
 
32
44
  return render_template(
33
45
  "search/index.html.j2",
34
- activities=list(activities.iterrows()),
46
+ activities=reversed(list(activities.iterrows())),
35
47
  query=query.to_jinja(),
36
48
  )
37
49
 
50
+ @blueprint.route("/save-search-query")
51
+ @needs_authentication(authenticator)
52
+ def save_search_query():
53
+ query = search_query_from_form(request.args)
54
+ primitives = query.to_primitives()
55
+ if primitives not in config_accessor().search_queries_favorites:
56
+ config_accessor().search_queries_favorites.append(primitives)
57
+ config_accessor.save()
58
+ return redirect(urllib.parse.unquote_plus(request.args["redirect"]))
59
+
60
+ @blueprint.route("/delete-search-query")
61
+ @needs_authentication(authenticator)
62
+ def delete_search_query():
63
+ query = search_query_from_form(request.args)
64
+ primitives = query.to_primitives()
65
+ if primitives in config_accessor().search_queries_favorites:
66
+ config_accessor().search_queries_favorites.remove(primitives)
67
+ config_accessor.save()
68
+ return redirect(urllib.parse.unquote_plus(request.args["redirect"]))
69
+
38
70
  return blueprint
@@ -1,10 +1,11 @@
1
- import datetime
2
1
  from typing import Optional
3
2
 
4
- import dateutil.parser
5
3
  from werkzeug.datastructures import MultiDict
6
4
 
5
+ from geo_activity_playground.core.config import ConfigAccessor
6
+ from geo_activity_playground.core.meta_search import _parse_date_or_none
7
7
  from geo_activity_playground.core.meta_search import SearchQuery
8
+ from geo_activity_playground.webui.authenticator import Authenticator
8
9
 
9
10
 
10
11
  def search_query_from_form(args: MultiDict) -> SearchQuery:
@@ -20,12 +21,44 @@ def search_query_from_form(args: MultiDict) -> SearchQuery:
20
21
  return query
21
22
 
22
23
 
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
24
  def _parse_bool(s: str) -> bool:
31
25
  return s == "true"
26
+
27
+
28
+ class SearchQueryHistory:
29
+ def __init__(
30
+ self, config_accessor: ConfigAccessor, authenticator: Authenticator
31
+ ) -> None:
32
+ self._config_accessor = config_accessor
33
+ self._authenticator = authenticator
34
+
35
+ def register_query(self, search_query: SearchQuery) -> None:
36
+ if not self._authenticator.is_authenticated():
37
+ return
38
+
39
+ if not search_query.active:
40
+ return
41
+
42
+ primitives = search_query.to_primitives()
43
+ while primitives in self._config_accessor().search_queries_last:
44
+ self._config_accessor().search_queries_last.remove(primitives)
45
+ self._config_accessor().search_queries_last.append(primitives)
46
+ while (
47
+ len(self._config_accessor().search_queries_last)
48
+ > self._config_accessor().search_queries_num_keep
49
+ ):
50
+ self._config_accessor().search_queries_last.pop(0)
51
+ self._config_accessor.save()
52
+
53
+ def prepare_favorites(self) -> list[dict]:
54
+ return self._prepare_list(self._config_accessor().search_queries_favorites)
55
+
56
+ def prepare_last(self) -> list[dict]:
57
+ return self._prepare_list(self._config_accessor().search_queries_last)
58
+
59
+ def _prepare_list(self, l: list[dict]) -> list[tuple[str, dict]]:
60
+ result = []
61
+ for elem in l:
62
+ search_query = SearchQuery.from_primitives(elem)
63
+ result.append((str(search_query), search_query.to_url_str()))
64
+ return result
@@ -13,14 +13,20 @@ from geo_activity_playground.core.config import Config
13
13
  from geo_activity_playground.core.meta_search import apply_search_query
14
14
  from geo_activity_playground.webui.plot_util import make_kind_scale
15
15
  from geo_activity_playground.webui.search_util import search_query_from_form
16
+ from geo_activity_playground.webui.search_util import SearchQueryHistory
16
17
 
17
18
 
18
- def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Blueprint:
19
+ def make_summary_blueprint(
20
+ repository: ActivityRepository,
21
+ config: Config,
22
+ search_query_history: SearchQueryHistory,
23
+ ) -> Blueprint:
19
24
  blueprint = Blueprint("summary", __name__, template_folder="templates")
20
25
 
21
26
  @blueprint.route("/")
22
27
  def index():
23
28
  query = search_query_from_form(request.args)
29
+ search_query_history.register_query(query)
24
30
  activities = apply_search_query(repository.meta, query)
25
31
 
26
32
  kind_scale = make_kind_scale(repository.meta, config)
@@ -63,44 +69,35 @@ def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Bl
63
69
  def nominate_activities(meta: pd.DataFrame) -> dict[int, list[str]]:
64
70
  nominations: dict[int, list[str]] = collections.defaultdict(list)
65
71
 
66
- i = meta["distance_km"].idxmax()
67
- nominations[i].append(f"Greatest distance: {meta.loc[i].distance_km:.1f} km")
72
+ _nominate_activities_inner(meta, "", nominations)
68
73
 
69
- i = meta["elapsed_time"].idxmax()
70
- nominations[i].append(f"Longest elapsed time: {meta.loc[i].elapsed_time}")
74
+ for kind, group in meta.groupby("kind"):
75
+ _nominate_activities_inner(group, f" for {kind}", nominations)
76
+ for equipment, group in meta.groupby("equipment"):
77
+ _nominate_activities_inner(group, f" with {equipment}", nominations)
71
78
 
72
- if "calories" in meta.columns and not pd.isna(meta["calories"]).all():
73
- i = meta["calories"].idxmax()
74
- nominations[i].append(f"Most calories burnt: {meta.loc[i].calories:.0f} kcal")
79
+ return nominations
75
80
 
76
- if "steps" in meta.columns and not pd.isna(meta["steps"]).all():
77
- i = meta["steps"].idxmax()
78
- nominations[i].append(f"Most steps: {meta.loc[i].steps:.0f}")
79
81
 
80
- for kind, group in meta.groupby("kind"):
81
- for key, text in [
82
- (
83
- "distance_km",
84
- lambda row: f"Greatest distance for {row.kind}: {row.distance_km:.1f} km",
85
- ),
86
- (
87
- "elapsed_time",
88
- lambda row: f"Longest elapsed time for {row.kind}: {row.elapsed_time}",
89
- ),
90
- (
91
- "calories",
92
- lambda row: f"Most calories burnt for {row.kind}: {row.calories:.0f} kcal",
93
- ),
94
- ("steps", lambda row: f"Most steps for {row.kind}: {row.steps:.0f}"),
95
- ]:
96
- if key in group.columns:
97
- series = group[key]
98
- if not pd.isna(series).all():
99
- i = series.idxmax()
100
- if not pd.isna(i):
101
- nominations[i].append(text(meta.loc[i]))
82
+ def _nominate_activities_inner(
83
+ meta: pd.DataFrame, title_suffix: str, nominations: dict[int, list[str]]
84
+ ) -> None:
85
+ ratings = [
86
+ ("distance_km", "Greatest distance", "{:.1f} km"),
87
+ ("elapsed_time", "Longest elapsed time", "{}"),
88
+ ("average_speed_moving_kmh", "Highest average moving speed", "{:.1f} km/h"),
89
+ ("average_speed_elapsed_kmh", "Highest average elapsed speed", "{:.1f} km/h"),
90
+ ("calories", "Most calories burnt", "{:.0f}"),
91
+ ("steps", "Most steps", "{:.0f}"),
92
+ ("elevation_gain", "Largest elevation gain", "{:.0f} m"),
93
+ ]
102
94
 
103
- return nominations
95
+ for variable, title, format_str in ratings:
96
+ if variable in meta.columns and not pd.isna(meta[variable]).all():
97
+ i = meta[variable].idxmax()
98
+ value = meta.loc[i, variable]
99
+ format_applied = format_str.format(value)
100
+ nominations[i].append(f"{title}{title_suffix}: {format_applied}")
104
101
 
105
102
 
106
103
  def embellished_activities(meta: pd.DataFrame) -> pd.DataFrame:
@@ -5,7 +5,7 @@
5
5
  <h1 class="mb-3">Eddington Number</h1>
6
6
 
7
7
  <div class="mb-3">
8
- {{ search_form(query, equipments_avail, kinds_avail) }}
8
+ {{ search_form(query, equipments_avail, kinds_avail, search_query_favorites, search_query_last, request_url) }}
9
9
  </div>
10
10
 
11
11
  <div class="row mb-3">
@@ -55,7 +55,74 @@
55
55
  <p>In a graphical representation, the Eddington number is the distance where the red line intersects with the
56
56
  blue area.</p>
57
57
 
58
- {{ vega_direct("eddington-log", logarithmic_plot) }}
58
+ {{ vega_direct("logarithmic_plot", logarithmic_plot) }}
59
59
  </div>
60
60
  </div>
61
+
62
+ <div class="mb-3">
63
+ <h2 class="mb-3">Eddington number history</h2>
64
+
65
+ <p>How did the Eddington number evolve over time?</p>
66
+
67
+ {{ vega_direct("eddington_number_history_plot", eddington_number_history_plot) }}
68
+ </div>
69
+
70
+ <div class="mb-3">
71
+ <h2 class="mb-3">Yearly Eddington number</h2>
72
+
73
+ <p>If we only consider the activities within one calendar year for the Eddington number, we get the following:</p>
74
+
75
+ <table class="table">
76
+ <thead>
77
+ <tr>
78
+ <th>Year</th>
79
+ <th>Eddington number</th>
80
+ </tr>
81
+ </thead>
82
+ <tbody>
83
+ {% for year, eddington in yearly_eddington.items() %}
84
+ <tr>
85
+ <td>{{ year }}</td>
86
+ <td>{{ eddington }}</td>
87
+ </tr>
88
+ {% endfor %}
89
+ </tbody>
90
+ </table>
91
+ </div>
92
+
93
+
94
+ <h2 class="mb-3">Eddington number per Week</h2>
95
+
96
+ <div class="row mb-3">
97
+ <div class="col-md-8">
98
+ {{ vega_direct("eddington_per_week_plot", eddington_per_week_plot) }}
99
+ </div>
100
+
101
+ <div class="col-md-4">
102
+ <table class="table">
103
+ <thead>
104
+ <tr>
105
+ <th>Distance / km</th>
106
+ <th>Count</th>
107
+ <th>Missing weeks</th>
108
+ </tr>
109
+ </thead>
110
+ <tbody>
111
+ {% for row in eddington_table_weeks %}
112
+ <tr>
113
+ <td>{{ row['distance_km'] }}</td>
114
+ <td>{{ row['total'] }}</td>
115
+ <td>{{ row['missing'] }}</td>
116
+ </tr>
117
+ {% endfor %}
118
+ </tbody>
119
+ </table>
120
+ </div>
121
+ </div>
122
+
123
+ <div class="row mb-1">
124
+
125
+ </div>
126
+
127
+
61
128
  {% endblock %}
@@ -6,17 +6,19 @@
6
6
  <h1 class="row mb-3">Activities Overview & Search</h1>
7
7
 
8
8
  <div class="mb-3">
9
- {{ search_form(query, equipments_avail, kinds_avail) }}
9
+ {{ search_form(query, equipments_avail, kinds_avail, search_query_favorites, search_query_last, request_url) }}
10
10
  </div>
11
11
 
12
12
  <table class="table table-sort table-arrows">
13
13
  <thead>
14
14
  <tr>
15
15
  <th>Name</th>
16
- <th>Start</th>
17
- <th>Kind</th>
16
+ <th>Date</th>
18
17
  <th class="numeric-sort">Distance</th>
19
18
  <th>Elapsed time</th>
19
+ <th>Speed / km/h</th>
20
+ <th>Equipment</th>
21
+ <th>Kind</th>
20
22
  </tr>
21
23
  </thead>
22
24
  <tbody>
@@ -28,9 +30,11 @@
28
30
  {{ activity['start']|dt }}
29
31
  {% endif %}
30
32
  </td>
31
- <td>{{ activity['kind'] }}</td>
32
33
  <td>{{ '%.1f' % activity["distance_km"] }} km</td>
33
34
  <td>{{ activity.elapsed_time|td }}</td>
35
+ <td>{{ activity.average_speed_moving_kmh|round(1) }}</td>
36
+ <td>{{ activity["equipment"] }}</td>
37
+ <td>{{ activity['kind'] }}</td>
34
38
  </tr>
35
39
  {% endfor %}
36
40
  </tbody>
@@ -1,5 +1,4 @@
1
- {% macro search_form(query, equipments_avail, kinds_avail) %}
2
-
1
+ {% macro search_form(query, equipments_avail, kinds_avail, search_query_favorites, search_query_last, request_url) %}
3
2
  <div class="accordion" id="search_form_accordion">
4
3
  <div class="accordion-item">
5
4
  <h2 class="accordion-header">
@@ -12,71 +11,106 @@
12
11
  <div id="search_form" class="accordion-collapse collapse {{ 'show' if query.active else '' }}"
13
12
  data-bs-parent="#search_form_accordion">
14
13
  <div class="accordion-body">
15
- <form>
16
- <div class="row g-3">
17
- <div class="col-8">
18
- <label for="name" class="form-label">Name</label>
19
- <input type="text" class="form-control" id="name" name="name" value="{{ query.name }}">
20
- </div>
21
- <div class="col-4">
22
- <div class="form-check">
23
- <input class="form-check-input" type="checkbox" name="name_case_sensitive" value="true"
24
- id="name_case_sensitive" {% if query.name_case_sensitive %} checked {% endif %}>
25
- <label class="form-check-label" for="name_case_sensitive">
26
- Case sensitive
27
- </label>
28
- </div>
29
- </div>
14
+ <div class="row">
15
+ <div class="col-8">
16
+ <form>
17
+ <div class="row g-3">
18
+ <div class="col-12">
19
+ <label for="name" class="form-label">Name</label>
20
+ <input type="text" class="form-control" id="name" name="name"
21
+ value="{{ query.name }}">
30
22
 
31
- <div class="col-6">
32
- <label for="start_begin" class="form-label">After</label>
33
- <input type="date" class="form-control" id="start_begin" name="start_begin"
34
- value="{{ query.start_begin }}">
35
- </div>
36
- <div class="col-6">
37
- <label for="start_end" class="form-label">Until</label>
38
- <input type="date" class="form-control" id="start_end" name="start_end"
39
- value="{{ query.start_end }}">
40
- </div>
23
+ <div class="form-check">
24
+ <input class="form-check-input" type="checkbox" name="name_case_sensitive"
25
+ value="true" id="name_case_sensitive" {% if query.name_case_sensitive %}
26
+ checked {% endif %}>
27
+ <label class="form-check-label" for="name_case_sensitive">
28
+ Case sensitive
29
+ </label>
30
+ </div>
31
+ </div>
41
32
 
42
- <div class="col-12">
43
- <label for="" class="form-label">Kind</label>
44
- <div class="form-control">
45
- {% for kind in kinds_avail %}
46
- <div class="form-check form-check-inline">
47
- <input class="form-check-input" type="checkbox" name="kind" value="{{ kind }}"
48
- id="kind_{{ kind }}" {% if kind in query.kind %} checked {% endif %}>
49
- <label class="form-check-label" for="kind_{{ kind }}">
50
- {{ kind }}
51
- </label>
33
+ <div class="col-6">
34
+ <label for="start_begin" class="form-label">After</label>
35
+ <input type="date" class="form-control" id="start_begin" name="start_begin"
36
+ value="{{ query.start_begin }}">
37
+ </div>
38
+ <div class="col-6">
39
+ <label for="start_end" class="form-label">Until</label>
40
+ <input type="date" class="form-control" id="start_end" name="start_end"
41
+ value="{{ query.start_end }}">
42
+ </div>
43
+
44
+ <div class="col-12">
45
+ <label for="" class="form-label">Kind</label>
46
+ <div class="form-control">
47
+ {% for kind in kinds_avail %}
48
+ <div class="form-check form-check-inline">
49
+ <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 }}
54
+ </label>
55
+ </div>
56
+ {% endfor %}
57
+ </div>
58
+ </div>
59
+
60
+ <div class="col-12">
61
+ <label for="" class="form-label">Equipment</label>
62
+ <div class="form-control">
63
+ {% for equipment in equipments_avail %}
64
+ <div class="form-check form-check-inline">
65
+ <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 }}
70
+ </label>
71
+ </div>
72
+ {% endfor %}
73
+ </div>
52
74
  </div>
53
- {% endfor %}
54
- </div>
55
- </div>
56
75
 
57
- <div class="col-12">
58
- <label for="" class="form-label">Equipment</label>
59
- <div class="form-control">
60
- {% for equipment in equipments_avail %}
61
- <div class="form-check form-check-inline">
62
- <input class="form-check-input" type="checkbox" name="equipment"
63
- value="{{ equipment }}" id="equipment_{{ equipment }}" {% if equipment in
64
- query.equipment %} checked {% endif %}>
65
- <label class="form-check-label" for="equipment_{{ equipment }}">
66
- {{ equipment }}
67
- </label>
76
+ <div class="col-6">
77
+ <button type="submit" class="btn btn-primary">Filter</button>
78
+ </div>
79
+ <div class="col-6">
80
+ <a class="btn btn-danger" href="?">Reset</a>
68
81
  </div>
69
- {% endfor %}
70
82
  </div>
71
- </div>
83
+ </form>
84
+ </div>
85
+ <div class="col-4">
86
+ <h5 class="mb-3">Favorite queries</h5>
87
+ <ul class="mb-3">
88
+ {% for description, url_parameters in search_query_favorites %}
89
+ {{ _show_search_query(description, url_parameters, True, request_url) }}
90
+ {% endfor %}
91
+ </ul>
72
92
 
73
- <div class="col-12">
74
- <button type="submit" class="btn btn-primary">Filter</button>
75
- </div>
93
+ <h5 class="mb-3">Last queries</h5>
94
+ <ol class="mb-3">
95
+ {% for description, url_parameters in search_query_last %}
96
+ {{ _show_search_query(description, url_parameters, False, request_url) }}
97
+ {% endfor %}
98
+ </ol>
76
99
  </div>
77
- </form>
100
+ </div>
78
101
  </div>
79
102
  </div>
80
103
  </div>
81
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>
82
116
  {% endmacro %}
@@ -6,7 +6,7 @@
6
6
  <h1>Summary Statistics</h1>
7
7
 
8
8
  <div class="mb-3">
9
- {{ search_form(query, equipments_avail, kinds_avail) }}
9
+ {{ search_form(query, equipments_avail, kinds_avail, search_query_favorites, search_query_last, request_url) }}
10
10
  </div>
11
11
 
12
12
  <h2>Distances</h2>