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.
- geo_activity_playground/__main__.py +1 -2
- geo_activity_playground/core/activities.py +3 -3
- geo_activity_playground/core/config.py +4 -0
- geo_activity_playground/core/paths.py +10 -0
- geo_activity_playground/core/tasks.py +7 -6
- geo_activity_playground/explorer/tile_visits.py +168 -133
- geo_activity_playground/webui/activity/controller.py +51 -14
- geo_activity_playground/webui/activity/templates/activity/show.html.j2 +37 -9
- geo_activity_playground/webui/app.py +20 -22
- geo_activity_playground/webui/auth/blueprint.py +27 -0
- geo_activity_playground/webui/auth/templates/auth/index.html.j2 +21 -0
- geo_activity_playground/webui/authenticator.py +46 -0
- geo_activity_playground/webui/entry_controller.py +8 -4
- geo_activity_playground/webui/equipment/controller.py +2 -1
- geo_activity_playground/webui/explorer/controller.py +4 -3
- geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +2 -0
- geo_activity_playground/webui/heatmap/heatmap_controller.py +20 -6
- geo_activity_playground/webui/plot_util.py +9 -0
- geo_activity_playground/webui/search/blueprint.py +20 -0
- geo_activity_playground/webui/settings/blueprint.py +101 -1
- geo_activity_playground/webui/settings/controller.py +43 -0
- geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2 +19 -0
- geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +33 -0
- geo_activity_playground/webui/settings/templates/settings/index.html.j2 +27 -0
- geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2 +22 -0
- geo_activity_playground/webui/square_planner/controller.py +1 -1
- geo_activity_playground/webui/summary/blueprint.py +3 -2
- geo_activity_playground/webui/summary/controller.py +20 -13
- geo_activity_playground/webui/templates/home.html.j2 +1 -1
- geo_activity_playground/webui/templates/page.html.j2 +57 -29
- geo_activity_playground/webui/upload/blueprint.py +7 -0
- geo_activity_playground/webui/upload/controller.py +4 -8
- geo_activity_playground/webui/upload/templates/upload/index.html.j2 +15 -31
- {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.29.0.dist-info}/METADATA +3 -4
- {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.29.0.dist-info}/RECORD +39 -32
- geo_activity_playground/webui/search_controller.py +0 -19
- /geo_activity_playground/webui/{templates/search.html.j2 → search/templates/search/index.html.j2} +0 -0
- {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.29.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.29.0.dist-info}/WHEEL +0 -0
- {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 .
|
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
|
29
|
-
|
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
|
-
|
75
|
-
|
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
|
-
|
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(
|
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=
|
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
|
-
|
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.
|
58
|
-
tile_visits = self._tile_visit_accessor.
|
59
|
-
tile_histories = self._tile_visit_accessor.
|
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.
|
38
|
-
self.tile_evolution_states = self._tile_visit_accessor.
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
-
|
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(
|
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 %}
|