geo-activity-playground 0.27.1__py3-none-any.whl → 0.29.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 (40) hide show
  1. geo_activity_playground/__main__.py +1 -2
  2. geo_activity_playground/core/activities.py +3 -3
  3. geo_activity_playground/core/config.py +4 -0
  4. geo_activity_playground/core/paths.py +10 -0
  5. geo_activity_playground/core/tasks.py +7 -6
  6. geo_activity_playground/explorer/tile_visits.py +168 -133
  7. geo_activity_playground/webui/activity/controller.py +51 -14
  8. geo_activity_playground/webui/activity/templates/activity/show.html.j2 +37 -9
  9. geo_activity_playground/webui/app.py +20 -22
  10. geo_activity_playground/webui/auth/blueprint.py +27 -0
  11. geo_activity_playground/webui/auth/templates/auth/index.html.j2 +21 -0
  12. geo_activity_playground/webui/authenticator.py +46 -0
  13. geo_activity_playground/webui/entry_controller.py +8 -4
  14. geo_activity_playground/webui/equipment/controller.py +2 -1
  15. geo_activity_playground/webui/explorer/controller.py +4 -3
  16. geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +2 -0
  17. geo_activity_playground/webui/heatmap/heatmap_controller.py +20 -6
  18. geo_activity_playground/webui/plot_util.py +9 -0
  19. geo_activity_playground/webui/search/blueprint.py +20 -0
  20. geo_activity_playground/webui/settings/blueprint.py +101 -1
  21. geo_activity_playground/webui/settings/controller.py +43 -0
  22. geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2 +19 -0
  23. geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +33 -0
  24. geo_activity_playground/webui/settings/templates/settings/index.html.j2 +27 -0
  25. geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2 +22 -0
  26. geo_activity_playground/webui/square_planner/controller.py +1 -1
  27. geo_activity_playground/webui/summary/blueprint.py +3 -2
  28. geo_activity_playground/webui/summary/controller.py +20 -13
  29. geo_activity_playground/webui/templates/home.html.j2 +1 -1
  30. geo_activity_playground/webui/templates/page.html.j2 +57 -29
  31. geo_activity_playground/webui/upload/blueprint.py +7 -0
  32. geo_activity_playground/webui/upload/controller.py +4 -8
  33. geo_activity_playground/webui/upload/templates/upload/index.html.j2 +15 -31
  34. {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.29.0.dist-info}/METADATA +3 -4
  35. {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.29.0.dist-info}/RECORD +39 -32
  36. geo_activity_playground/webui/search_controller.py +0 -19
  37. /geo_activity_playground/webui/{templates/search.html.j2 → search/templates/search/index.html.j2} +0 -0
  38. {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.29.0.dist-info}/LICENSE +0 -0
  39. {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.29.0.dist-info}/WHEEL +0 -0
  40. {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.29.0.dist-info}/entry_points.txt +0 -0
@@ -5,7 +5,6 @@ import secrets
5
5
 
6
6
  from flask import Flask
7
7
  from flask import render_template
8
- from flask import request
9
8
 
10
9
  from ..core.activities import ActivityRepository
11
10
  from ..explorer.tile_visits import TileVisitAccessor
@@ -16,29 +15,20 @@ from .entry_controller import EntryController
16
15
  from .equipment.blueprint import make_equipment_blueprint
17
16
  from .explorer.blueprint import make_explorer_blueprint
18
17
  from .heatmap.blueprint import make_heatmap_blueprint
19
- from .search_controller import SearchController
18
+ from .search.blueprint import make_search_blueprint
20
19
  from .square_planner.blueprint import make_square_planner_blueprint
21
20
  from .summary.blueprint import make_summary_blueprint
22
21
  from .tile.blueprint import make_tile_blueprint
23
22
  from .upload.blueprint import make_upload_blueprint
23
+ from geo_activity_playground.core.config import Config
24
24
  from geo_activity_playground.core.config import ConfigAccessor
25
+ from geo_activity_playground.webui.auth.blueprint import make_auth_blueprint
26
+ from geo_activity_playground.webui.authenticator import Authenticator
25
27
  from geo_activity_playground.webui.settings.blueprint import make_settings_blueprint
26
28
 
27
29
 
28
- def route_search(app: Flask, repository: ActivityRepository) -> None:
29
- search_controller = SearchController(repository)
30
-
31
- @app.route("/search", methods=["POST"])
32
- def search():
33
- form_input = request.form
34
- return render_template(
35
- "search.html.j2",
36
- **search_controller.render_search_results(form_input["name"])
37
- )
38
-
39
-
40
- def route_start(app: Flask, repository: ActivityRepository) -> None:
41
- entry_controller = EntryController(repository)
30
+ def route_start(app: Flask, repository: ActivityRepository, config: Config) -> None:
31
+ entry_controller = EntryController(repository, config)
42
32
 
43
33
  @app.route("/")
44
34
  def index():
@@ -64,15 +54,17 @@ def web_ui_main(
64
54
  host: str,
65
55
  port: int,
66
56
  ) -> None:
67
-
68
57
  repository.reload()
69
58
 
70
59
  app = Flask(__name__)
71
60
  app.config["UPLOAD_FOLDER"] = "Activities"
72
61
  app.secret_key = get_secret_key()
73
62
 
74
- route_search(app, repository)
75
- route_start(app, repository)
63
+ authenticator = Authenticator(config_accessor())
64
+
65
+ route_start(app, repository, config_accessor())
66
+
67
+ app.register_blueprint(make_auth_blueprint(authenticator), url_prefix="/auth")
76
68
 
77
69
  app.register_blueprint(
78
70
  make_activity_blueprint(
@@ -97,7 +89,7 @@ def web_ui_main(
97
89
  make_heatmap_blueprint(repository, tile_visit_accessor), url_prefix="/heatmap"
98
90
  )
99
91
  app.register_blueprint(
100
- make_settings_blueprint(config_accessor),
92
+ make_settings_blueprint(config_accessor, authenticator),
101
93
  url_prefix="/settings",
102
94
  )
103
95
  app.register_blueprint(
@@ -105,12 +97,18 @@ def web_ui_main(
105
97
  url_prefix="/square-planner",
106
98
  )
107
99
  app.register_blueprint(
108
- make_summary_blueprint(repository),
100
+ make_search_blueprint(repository),
101
+ url_prefix="/search",
102
+ )
103
+ app.register_blueprint(
104
+ make_summary_blueprint(repository, config_accessor()),
109
105
  url_prefix="/summary",
110
106
  )
111
107
  app.register_blueprint(make_tile_blueprint(), url_prefix="/tile")
112
108
  app.register_blueprint(
113
- make_upload_blueprint(repository, tile_visit_accessor, config_accessor()),
109
+ make_upload_blueprint(
110
+ repository, tile_visit_accessor, config_accessor(), authenticator
111
+ ),
114
112
  url_prefix="/upload",
115
113
  )
116
114
 
@@ -0,0 +1,27 @@
1
+ from flask import Blueprint
2
+ from flask import redirect
3
+ from flask import render_template
4
+ from flask import request
5
+ from flask import url_for
6
+
7
+ from geo_activity_playground.webui.authenticator import Authenticator
8
+
9
+
10
+ def make_auth_blueprint(authenticator: Authenticator) -> Blueprint:
11
+ blueprint = Blueprint("auth", __name__, template_folder="templates")
12
+
13
+ @blueprint.route("/", methods=["GET", "POST"])
14
+ def index():
15
+ if request.method == "POST":
16
+ authenticator.authenticate(request.form["password"])
17
+ return render_template(
18
+ "auth/index.html.j2",
19
+ is_authenticated=authenticator.is_authenticated(),
20
+ )
21
+
22
+ @blueprint.route("/logout")
23
+ def logout():
24
+ authenticator.logout()
25
+ return redirect(url_for(".index"))
26
+
27
+ return blueprint
@@ -0,0 +1,21 @@
1
+ {% extends "page.html.j2" %}
2
+
3
+ {% block container %}
4
+ <h1>Authentication</h1>
5
+
6
+ {% if is_authenticated %}
7
+ <p>You are either logged in or don't have a password set. You can do everything.</p>
8
+
9
+ <a class="btn btn-primary" href="{{ url_for('.logout') }}">Log Out</a>
10
+ {% else %}
11
+ <form method="POST">
12
+ <div class="mb-3">
13
+ <label for="password" class="form-label">Password</label>
14
+ <input type="password" class="form-control" id="password" name="password" />
15
+ </div>
16
+
17
+ <button type="submit" class="btn btn-primary">Log In</button>
18
+ </form>
19
+ {% endif %}
20
+
21
+ {% endblock %}
@@ -0,0 +1,46 @@
1
+ import functools
2
+ from typing import Callable
3
+
4
+ from flask import flash
5
+ from flask import redirect
6
+ from flask import session
7
+ from flask import url_for
8
+
9
+ from geo_activity_playground.core.config import Config
10
+
11
+
12
+ class Authenticator:
13
+ def __init__(self, config: Config) -> None:
14
+ self._config = config
15
+
16
+ def is_authenticated(self) -> bool:
17
+ return not self._config.upload_password or session.get(
18
+ "is_authenticated", False
19
+ )
20
+
21
+ def authenticate(self, password: str) -> None:
22
+ if password == self._config.upload_password:
23
+ session["is_authenticated"] = True
24
+ session.permanent = True
25
+ flash("Login successful.", category="success")
26
+ else:
27
+ flash("Incorrect password.", category="warning")
28
+
29
+ def logout(self) -> None:
30
+ session["is_authenticated"] = False
31
+ flash("Logout successful.", category="success")
32
+
33
+
34
+ def needs_authentication(authenticator: Authenticator) -> Callable:
35
+ def decorator(route: Callable) -> Callable:
36
+ @functools.wraps(route)
37
+ def wrapped_route(*args, **kwargs):
38
+ if authenticator.is_authenticated():
39
+ return route(*args, **kwargs)
40
+ else:
41
+ flash("You need to be logged in to view that site.", category="Warning")
42
+ return redirect(url_for("auth.index"))
43
+
44
+ return wrapped_route
45
+
46
+ return decorator
@@ -6,18 +6,22 @@ import pandas as pd
6
6
 
7
7
  from geo_activity_playground.core.activities import ActivityRepository
8
8
  from geo_activity_playground.core.activities import make_geojson_from_time_series
9
+ from geo_activity_playground.core.config import Config
10
+ from geo_activity_playground.webui.plot_util import make_kind_scale
9
11
 
10
12
 
11
13
  class EntryController:
12
- def __init__(self, repository: ActivityRepository) -> None:
14
+ def __init__(self, repository: ActivityRepository, config: Config) -> None:
13
15
  self._repository = repository
16
+ self._config = config
14
17
 
15
18
  def render(self) -> dict:
19
+ kind_scale = make_kind_scale(self._repository.meta, self._config)
16
20
  result = {"latest_activities": []}
17
21
 
18
22
  if len(self._repository):
19
23
  result["distance_last_30_days_plot"] = distance_last_30_days_meta_plot(
20
- self._repository.meta
24
+ self._repository.meta, kind_scale
21
25
  )
22
26
 
23
27
  for activity in itertools.islice(
@@ -33,7 +37,7 @@ class EntryController:
33
37
  return result
34
38
 
35
39
 
36
- def distance_last_30_days_meta_plot(meta: pd.DataFrame) -> str:
40
+ def distance_last_30_days_meta_plot(meta: pd.DataFrame, kind_scale: alt.Scale) -> str:
37
41
  before_30_days = pd.to_datetime(
38
42
  datetime.datetime.now() - datetime.timedelta(days=31)
39
43
  )
@@ -48,7 +52,7 @@ def distance_last_30_days_meta_plot(meta: pd.DataFrame) -> str:
48
52
  .encode(
49
53
  alt.X("yearmonthdate(start)", title="Date"),
50
54
  alt.Y("sum(distance_km)", title="Distance / km"),
51
- alt.Color("kind", scale=alt.Scale(scheme="category10"), title="Kind"),
55
+ alt.Color("kind", scale=kind_scale, title="Kind"),
52
56
  [
53
57
  alt.Tooltip("yearmonthdate(start)", title="Date"),
54
58
  alt.Tooltip("kind", title="Kind"),
@@ -103,7 +103,8 @@ class EquipmentController:
103
103
  }
104
104
 
105
105
  for equipment, offset in self._config.equipment_offsets.items():
106
- equipment_summary.loc[equipment, "total_distance_km"] += offset
106
+ if equipment in equipment_summary.index:
107
+ equipment_summary.loc[equipment, "total_distance_km"] += offset
107
108
 
108
109
  return {
109
110
  "equipment_variables": equipment_variables,
@@ -54,9 +54,9 @@ class ExplorerController:
54
54
  if zoom not in self._config_accessor().explorer_zoom_levels:
55
55
  return {"zoom_level_not_generated": zoom}
56
56
 
57
- tile_evolution_states = self._tile_visit_accessor.states
58
- tile_visits = self._tile_visit_accessor.visits
59
- tile_histories = self._tile_visit_accessor.histories
57
+ tile_evolution_states = self._tile_visit_accessor.tile_state["evolution_state"]
58
+ tile_visits = self._tile_visit_accessor.tile_state["tile_visits"]
59
+ tile_histories = self._tile_visit_accessor.tile_state["tile_history"]
60
60
 
61
61
  medians = tile_histories[zoom].median()
62
62
  median_lat, median_lon = get_tile_upper_left_lat_lon(
@@ -161,6 +161,7 @@ def get_three_color_tiles(
161
161
  "last_visit": tile_data["last_time"].date().isoformat(),
162
162
  "num_visits": len(tile_data["activity_ids"]),
163
163
  "square": False,
164
+ "tile": f"({zoom}, {tile[0]}, {tile[1]})",
164
165
  }
165
166
 
166
167
  # Mark biggest square.
@@ -48,6 +48,8 @@
48
48
  function onEachFeature(feature, layer) {
49
49
  if (feature.properties && feature.properties.first_visit) {
50
50
  let lines = [
51
+ `<dt>Tile</dt>`,
52
+ `<dd>${feature.properties.tile}</dd>`,
51
53
  `<dt>First visit</dt>`,
52
54
  `<dd>${feature.properties.first_visit}</br><a href=/activity/${feature.properties.first_activity_id}>${feature.properties.first_activity_name}</a></dd>`,
53
55
  `<dt>Last visit</dt>`,
@@ -34,10 +34,14 @@ class HeatmapController:
34
34
  self._repository = repository
35
35
  self._tile_visit_accessor = tile_visit_accessor
36
36
 
37
- self.tile_histories = self._tile_visit_accessor.histories
38
- self.tile_evolution_states = self._tile_visit_accessor.states
39
- self.tile_visits = self._tile_visit_accessor.visits
40
- self.activities_per_tile = self._tile_visit_accessor.activities_per_tile
37
+ self.tile_histories = self._tile_visit_accessor.tile_state["tile_history"]
38
+ self.tile_evolution_states = self._tile_visit_accessor.tile_state[
39
+ "evolution_state"
40
+ ]
41
+ self.tile_visits = self._tile_visit_accessor.tile_state["tile_visits"]
42
+ self.activities_per_tile = self._tile_visit_accessor.tile_state[
43
+ "activities_per_tile"
44
+ ]
41
45
 
42
46
  def render(self, kinds: list[str] = []) -> dict:
43
47
  zoom = 14
@@ -74,7 +78,14 @@ class HeatmapController:
74
78
  tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
75
79
  tile_count_cache_path = pathlib.Path(f"Cache/Heatmap/{kind}/{z}/{x}/{y}.npy")
76
80
  if tile_count_cache_path.exists():
77
- tile_counts = np.load(tile_count_cache_path)
81
+ try:
82
+ tile_counts = np.load(tile_count_cache_path)
83
+ except ValueError:
84
+ logger.warning(
85
+ f"Heatmap count file {tile_count_cache_path} is corrupted, deleting."
86
+ )
87
+ tile_count_cache_path.unlink()
88
+ tile_counts = np.zeros(tile_pixels, dtype=np.int32)
78
89
  else:
79
90
  tile_counts = np.zeros(tile_pixels, dtype=np.int32)
80
91
  tile_count_cache_path.parent.mkdir(parents=True, exist_ok=True)
@@ -110,7 +121,10 @@ class HeatmapController:
110
121
  draw.line(pixels, fill=1, width=max(3, 6 * (z - 17)))
111
122
  aim = np.array(im)
112
123
  tile_counts += aim
113
- np.save(tile_count_cache_path, tile_counts)
124
+ tmp_path = tile_count_cache_path.with_suffix(".tmp.npy")
125
+ np.save(tmp_path, tile_counts)
126
+ tile_count_cache_path.unlink(missing_ok=True)
127
+ tmp_path.rename(tile_count_cache_path)
114
128
  return tile_counts
115
129
 
116
130
  def _render_tile_image(
@@ -0,0 +1,9 @@
1
+ import altair as alt
2
+ import pandas as pd
3
+
4
+ from geo_activity_playground.core.config import Config
5
+
6
+
7
+ def make_kind_scale(meta: pd.DataFrame, config: Config) -> alt.Scale:
8
+ kinds = sorted(meta["kind"].unique())
9
+ return alt.Scale(domain=kinds, scheme=config.color_scheme_for_kind)
@@ -0,0 +1,20 @@
1
+ from flask import Blueprint
2
+ from flask import render_template
3
+ from flask import request
4
+ from flask import Response
5
+
6
+ from ...core.activities import ActivityRepository
7
+
8
+
9
+ def make_search_blueprint(repository: ActivityRepository) -> Blueprint:
10
+ blueprint = Blueprint("search", __name__, template_folder="templates")
11
+
12
+ @blueprint.route("/", methods=["POST"])
13
+ def index():
14
+ activities = []
15
+ for _, row in repository.meta.iterrows():
16
+ if request.form["name"] in row["name"]:
17
+ activities.append(row)
18
+ return render_template("search/index.html.j2", activities=activities)
19
+
20
+ return blueprint
@@ -8,6 +8,8 @@ from flask import request
8
8
  from flask import url_for
9
9
 
10
10
  from geo_activity_playground.core.config import ConfigAccessor
11
+ from geo_activity_playground.webui.authenticator import Authenticator
12
+ from geo_activity_playground.webui.authenticator import needs_authentication
11
13
  from geo_activity_playground.webui.settings.controller import SettingsController
12
14
 
13
15
 
@@ -19,15 +21,96 @@ def int_or_none(s: str) -> Optional[int]:
19
21
  flash(f"Cannot parse integer from {s}: {e}", category="danger")
20
22
 
21
23
 
22
- def make_settings_blueprint(config_accessor: ConfigAccessor) -> Blueprint:
24
+ def make_settings_blueprint(
25
+ config_accessor: ConfigAccessor, authenticator: Authenticator
26
+ ) -> Blueprint:
23
27
  settings_controller = SettingsController(config_accessor)
24
28
  blueprint = Blueprint("settings", __name__, template_folder="templates")
25
29
 
26
30
  @blueprint.route("/")
31
+ @needs_authentication(authenticator)
27
32
  def index():
28
33
  return render_template("settings/index.html.j2")
29
34
 
35
+ @blueprint.route("/admin-password", methods=["GET", "POST"])
36
+ @needs_authentication(authenticator)
37
+ def admin_password():
38
+ if request.method == "POST":
39
+ settings_controller.save_admin_password(request.form["password"])
40
+ return render_template(
41
+ "settings/admin-password.html.j2",
42
+ **settings_controller.render_admin_password(),
43
+ )
44
+
45
+ @blueprint.route("/color-schemes", methods=["GET", "POST"])
46
+ @needs_authentication(authenticator)
47
+ def color_schemes():
48
+ if request.method == "POST":
49
+ config_accessor().color_scheme_for_counts = request.form[
50
+ "color_scheme_for_counts"
51
+ ]
52
+ config_accessor().color_scheme_for_kind = request.form[
53
+ "color_scheme_for_kind"
54
+ ]
55
+ config_accessor.save()
56
+ flash("Updated color schemes.", category="success")
57
+ return render_template(
58
+ "settings/color-schemes.html.j2",
59
+ color_scheme_for_counts=config_accessor().color_scheme_for_counts,
60
+ color_scheme_for_counts_avail=[
61
+ "viridis",
62
+ "magma",
63
+ "inferno",
64
+ "plasma",
65
+ "cividis",
66
+ "turbo",
67
+ "bluegreen",
68
+ "bluepurple",
69
+ "goldgreen",
70
+ "goldorange",
71
+ "goldred",
72
+ "greenblue",
73
+ "orangered",
74
+ "purplebluegreen",
75
+ "purpleblue",
76
+ "purplered",
77
+ "redpurple",
78
+ "yellowgreenblue",
79
+ "yellowgreen",
80
+ "yelloworangebrown",
81
+ "yelloworangered",
82
+ "darkblue",
83
+ "darkgold",
84
+ "darkgreen",
85
+ "darkmulti",
86
+ "darkred",
87
+ "lightgreyred",
88
+ "lightgreyteal",
89
+ "lightmulti",
90
+ "lightorange",
91
+ "lighttealblue",
92
+ ],
93
+ color_scheme_for_kind=config_accessor().color_scheme_for_kind,
94
+ color_scheme_for_kind_avail=[
95
+ "accent",
96
+ "category10",
97
+ "category20",
98
+ "category20b",
99
+ "category20c",
100
+ "dark2",
101
+ "paired",
102
+ "pastel1",
103
+ "pastel2",
104
+ "set1",
105
+ "set2",
106
+ "set3",
107
+ "tableau10",
108
+ "tableau20",
109
+ ],
110
+ )
111
+
30
112
  @blueprint.route("/equipment-offsets", methods=["GET", "POST"])
113
+ @needs_authentication(authenticator)
31
114
  def equipment_offsets():
32
115
  if request.method == "POST":
33
116
  equipments = request.form.getlist("equipment")
@@ -39,6 +122,7 @@ def make_settings_blueprint(config_accessor: ConfigAccessor) -> Blueprint:
39
122
  )
40
123
 
41
124
  @blueprint.route("/heart-rate", methods=["GET", "POST"])
125
+ @needs_authentication(authenticator)
42
126
  def heart_rate():
43
127
  if request.method == "POST":
44
128
  birth_year = int_or_none(request.form["birth_year"])
@@ -54,6 +138,7 @@ def make_settings_blueprint(config_accessor: ConfigAccessor) -> Blueprint:
54
138
  )
55
139
 
56
140
  @blueprint.route("/kinds-without-achievements", methods=["GET", "POST"])
141
+ @needs_authentication(authenticator)
57
142
  def kinds_without_achievements():
58
143
  if request.method == "POST":
59
144
  kinds = request.form.getlist("kind")
@@ -64,6 +149,7 @@ def make_settings_blueprint(config_accessor: ConfigAccessor) -> Blueprint:
64
149
  )
65
150
 
66
151
  @blueprint.route("/metadata-extraction", methods=["GET", "POST"])
152
+ @needs_authentication(authenticator)
67
153
  def metadata_extraction():
68
154
  if request.method == "POST":
69
155
  regexes = request.form.getlist("regex")
@@ -74,6 +160,7 @@ def make_settings_blueprint(config_accessor: ConfigAccessor) -> Blueprint:
74
160
  )
75
161
 
76
162
  @blueprint.route("/privacy-zones", methods=["GET", "POST"])
163
+ @needs_authentication(authenticator)
77
164
  def privacy_zones():
78
165
  if request.method == "POST":
79
166
  zone_names = request.form.getlist("zone_name")
@@ -84,7 +171,19 @@ def make_settings_blueprint(config_accessor: ConfigAccessor) -> Blueprint:
84
171
  **settings_controller.render_privacy_zones(),
85
172
  )
86
173
 
174
+ @blueprint.route("/sharepic", methods=["GET", "POST"])
175
+ @needs_authentication(authenticator)
176
+ def sharepic():
177
+ if request.method == "POST":
178
+ names = request.form.getlist("name")
179
+ settings_controller.save_sharepic(names)
180
+ return render_template(
181
+ "settings/sharepic.html.j2",
182
+ **settings_controller.render_sharepic(),
183
+ )
184
+
87
185
  @blueprint.route("/strava", methods=["GET", "POST"])
186
+ @needs_authentication(authenticator)
88
187
  def strava():
89
188
  if request.method == "POST":
90
189
  strava_client_id = request.form["strava_client_id"]
@@ -98,6 +197,7 @@ def make_settings_blueprint(config_accessor: ConfigAccessor) -> Blueprint:
98
197
  )
99
198
 
100
199
  @blueprint.route("/strava-callback")
200
+ @needs_authentication(authenticator)
101
201
  def strava_callback():
102
202
  code = request.args.get("code", type=str)
103
203
  settings_controller.save_strava_code(code)
@@ -10,10 +10,32 @@ from geo_activity_playground.core.config import ConfigAccessor
10
10
  from geo_activity_playground.core.heart_rate import HeartRateZoneComputer
11
11
 
12
12
 
13
+ SHAREPIC_FIELDS = {
14
+ "calories": "Calories",
15
+ "distance_km": "Distance",
16
+ "elapsed_time": "Elapsed time",
17
+ "equipment": "Equipment",
18
+ "kind": "Kind",
19
+ "name": "Name",
20
+ "start": "Date",
21
+ "Steps": "Steps",
22
+ }
23
+
24
+
13
25
  class SettingsController:
14
26
  def __init__(self, config_accessor: ConfigAccessor) -> None:
15
27
  self._config_accessor = config_accessor
16
28
 
29
+ def render_admin_password(self) -> dict:
30
+ return {
31
+ "password": self._config_accessor().upload_password,
32
+ }
33
+
34
+ def save_admin_password(self, password: str) -> None:
35
+ self._config_accessor().upload_password = password
36
+ self._config_accessor.save()
37
+ flash("Updated admin password.", category="success")
38
+
17
39
  def render_equipment_offsets(self) -> dict:
18
40
  return {
19
41
  "equipment_offsets": self._config_accessor().equipment_offsets,
@@ -184,6 +206,27 @@ class SettingsController:
184
206
  self._config_accessor.save()
185
207
  flash("Updated privacy zones.", category="success")
186
208
 
209
+ def render_sharepic(self) -> dict:
210
+
211
+ return {
212
+ "names": [
213
+ (
214
+ name,
215
+ label,
216
+ name not in self._config_accessor().sharepic_suppressed_fields,
217
+ )
218
+ for name, label in SHAREPIC_FIELDS.items()
219
+ ]
220
+ }
221
+
222
+ def save_sharepic(self, names: list[str]) -> None:
223
+ self._config_accessor().sharepic_suppressed_fields = list(
224
+ set(SHAREPIC_FIELDS) - set(names)
225
+ )
226
+ self._config_accessor.save()
227
+ flash("Updated sharepic preferences.", category="success")
228
+ pass
229
+
187
230
  def render_strava(self) -> dict:
188
231
  return {
189
232
  "strava_client_id": self._config_accessor().strava_client_id,
@@ -0,0 +1,19 @@
1
+ {% extends "page.html.j2" %}
2
+
3
+ {% block container %}
4
+
5
+ <h1 class="mb-3">Admin Password</h1>
6
+
7
+ <p>To protect the settings and the upload functionality, you may specify a password.</p>
8
+
9
+ <form method="POST">
10
+ <div class="mb-3">
11
+ <label for="password" class="form-label">Password</label>
12
+ <input type="text" class="form-control" id="password" name="password" value="{{ password }}" />
13
+ </div>
14
+
15
+ <button type="submit" class="btn btn-primary">Save</button>
16
+ </form>
17
+
18
+
19
+ {% endblock %}
@@ -0,0 +1,33 @@
1
+ {% extends "page.html.j2" %}
2
+
3
+ {% block container %}
4
+
5
+ <h1 class="mb-3">Color Schemes for Plots</h1>
6
+
7
+ <p>Don't like color schemes in the plots? Have a look at the <a href="https://vega.github.io/vega/docs/schemes/"
8
+ target="_blank">colors schemes of Vega</a> and pick one that you like.</p>
9
+
10
+ <form method="POST">
11
+ <div class="mb-3">
12
+ <label class="form-label">Color scheme for activity kinds</label>
13
+ <select class="form-select" aria-label="Color scheme for activity kinds" name="color_scheme_for_kind">
14
+ {% for cs in color_scheme_for_kind_avail %}
15
+ <option {% if cs==color_scheme_for_kind %} selected {% endif %}>{{ cs }}</option>
16
+ {% endfor %}
17
+ </select>
18
+ </div>
19
+
20
+ <div class="mb-3">
21
+ <label class="form-label">Color scheme for heatmaps</label>
22
+ <select class="form-select" aria-label="Color scheme for heatmaps" name="color_scheme_for_counts">
23
+ {% for cs in color_scheme_for_counts_avail %}
24
+ <option {% if cs==color_scheme_for_counts %} selected {% endif %}>{{ cs }}</option>
25
+ {% endfor %}
26
+ </select>
27
+ </div>
28
+
29
+ <button type="submit" class="btn btn-primary">Save</button>
30
+ </form>
31
+
32
+
33
+ {% endblock %}