geo-activity-playground 0.22.0__py3-none-any.whl → 0.24.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 (77) hide show
  1. geo_activity_playground/__main__.py +1 -1
  2. geo_activity_playground/core/activities.py +16 -9
  3. geo_activity_playground/core/activity_parsers.py +17 -32
  4. geo_activity_playground/core/cache_migrations.py +24 -0
  5. geo_activity_playground/core/heatmap.py +21 -21
  6. geo_activity_playground/core/privacy_zones.py +16 -0
  7. geo_activity_playground/core/similarity.py +1 -1
  8. geo_activity_playground/core/test_time_conversion.py +37 -0
  9. geo_activity_playground/core/time_conversion.py +14 -0
  10. geo_activity_playground/explorer/tile_visits.py +44 -27
  11. geo_activity_playground/importers/__init__.py +0 -0
  12. geo_activity_playground/importers/directory.py +7 -2
  13. geo_activity_playground/importers/strava_api.py +6 -0
  14. geo_activity_playground/importers/strava_checkout.py +12 -3
  15. geo_activity_playground/webui/__init__.py +0 -0
  16. geo_activity_playground/webui/activity/__init__.py +0 -0
  17. geo_activity_playground/webui/activity/blueprint.py +58 -0
  18. geo_activity_playground/webui/{activity_controller.py → activity/controller.py} +128 -18
  19. geo_activity_playground/webui/{templates/activity-day.html.j2 → activity/templates/activity/day.html.j2} +14 -2
  20. geo_activity_playground/webui/{templates/activity-name.html.j2 → activity/templates/activity/name.html.j2} +1 -1
  21. geo_activity_playground/webui/{templates/activity.html.j2 → activity/templates/activity/show.html.j2} +9 -4
  22. geo_activity_playground/webui/app.py +68 -281
  23. geo_activity_playground/webui/calendar/__init__.py +0 -0
  24. geo_activity_playground/webui/calendar/blueprint.py +26 -0
  25. geo_activity_playground/webui/{calendar_controller.py → calendar/controller.py} +5 -5
  26. geo_activity_playground/webui/{templates/calendar.html.j2 → calendar/templates/calendar/index.html.j2} +3 -2
  27. geo_activity_playground/webui/{templates/calendar-month.html.j2 → calendar/templates/calendar/month.html.j2} +2 -2
  28. geo_activity_playground/webui/eddington/__init__.py +0 -0
  29. geo_activity_playground/webui/eddington/blueprint.py +19 -0
  30. geo_activity_playground/webui/{eddington_controller.py → eddington/controller.py} +14 -6
  31. geo_activity_playground/webui/eddington/templates/eddington/index.html.j2 +56 -0
  32. geo_activity_playground/webui/entry_controller.py +4 -2
  33. geo_activity_playground/webui/equipment/__init__.py +0 -0
  34. geo_activity_playground/webui/equipment/blueprint.py +19 -0
  35. geo_activity_playground/webui/{equipment_controller.py → equipment/controller.py} +5 -3
  36. geo_activity_playground/webui/explorer/__init__.py +0 -0
  37. geo_activity_playground/webui/explorer/blueprint.py +54 -0
  38. geo_activity_playground/webui/{explorer_controller.py → explorer/controller.py} +6 -2
  39. geo_activity_playground/webui/{templates/explorer.html.j2 → explorer/templates/explorer/index.html.j2} +2 -2
  40. geo_activity_playground/webui/heatmap/__init__.py +0 -0
  41. geo_activity_playground/webui/heatmap/blueprint.py +41 -0
  42. geo_activity_playground/webui/{heatmap_controller.py → heatmap/heatmap_controller.py} +36 -13
  43. geo_activity_playground/webui/{templates/heatmap.html.j2 → heatmap/templates/heatmap/index.html.j2} +17 -2
  44. geo_activity_playground/webui/search_controller.py +1 -9
  45. geo_activity_playground/webui/square_planner/__init__.py +0 -0
  46. geo_activity_playground/webui/square_planner/blueprint.py +38 -0
  47. geo_activity_playground/webui/summary/__init__.py +0 -0
  48. geo_activity_playground/webui/summary/blueprint.py +16 -0
  49. geo_activity_playground/webui/summary/controller.py +268 -0
  50. geo_activity_playground/webui/summary/templates/summary/index.html.j2 +135 -0
  51. geo_activity_playground/webui/templates/{index.html.j2 → home.html.j2} +1 -1
  52. geo_activity_playground/webui/templates/page.html.j2 +32 -19
  53. geo_activity_playground/webui/templates/search.html.j2 +1 -1
  54. geo_activity_playground/webui/tile/__init__.py +0 -0
  55. geo_activity_playground/webui/tile/blueprint.py +31 -0
  56. geo_activity_playground/webui/upload/__init__.py +0 -0
  57. geo_activity_playground/webui/upload/blueprint.py +28 -0
  58. geo_activity_playground/webui/{upload_controller.py → upload/controller.py} +15 -6
  59. geo_activity_playground/webui/{templates/upload.html.j2 → upload/templates/upload/index.html.j2} +12 -11
  60. {geo_activity_playground-0.22.0.dist-info → geo_activity_playground-0.24.0.dist-info}/METADATA +2 -1
  61. geo_activity_playground-0.24.0.dist-info/RECORD +95 -0
  62. geo_activity_playground/webui/config_controller.py +0 -12
  63. geo_activity_playground/webui/locations_controller.py +0 -28
  64. geo_activity_playground/webui/summary_controller.py +0 -60
  65. geo_activity_playground/webui/templates/config.html.j2 +0 -24
  66. geo_activity_playground/webui/templates/eddington.html.j2 +0 -18
  67. geo_activity_playground/webui/templates/locations.html.j2 +0 -38
  68. geo_activity_playground/webui/templates/summary.html.j2 +0 -21
  69. geo_activity_playground-0.22.0.dist-info/RECORD +0 -74
  70. /geo_activity_playground/webui/{templates/activity-lines.html.j2 → activity/templates/activity/lines.html.j2} +0 -0
  71. /geo_activity_playground/webui/{templates/equipment.html.j2 → equipment/templates/equipment/index.html.j2} +0 -0
  72. /geo_activity_playground/webui/{square_planner_controller.py → square_planner/controller.py} +0 -0
  73. /geo_activity_playground/webui/{templates/square-planner.html.j2 → square_planner/templates/square_planner/index.html.j2} +0 -0
  74. /geo_activity_playground/webui/{tile_controller.py → tile/controller.py} +0 -0
  75. {geo_activity_playground-0.22.0.dist-info → geo_activity_playground-0.24.0.dist-info}/LICENSE +0 -0
  76. {geo_activity_playground-0.22.0.dist-info → geo_activity_playground-0.24.0.dist-info}/WHEEL +0 -0
  77. {geo_activity_playground-0.22.0.dist-info → geo_activity_playground-0.24.0.dist-info}/entry_points.txt +0 -0
@@ -1,204 +1,28 @@
1
- import urllib
1
+ import json
2
+ import pathlib
3
+ import secrets
2
4
 
3
5
  from flask import Flask
4
6
  from flask import redirect
5
7
  from flask import render_template
6
8
  from flask import request
7
- from flask import Response
8
9
 
9
- from .locations_controller import LocationsController
10
+ from ..core.activities import ActivityRepository
11
+ from ..explorer.tile_visits import TileVisitAccessor
12
+ from .activity.blueprint import make_activity_blueprint
13
+ from .calendar.blueprint import make_calendar_blueprint
14
+ from .eddington.blueprint import make_eddington_blueprint
15
+ from .entry_controller import EntryController
16
+ from .equipment.blueprint import make_equipment_blueprint
17
+ from .explorer.blueprint import make_explorer_blueprint
18
+ from .heatmap.blueprint import make_heatmap_blueprint
10
19
  from .search_controller import SearchController
11
- from geo_activity_playground.core.activities import ActivityRepository
12
- from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
13
- from geo_activity_playground.webui.activity_controller import ActivityController
14
- from geo_activity_playground.webui.calendar_controller import CalendarController
15
- from geo_activity_playground.webui.config_controller import ConfigController
16
- from geo_activity_playground.webui.eddington_controller import EddingtonController
17
- from geo_activity_playground.webui.entry_controller import EntryController
18
- from geo_activity_playground.webui.equipment_controller import EquipmentController
19
- from geo_activity_playground.webui.explorer_controller import ExplorerController
20
- from geo_activity_playground.webui.heatmap_controller import HeatmapController
21
- from geo_activity_playground.webui.search_controller import SearchController
22
- from geo_activity_playground.webui.square_planner_controller import (
23
- SquarePlannerController,
24
- )
25
- from geo_activity_playground.webui.strava_controller import StravaController
26
- from geo_activity_playground.webui.summary_controller import SummaryController
27
- from geo_activity_playground.webui.tile_controller import (
28
- TileController,
29
- )
30
- from geo_activity_playground.webui.upload_controller import UploadController
31
-
32
-
33
- def route_activity(app: Flask, repository: ActivityRepository) -> None:
34
- activity_controller = ActivityController(repository)
35
-
36
- @app.route("/activity/all")
37
- def activity_all():
38
- return render_template(
39
- "activity-lines.html.j2", **activity_controller.render_all()
40
- )
41
-
42
- @app.route("/activity/<id>")
43
- def activity(id: str):
44
- return render_template(
45
- "activity.html.j2", **activity_controller.render_activity(int(id))
46
- )
47
-
48
- @app.route("/activity/<id>/sharepic.png")
49
- def activity_sharepic(id: str):
50
- return Response(
51
- activity_controller.render_sharepic(int(id)),
52
- mimetype="image/png",
53
- )
54
-
55
- @app.route("/activity/day/<year>/<month>/<day>")
56
- def activity_day(year: str, month: str, day: str):
57
- return render_template(
58
- "activity-day.html.j2",
59
- **activity_controller.render_day(int(year), int(month), int(day))
60
- )
61
-
62
- @app.route("/activity/name/<name>")
63
- def activity_name(name: str):
64
- return render_template(
65
- "activity-name.html.j2",
66
- **activity_controller.render_name(urllib.parse.unquote(name))
67
- )
68
-
69
-
70
- def route_calendar(app: Flask, repository: ActivityRepository) -> None:
71
- calendar_controller = CalendarController(repository)
72
-
73
- @app.route("/calendar")
74
- def calendar():
75
- return render_template(
76
- "calendar.html.j2", **calendar_controller.render_overview()
77
- )
78
-
79
- @app.route("/calendar/<year>/<month>")
80
- def calendar_month(year: str, month: str):
81
- return render_template(
82
- "calendar-month.html.j2",
83
- **calendar_controller.render_month(int(year), int(month))
84
- )
85
-
86
-
87
- def route_config(app: Flask, repository: ActivityRepository) -> None:
88
- config_controller = ConfigController(repository)
89
-
90
- @app.route("/config")
91
- def config_index():
92
- return render_template("config.html.j2", **config_controller.action_index())
93
-
94
- @app.route("/config/save", methods=["POST"])
95
- def config_save():
96
- form_input = request.form
97
- return render_template(
98
- "config.html.j2", **config_controller.action_save(form_input)
99
- )
100
-
101
-
102
- def route_eddington(app: Flask, repository: ActivityRepository) -> None:
103
- eddington_controller = EddingtonController(repository)
104
-
105
- @app.route("/eddington")
106
- def eddington():
107
- return render_template("eddington.html.j2", **eddington_controller.render())
108
-
109
-
110
- def route_equipment(app: Flask, repository: ActivityRepository) -> None:
111
- equipment_controller = EquipmentController(repository)
112
-
113
- @app.route("/equipment")
114
- def equipment():
115
- return render_template("equipment.html.j2", **equipment_controller.render())
116
-
117
-
118
- def route_explorer(
119
- app: Flask, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
120
- ) -> None:
121
- explorer_controller = ExplorerController(repository, tile_visit_accessor)
122
-
123
- @app.route("/explorer/<zoom>")
124
- def explorer(zoom: str):
125
- return render_template(
126
- "explorer.html.j2", **explorer_controller.render(int(zoom))
127
- )
128
-
129
- @app.route("/explorer/<zoom>/<north>/<east>/<south>/<west>/explored.<suffix>")
130
- def explorer_download(
131
- zoom: str, north: str, east: str, south: str, west: str, suffix: str
132
- ):
133
- mimetypes = {"geojson": "application/json", "gpx": "application/xml"}
134
- return Response(
135
- explorer_controller.export_explored_tiles(
136
- int(zoom),
137
- float(north),
138
- float(east),
139
- float(south),
140
- float(west),
141
- suffix,
142
- ),
143
- mimetype=mimetypes[suffix],
144
- headers={"Content-disposition": "attachment"},
145
- )
146
-
147
- @app.route("/explorer/<zoom>/<north>/<east>/<south>/<west>/missing.<suffix>")
148
- def explorer_missing(
149
- zoom: str, north: str, east: str, south: str, west: str, suffix: str
150
- ):
151
- mimetypes = {"geojson": "application/json", "gpx": "application/xml"}
152
- return Response(
153
- explorer_controller.export_missing_tiles(
154
- int(zoom),
155
- float(north),
156
- float(east),
157
- float(south),
158
- float(west),
159
- suffix,
160
- ),
161
- mimetype=mimetypes[suffix],
162
- headers={"Content-disposition": "attachment"},
163
- )
164
-
165
-
166
- def route_heatmap(
167
- app: Flask, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
168
- ) -> None:
169
- heatmap_controller = HeatmapController(repository, tile_visit_accessor)
170
-
171
- @app.route("/heatmap")
172
- def heatmap():
173
- return render_template("heatmap.html.j2", **heatmap_controller.render())
174
-
175
- @app.route("/heatmap/tile/<z>/<x>/<y>.png")
176
- def heatmap_tile(x: str, y: str, z: str):
177
- return Response(
178
- heatmap_controller.render_tile(int(x), int(y), int(z)),
179
- mimetype="image/png",
180
- )
181
-
182
- @app.route("/heatmap/download/<north>/<east>/<south>/<west>")
183
- def heatmap_download(north: str, east: str, south: str, west: str):
184
- return Response(
185
- heatmap_controller.download_heatmap(
186
- float(north),
187
- float(east),
188
- float(south),
189
- float(west),
190
- ),
191
- mimetype="image/png",
192
- headers={"Content-disposition": 'attachment; filename="heatmap.png"'},
193
- )
194
-
195
-
196
- def route_locations(app: Flask, repository: ActivityRepository) -> None:
197
- controller = LocationsController(repository)
198
-
199
- @app.route("/locations")
200
- def locations_index():
201
- return render_template("locations.html.j2", **controller.render_index())
20
+ from .square_planner.blueprint import make_square_planner_blueprint
21
+ from .strava_controller import StravaController
22
+ from .summary.blueprint import make_summary_blueprint
23
+ from .tile.blueprint import make_tile_blueprint
24
+ from .upload.blueprint import make_upload_blueprint
25
+ from geo_activity_playground.core.privacy_zones import PrivacyZone
202
26
 
203
27
 
204
28
  def route_search(app: Flask, repository: ActivityRepository) -> None:
@@ -213,40 +37,12 @@ def route_search(app: Flask, repository: ActivityRepository) -> None:
213
37
  )
214
38
 
215
39
 
216
- def route_square_planner(
217
- app: Flask, repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
218
- ) -> None:
219
- controller = SquarePlannerController(repository, tile_visit_accessor)
220
-
221
- @app.route("/square-planner/<zoom>/<x>/<y>/<size>")
222
- def square_planner_planner(zoom, x, y, size):
223
- return render_template(
224
- "square-planner.html.j2",
225
- **controller.action_planner(int(zoom), int(x), int(y), int(size))
226
- )
227
-
228
- @app.route("/square-planner/<zoom>/<x>/<y>/<size>/missing.<suffix>")
229
- def square_planner_missing(zoom, x, y, size, suffix: str):
230
- mimetypes = {"geojson": "application/json", "gpx": "application/xml"}
231
- return Response(
232
- controller.export_missing_tiles(
233
- int(zoom),
234
- int(x),
235
- int(y),
236
- int(size),
237
- suffix,
238
- ),
239
- mimetype=mimetypes[suffix],
240
- headers={"Content-disposition": "attachment"},
241
- )
242
-
243
-
244
40
  def route_start(app: Flask, repository: ActivityRepository) -> None:
245
41
  entry_controller = EntryController(repository)
246
42
 
247
43
  @app.route("/")
248
44
  def index():
249
- return render_template("index.html.j2", **entry_controller.render())
45
+ return render_template("home.html.j2", **entry_controller.render())
250
46
 
251
47
 
252
48
  def route_strava(app: Flask, host: str, port: int) -> None:
@@ -280,52 +76,16 @@ def route_strava(app: Flask, host: str, port: int) -> None:
280
76
  )
281
77
 
282
78
 
283
- def route_summary(app: Flask, repository: ActivityRepository) -> None:
284
- summary_controller = SummaryController(repository)
285
-
286
- @app.route("/summary")
287
- def summary_statistics():
288
- return render_template("summary.html.j2", **summary_controller.render())
289
-
290
-
291
- def route_tiles(app: Flask, repository: ActivityRepository) -> None:
292
- tile_controller = TileController()
293
-
294
- @app.route("/tile/color/<z>/<x>/<y>.png")
295
- def tile_color(x: str, y: str, z: str):
296
- return Response(
297
- tile_controller.render_color(int(x), int(y), int(z)), mimetype="image/png"
298
- )
299
-
300
- @app.route("/tile/grayscale/<z>/<x>/<y>.png")
301
- def tile_grayscale(x: str, y: str, z: str):
302
- return Response(
303
- tile_controller.render_grayscale(int(x), int(y), int(z)),
304
- mimetype="image/png",
305
- )
306
-
307
- @app.route("/tile/pastel/<z>/<x>/<y>.png")
308
- def tile_pastel(x: str, y: str, z: str):
309
- return Response(
310
- tile_controller.render_pastel(int(x), int(y), int(z)), mimetype="image/png"
311
- )
312
-
313
-
314
- def route_upload(
315
- app: Flask,
316
- repository: ActivityRepository,
317
- tile_visit_accessor: TileVisitAccessor,
318
- config: dict,
319
- ):
320
- upload_controller = UploadController(repository, tile_visit_accessor, config)
321
-
322
- @app.route("/upload")
323
- def form():
324
- return render_template("upload.html.j2", **upload_controller.render_form())
325
-
326
- @app.route("/upload/receive", methods=["POST"])
327
- def receive():
328
- return upload_controller.receive()
79
+ def get_secret_key():
80
+ secret_file = pathlib.Path("Cache/flask-secret.json")
81
+ if secret_file.exists():
82
+ with open(secret_file) as f:
83
+ secret = json.load(f)
84
+ else:
85
+ secret = secrets.token_hex()
86
+ with open(secret_file, "w") as f:
87
+ json.dump(secret, f)
88
+ return secret
329
89
 
330
90
 
331
91
  def webui_main(
@@ -337,22 +97,49 @@ def webui_main(
337
97
  ) -> None:
338
98
  app = Flask(__name__)
339
99
 
340
- route_activity(app, repository)
341
- route_calendar(app, repository)
342
- route_config(app, repository)
343
- route_eddington(app, repository)
344
- route_equipment(app, repository)
345
- route_explorer(app, repository, tile_visit_accessor)
346
- route_heatmap(app, repository, tile_visit_accessor)
347
- route_locations(app, repository)
348
100
  route_search(app, repository)
349
- route_square_planner(app, repository, tile_visit_accessor)
350
101
  route_start(app, repository)
351
102
  route_strava(app, host, port)
352
- route_summary(app, repository)
353
- route_tiles(app, repository)
354
- route_upload(app, repository, tile_visit_accessor, config)
355
103
 
356
104
  app.config["UPLOAD_FOLDER"] = "Activities"
105
+ app.secret_key = get_secret_key()
106
+
107
+ app.register_blueprint(
108
+ make_activity_blueprint(
109
+ repository,
110
+ tile_visit_accessor,
111
+ [
112
+ PrivacyZone(points)
113
+ for points in config.get("privacy_zones", {}).values()
114
+ ],
115
+ ),
116
+ url_prefix="/activity",
117
+ )
118
+ app.register_blueprint(make_calendar_blueprint(repository), url_prefix="/calendar")
119
+ app.register_blueprint(
120
+ make_eddington_blueprint(repository), url_prefix="/eddington"
121
+ )
122
+ app.register_blueprint(
123
+ make_equipment_blueprint(repository), url_prefix="/equipment"
124
+ )
125
+ app.register_blueprint(
126
+ make_explorer_blueprint(repository, tile_visit_accessor), url_prefix="/explorer"
127
+ )
128
+ app.register_blueprint(
129
+ make_heatmap_blueprint(repository, tile_visit_accessor), url_prefix="/heatmap"
130
+ )
131
+ app.register_blueprint(
132
+ make_square_planner_blueprint(repository, tile_visit_accessor),
133
+ url_prefix="/square-planner",
134
+ )
135
+ app.register_blueprint(
136
+ make_summary_blueprint(repository),
137
+ url_prefix="/summary",
138
+ )
139
+ app.register_blueprint(make_tile_blueprint(), url_prefix="/tile")
140
+ app.register_blueprint(
141
+ make_upload_blueprint(repository, tile_visit_accessor, config),
142
+ url_prefix="/upload",
143
+ )
357
144
 
358
145
  app.run(host=host, port=port)
File without changes
@@ -0,0 +1,26 @@
1
+ from flask import Blueprint
2
+ from flask import render_template
3
+
4
+ from ...core.activities import ActivityRepository
5
+ from .controller import CalendarController
6
+
7
+
8
+ def make_calendar_blueprint(repository: ActivityRepository) -> Blueprint:
9
+ blueprint = Blueprint("calendar", __name__, template_folder="templates")
10
+
11
+ calendar_controller = CalendarController(repository)
12
+
13
+ @blueprint.route("/")
14
+ def index():
15
+ return render_template(
16
+ "calendar/index.html.j2", **calendar_controller.render_overview()
17
+ )
18
+
19
+ @blueprint.route("/<year>/<month>")
20
+ def month(year: str, month: str):
21
+ return render_template(
22
+ "calendar/month.html.j2",
23
+ **calendar_controller.render_month(int(year), int(month))
24
+ )
25
+
26
+ return blueprint
@@ -1,7 +1,7 @@
1
1
  import collections
2
2
  import datetime
3
3
 
4
- from geo_activity_playground.core.activities import ActivityRepository
4
+ from ...core.activities import ActivityRepository
5
5
 
6
6
 
7
7
  class CalendarController:
@@ -14,9 +14,9 @@ class CalendarController:
14
14
  meta["year"] = meta["start"].dt.year
15
15
  meta["month"] = meta["start"].dt.month
16
16
 
17
- monthly_distance = meta.groupby(["year", "month"]).apply(
18
- lambda group: sum(group["distance_km"])
19
- )
17
+ monthly_distance = meta.groupby(
18
+ ["year", "month"],
19
+ ).apply(lambda group: sum(group["distance_km"]), include_groups=False)
20
20
  monthly_distance.name = "total_distance_km"
21
21
  monthly_pivot = (
22
22
  monthly_distance.reset_index()
@@ -25,7 +25,7 @@ class CalendarController:
25
25
  )
26
26
 
27
27
  yearly_distance = meta.groupby(["year"]).apply(
28
- lambda group: sum(group["distance_km"])
28
+ lambda group: sum(group["distance_km"]), include_groups=False
29
29
  )
30
30
  yearly_distance.name = "total_distance_km"
31
31
  yearly_distances = {
@@ -21,13 +21,14 @@
21
21
  </thead>
22
22
  <tbody>
23
23
  {% for year, month_data in monthly_distances.items() %}
24
+ {% set year_int = year|round(0)|int %}
24
25
  <tr>
25
- <td>{{ year }}</td>
26
+ <td>{{ year_int }}</td>
26
27
  {% for month in range(1, 13) %}
27
28
  <td align="right">
28
29
  {% set distance = month_data[month] %}
29
30
  {% if distance %}
30
- <a href="/calendar/{{ year }}/{{ month }}">{{ distance|int() }} km</a>
31
+ <a href="{{ url_for(".month", year=year_int, month=month) }}">{{ distance|int() }} km</a>
31
32
  {% else %}
32
33
  0 km
33
34
  {% endif %}
@@ -29,7 +29,7 @@
29
29
  {% for day in range(1, 8) %}
30
30
  <td>
31
31
  {% if weeks[week][day] %}
32
- <a href="/activity/day/{{ year }}/{{ month }}/{{ day_of_month[week][day] }}"><b>{{
32
+ <a href="{{ url_for('activity.day', year=year, month=month, day=day_of_month[week][day]) }}"><b>{{
33
33
  day_of_month[week][day] }}.</b></a>
34
34
  {% elif day_of_month[week][day] %}
35
35
  <b>{{ day_of_month[week][day] }}.</b>
@@ -38,7 +38,7 @@
38
38
  {% if weeks[week][day] %}
39
39
  <ul>
40
40
  {% for activity in weeks[week][day] %}
41
- <li><a href="/activity/{{ activity.id }}">{{ activity.name }}</a></li>
41
+ <li><a href="{{ url_for('activity.show', id=activity.id) }}">{{ activity.name }}</a></li>
42
42
  {% endfor %}
43
43
  </ul>
44
44
  {% endif %}
File without changes
@@ -0,0 +1,19 @@
1
+ from flask import Blueprint
2
+ from flask import render_template
3
+
4
+ from ...core.activities import ActivityRepository
5
+ from .controller import EddingtonController
6
+
7
+
8
+ def make_eddington_blueprint(repository: ActivityRepository) -> Blueprint:
9
+ blueprint = Blueprint("eddington", __name__, template_folder="templates")
10
+
11
+ eddington_controller = EddingtonController(repository)
12
+
13
+ @blueprint.route("/")
14
+ def index():
15
+ return render_template(
16
+ "eddington/index.html.j2", **eddington_controller.render()
17
+ )
18
+
19
+ return blueprint
@@ -2,7 +2,7 @@ import altair as alt
2
2
  import numpy as np
3
3
  import pandas as pd
4
4
 
5
- from geo_activity_playground.core.activities import ActivityRepository
5
+ from ...core.activities import ActivityRepository
6
6
 
7
7
 
8
8
  class EddingtonController:
@@ -10,7 +10,9 @@ class EddingtonController:
10
10
  self._repository = repository
11
11
 
12
12
  def render(self) -> dict:
13
- activities = self._repository.meta.copy()
13
+ activities = self._repository.meta.loc[
14
+ self._repository.meta["consider_for_achievements"]
15
+ ].copy()
14
16
  activities["day"] = [start.date() for start in activities["start"]]
15
17
 
16
18
  sum_per_day = activities.groupby("day").apply(
@@ -37,7 +39,7 @@ class EddingtonController:
37
39
  width=1000,
38
40
  title=f"Eddington Number {en}",
39
41
  )
40
- .mark_bar()
42
+ .mark_area(interpolate="step")
41
43
  .encode(
42
44
  alt.X(
43
45
  "distance_km",
@@ -46,7 +48,7 @@ class EddingtonController:
46
48
  ),
47
49
  alt.Y(
48
50
  "total",
49
- scale=alt.Scale(type="log"),
51
+ scale=alt.Scale(domainMax=en + 10),
50
52
  title="Days exceeding distance",
51
53
  ),
52
54
  [
@@ -62,8 +64,14 @@ class EddingtonController:
62
64
  .encode(alt.X("distance_km"), alt.Y("total"))
63
65
  )
64
66
  )
65
- .interactive(bind_y=False)
67
+ .interactive()
66
68
  .to_json(format="vega")
67
69
  )
68
70
 
69
- return {"eddington_number": en, "logarithmic_plot": logarithmic_plot}
71
+ return {
72
+ "eddington_number": en,
73
+ "logarithmic_plot": logarithmic_plot,
74
+ "eddington_table": eddington.loc[
75
+ (eddington["distance_km"] > en) & (eddington["distance_km"] <= en + 10)
76
+ ].to_dict(orient="records"),
77
+ }
@@ -0,0 +1,56 @@
1
+ {% extends "page.html.j2" %}
2
+
3
+ {% block container %}
4
+ <h1 class="mb-3">Eddington Number</h1>
5
+
6
+ <div class="row mb-3">
7
+ <div class="col-md-4">
8
+ <h2>Definition</h2>
9
+
10
+ <p>Your Eddington number is <b>{{ eddington_number }}</b>.</p>
11
+
12
+ <p>That means that on {{ eddington_number }} separate days you
13
+ have recorded activities exceeding {{ eddington_number }} km. Going far is one thing, going often is
14
+ another. But going far often is hard. Also if you increment the Eddington number, all days with less
15
+ distance will not count any more. It becomes increasingly hard to increment the Eddington number because you
16
+ don't only need to achieve a higher count, but all shorter activities don't count towards the bigger number.
17
+ </p>
18
+ </div>
19
+
20
+ <div class="col-md-8">
21
+ <h2>Missing days</h2>
22
+
23
+ <p>How many more days do you need to increment your Eddington number?</p>
24
+
25
+ <table class="table">
26
+ <thead>
27
+ <tr>
28
+ <th>Distance / km</th>
29
+ <th>Count</th>
30
+ <th>Missing days</th>
31
+ </tr>
32
+ </thead>
33
+ <tbody>
34
+ {% for row in eddington_table %}
35
+ <tr>
36
+ <td>{{ row['distance_km'] }}</td>
37
+ <td>{{ row['total'] }}</td>
38
+ <td>{{ row['missing'] }}</td>
39
+ </tr>
40
+ {% endfor %}
41
+ </tbody>
42
+ </table>
43
+ </div>
44
+ </div>
45
+
46
+ <div class="row mb-1">
47
+ <div class="col">
48
+ <h2>Plot</h2>
49
+
50
+ <p>In a graphical representation, the Eddington number is the distance where the red line intersects with the
51
+ blue area.</p>
52
+
53
+ {{ vega_direct("eddington-log", logarithmic_plot) }}
54
+ </div>
55
+ </div>
56
+ {% endblock %}
@@ -20,7 +20,9 @@ class EntryController:
20
20
  "latest_activities": [],
21
21
  }
22
22
 
23
- for activity in itertools.islice(self._repository.iter_activities(), 15):
23
+ for activity in itertools.islice(
24
+ self._repository.iter_activities(dropna=True), 15
25
+ ):
24
26
  time_series = self._repository.get_time_series(activity["id"])
25
27
  result["latest_activities"].append(
26
28
  {
@@ -33,7 +35,7 @@ class EntryController:
33
35
 
34
36
  def distance_last_30_days_meta_plot(meta: pd.DataFrame) -> str:
35
37
  before_30_days = pd.to_datetime(
36
- datetime.datetime.utcnow() - datetime.timedelta(days=31), utc=True
38
+ datetime.datetime.now() - datetime.timedelta(days=31)
37
39
  )
38
40
  return (
39
41
  alt.Chart(
File without changes
@@ -0,0 +1,19 @@
1
+ from flask import Blueprint
2
+ from flask import render_template
3
+
4
+ from ...core.activities import ActivityRepository
5
+ from .controller import EquipmentController
6
+
7
+
8
+ def make_equipment_blueprint(repository: ActivityRepository) -> Blueprint:
9
+ blueprint = Blueprint("equipment", __name__, template_folder="templates")
10
+
11
+ equipment_controller = EquipmentController(repository)
12
+
13
+ @blueprint.route("/")
14
+ def index():
15
+ return render_template(
16
+ "equipment/index.html.j2", **equipment_controller.render()
17
+ )
18
+
19
+ return blueprint