geo-activity-playground 0.27.1__py3-none-any.whl → 0.28.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 +1 -0
- geo_activity_playground/core/tasks.py +2 -2
- geo_activity_playground/explorer/tile_visits.py +135 -134
- geo_activity_playground/webui/activity/controller.py +29 -13
- geo_activity_playground/webui/activity/templates/activity/show.html.j2 +6 -11
- geo_activity_playground/webui/app.py +10 -2
- 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 +49 -0
- geo_activity_playground/webui/equipment/controller.py +2 -1
- geo_activity_playground/webui/explorer/controller.py +3 -3
- geo_activity_playground/webui/heatmap/heatmap_controller.py +19 -6
- geo_activity_playground/webui/settings/blueprint.py +34 -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/index.html.j2 +18 -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/templates/page.html.j2 +1 -1
- 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.28.0.dist-info}/METADATA +1 -1
- {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.28.0.dist-info}/RECORD +29 -24
- {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.28.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.28.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.28.0.dist-info}/entry_points.txt +0 -0
@@ -109,10 +109,9 @@ def make_activity_repository(
|
|
109
109
|
|
110
110
|
|
111
111
|
def main_cache(basedir: pathlib.Path) -> None:
|
112
|
-
repository, tile_visit_accessor, config_accessor = make_activity_repository(
|
112
|
+
(repository, tile_visit_accessor, config_accessor) = make_activity_repository(
|
113
113
|
basedir, False
|
114
114
|
)
|
115
|
-
scan_for_activities(repository, tile_visit_accessor, config_accessor())
|
116
115
|
|
117
116
|
|
118
117
|
if __name__ == "__main__":
|
@@ -128,11 +128,11 @@ class ActivityRepository:
|
|
128
128
|
else:
|
129
129
|
return None
|
130
130
|
|
131
|
-
def get_activity_ids(self, only_achievements: bool = False) ->
|
131
|
+
def get_activity_ids(self, only_achievements: bool = False) -> list[int]:
|
132
132
|
if only_achievements:
|
133
|
-
return
|
133
|
+
return list(self.meta.loc[self.meta["consider_for_achievements"]].index)
|
134
134
|
else:
|
135
|
-
return
|
135
|
+
return list(self.meta.index)
|
136
136
|
|
137
137
|
def iter_activities(self, new_to_old=True, dropna=False) -> Iterator[ActivityMeta]:
|
138
138
|
direction = -1 if new_to_old else 1
|
@@ -33,6 +33,7 @@ class Config:
|
|
33
33
|
privacy_zones: dict[str, list[list[float]]] = dataclasses.field(
|
34
34
|
default_factory=dict
|
35
35
|
)
|
36
|
+
sharepic_suppressed_fields: list[str] = dataclasses.field(default_factory=list)
|
36
37
|
strava_client_id: int = 131693
|
37
38
|
strava_client_secret: str = "0ccc0100a2c218512a7ef0cea3b0e322fb4b4365"
|
38
39
|
strava_client_code: Optional[str] = None
|
@@ -59,8 +59,8 @@ class WorkTracker:
|
|
59
59
|
else:
|
60
60
|
self._done = set()
|
61
61
|
|
62
|
-
def filter(self, ids: Iterable) ->
|
63
|
-
return
|
62
|
+
def filter(self, ids: Iterable) -> list:
|
63
|
+
return [elem for elem in ids if elem not in self._done]
|
64
64
|
|
65
65
|
def mark_done(self, id: int) -> None:
|
66
66
|
self._done.add(id)
|
@@ -25,139 +25,159 @@ from geo_activity_playground.core.tiles import interpolate_missing_tile
|
|
25
25
|
logger = logging.getLogger(__name__)
|
26
26
|
|
27
27
|
|
28
|
+
class TileInfo(TypedDict):
|
29
|
+
activity_ids: set[int]
|
30
|
+
first_time: datetime.datetime
|
31
|
+
first_id: int
|
32
|
+
last_time: datetime.datetime
|
33
|
+
last_id: int
|
34
|
+
|
35
|
+
|
28
36
|
class TileHistoryRow(TypedDict):
|
37
|
+
activity_id: int
|
29
38
|
time: datetime.datetime
|
30
39
|
tile_x: int
|
31
40
|
tile_y: int
|
32
41
|
|
33
42
|
|
43
|
+
class TileEvolutionState:
|
44
|
+
def __init__(self) -> None:
|
45
|
+
self.num_neighbors: dict[tuple[int, int], int] = {}
|
46
|
+
self.memberships: dict[tuple[int, int], tuple[int, int]] = {}
|
47
|
+
self.clusters: dict[tuple[int, int], list[tuple[int, int]]] = {}
|
48
|
+
self.cluster_evolution = pd.DataFrame()
|
49
|
+
self.square_start = 0
|
50
|
+
self.cluster_start = 0
|
51
|
+
self.max_square_size = 0
|
52
|
+
self.visited_tiles: set[tuple[int, int]] = set()
|
53
|
+
self.square_evolution = pd.DataFrame()
|
54
|
+
self.square_x: Optional[int] = None
|
55
|
+
self.square_y: Optional[int] = None
|
56
|
+
|
57
|
+
|
58
|
+
class TileState(TypedDict):
|
59
|
+
tile_visits: dict[int, dict[tuple[int, int], TileInfo]]
|
60
|
+
tile_history: dict[int, pd.DataFrame]
|
61
|
+
activities_per_tile: dict[int, set[int]]
|
62
|
+
processed_activities: set[int]
|
63
|
+
evolution_state: dict[int, TileEvolutionState]
|
64
|
+
version: int
|
65
|
+
|
66
|
+
|
67
|
+
TILE_STATE_VERSION = 2
|
68
|
+
|
69
|
+
|
34
70
|
class TileVisitAccessor:
|
35
|
-
|
36
|
-
TILE_HISTORIES_PATH = pathlib.Path(f"Cache/tile-history.pickle")
|
37
|
-
TILE_VISITS_PATH = pathlib.Path(f"Cache/tile-visits.pickle")
|
38
|
-
ACTIVITIES_PER_TILE_PATH = pathlib.Path(f"Cache/activities-per-tile.pickle")
|
71
|
+
PATH = pathlib.Path("Cache/tile-state-2.pickle")
|
39
72
|
|
40
73
|
def __init__(self) -> None:
|
41
|
-
self.
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
) or collections.defaultdict(pd.DataFrame)
|
49
|
-
|
50
|
-
self.states = try_load_pickle(
|
51
|
-
self.TILE_EVOLUTION_STATES_PATH
|
52
|
-
) or collections.defaultdict(TileEvolutionState)
|
53
|
-
|
54
|
-
self.activities_per_tile: dict[
|
55
|
-
int, dict[tuple[int, int], set[int]]
|
56
|
-
] = try_load_pickle(self.ACTIVITIES_PER_TILE_PATH) or collections.defaultdict(
|
57
|
-
dict
|
58
|
-
)
|
74
|
+
self.tile_state: TileState = try_load_pickle(self.PATH)
|
75
|
+
if (
|
76
|
+
self.tile_state is None
|
77
|
+
or self.tile_state.get("version", None) != TILE_STATE_VERSION
|
78
|
+
):
|
79
|
+
self.tile_state = make_tile_state()
|
80
|
+
# TODO: Reset work tracker
|
59
81
|
|
60
82
|
def save(self) -> None:
|
61
|
-
|
62
|
-
|
83
|
+
tmp_path = self.PATH.with_suffix(".tmp")
|
84
|
+
with open(tmp_path, "wb") as f:
|
85
|
+
pickle.dump(self.tile_state, f)
|
86
|
+
tmp_path.rename(self.PATH)
|
63
87
|
|
64
|
-
with open(self.TILE_HISTORIES_PATH, "wb") as f:
|
65
|
-
pickle.dump(self.histories, f)
|
66
88
|
|
67
|
-
|
68
|
-
|
89
|
+
def make_defaultdict_dict():
|
90
|
+
return collections.defaultdict(dict)
|
69
91
|
|
70
|
-
with open(self.ACTIVITIES_PER_TILE_PATH, "wb") as f:
|
71
|
-
pickle.dump(self.activities_per_tile, f)
|
72
92
|
|
93
|
+
def make_defaultdict_set():
|
94
|
+
return collections.defaultdict(set)
|
95
|
+
|
96
|
+
|
97
|
+
def make_tile_state() -> TileState:
|
98
|
+
tile_state: TileState = {
|
99
|
+
"tile_visits": collections.defaultdict(make_defaultdict_dict),
|
100
|
+
"tile_history": collections.defaultdict(pd.DataFrame),
|
101
|
+
"activities_per_tile": collections.defaultdict(make_defaultdict_set),
|
102
|
+
"processed_activities": set(),
|
103
|
+
"evolution_state": collections.defaultdict(TileEvolutionState),
|
104
|
+
"version": TILE_STATE_VERSION,
|
105
|
+
}
|
106
|
+
return tile_state
|
73
107
|
|
74
|
-
def compute_tile_visits(
|
75
|
-
repository: ActivityRepository, tile_visits_accessor: TileVisitAccessor
|
76
|
-
) -> None:
|
77
|
-
present_activity_ids = repository.get_activity_ids()
|
78
|
-
work_tracker = WorkTracker(work_tracker_path("tile-visits"))
|
79
|
-
|
80
|
-
changed_zoom_tile = collections.defaultdict(set)
|
81
|
-
|
82
|
-
# Delete visits from removed activities.
|
83
|
-
for zoom, activities_per_tile in tile_visits_accessor.activities_per_tile.items():
|
84
|
-
for tile, activity_ids in activities_per_tile.items():
|
85
|
-
deleted_ids = activity_ids - present_activity_ids
|
86
|
-
if deleted_ids:
|
87
|
-
logger.debug(
|
88
|
-
f"Removing activities {deleted_ids} from tile {tile} at {zoom=}."
|
89
|
-
)
|
90
|
-
for activity_id in deleted_ids:
|
91
|
-
activity_ids.remove(activity_id)
|
92
|
-
work_tracker.discard(activity_id)
|
93
|
-
changed_zoom_tile[zoom].add(tile)
|
94
108
|
|
95
|
-
|
96
|
-
|
109
|
+
def compute_tile_visits_new(
|
110
|
+
repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
|
111
|
+
) -> None:
|
112
|
+
work_tracker = WorkTracker(work_tracker_path("tile-state"))
|
97
113
|
for activity_id in tqdm(
|
98
|
-
|
114
|
+
work_tracker.filter(repository.get_activity_ids()), desc="Tile visits (new)"
|
99
115
|
):
|
100
|
-
|
101
|
-
for time, tile_x, tile_y in _tiles_from_points(
|
102
|
-
repository.get_time_series(activity_id), zoom
|
103
|
-
):
|
104
|
-
tile = (tile_x, tile_y)
|
105
|
-
if tile not in tile_visits_accessor.activities_per_tile[zoom]:
|
106
|
-
tile_visits_accessor.activities_per_tile[zoom][tile] = set()
|
107
|
-
tile_visits_accessor.activities_per_tile[zoom][tile].add(activity_id)
|
108
|
-
changed_zoom_tile[zoom].add(tile)
|
116
|
+
do_tile_stuff(repository, tile_visit_accessor.tile_state, activity_id)
|
109
117
|
work_tracker.mark_done(activity_id)
|
110
|
-
|
111
|
-
# Update tile visits structure.
|
112
|
-
for zoom, changed_tiles in tqdm(
|
113
|
-
changed_zoom_tile.items(), desc="Incorporate changes in tiles"
|
114
|
-
):
|
115
|
-
soa = {"activity_id": [], "time": [], "tile_x": [], "tile_y": []}
|
116
|
-
|
117
|
-
for tile in changed_tiles:
|
118
|
-
activity_ids = tile_visits_accessor.activities_per_tile[zoom][tile]
|
119
|
-
activities = [
|
120
|
-
repository.get_activity_by_id(activity_id)
|
121
|
-
for activity_id in activity_ids
|
122
|
-
]
|
123
|
-
activities_to_consider = [
|
124
|
-
activity
|
125
|
-
for activity in activities
|
126
|
-
if activity["consider_for_achievements"]
|
127
|
-
]
|
128
|
-
activities_to_consider.sort(key=lambda activity: activity["start"])
|
129
|
-
|
130
|
-
if activities_to_consider:
|
131
|
-
tile_visits_accessor.visits[zoom][tile] = {
|
132
|
-
"first_time": activities_to_consider[0]["start"],
|
133
|
-
"first_id": activities_to_consider[0]["id"],
|
134
|
-
"last_time": activities_to_consider[-1]["start"],
|
135
|
-
"last_id": activities_to_consider[-1]["id"],
|
136
|
-
"activity_ids": {
|
137
|
-
activity["id"] for activity in activities_to_consider
|
138
|
-
},
|
139
|
-
}
|
140
|
-
|
141
|
-
soa["activity_id"].append(activities_to_consider[0]["id"])
|
142
|
-
soa["time"].append(activities_to_consider[0]["start"])
|
143
|
-
soa["tile_x"].append(tile[0])
|
144
|
-
soa["tile_y"].append(tile[1])
|
145
|
-
else:
|
146
|
-
if tile in tile_visits_accessor.visits[zoom]:
|
147
|
-
del tile_visits_accessor.visits[zoom][tile]
|
148
|
-
|
149
|
-
df = pd.DataFrame(soa)
|
150
|
-
if len(df) > 0:
|
151
|
-
df = pd.concat([tile_visits_accessor.histories[zoom], df])
|
152
|
-
df.sort_values("time", inplace=True)
|
153
|
-
tile_visits_accessor.histories[zoom] = df.groupby(
|
154
|
-
["tile_x", "tile_y"]
|
155
|
-
).head(1)
|
156
|
-
|
157
|
-
tile_visits_accessor.save()
|
118
|
+
tile_visit_accessor.save()
|
158
119
|
work_tracker.close()
|
159
120
|
|
160
121
|
|
122
|
+
def do_tile_stuff(
|
123
|
+
repository: ActivityRepository, tile_state: TileState, activity_id: int
|
124
|
+
) -> None:
|
125
|
+
activity = repository.get_activity_by_id(activity_id)
|
126
|
+
time_series = repository.get_time_series(activity_id)
|
127
|
+
|
128
|
+
activity_tiles = pd.DataFrame(
|
129
|
+
_tiles_from_points(time_series, 19), columns=["time", "tile_x", "tile_y"]
|
130
|
+
)
|
131
|
+
for zoom in reversed(range(20)):
|
132
|
+
activities_per_tile = tile_state["activities_per_tile"][zoom]
|
133
|
+
|
134
|
+
new_tile_history_soa = {
|
135
|
+
"activity_id": [],
|
136
|
+
"time": [],
|
137
|
+
"tile_x": [],
|
138
|
+
"tile_y": [],
|
139
|
+
}
|
140
|
+
|
141
|
+
activity_tiles = activity_tiles.groupby(["tile_x", "tile_y"]).head(1)
|
142
|
+
|
143
|
+
for time, tile in zip(
|
144
|
+
activity_tiles["time"],
|
145
|
+
zip(activity_tiles["tile_x"], activity_tiles["tile_y"]),
|
146
|
+
):
|
147
|
+
if activity["consider_for_achievements"]:
|
148
|
+
if tile not in activities_per_tile:
|
149
|
+
new_tile_history_soa["activity_id"].append(activity_id)
|
150
|
+
new_tile_history_soa["time"].append(time)
|
151
|
+
new_tile_history_soa["tile_x"].append(tile[0])
|
152
|
+
new_tile_history_soa["tile_y"].append(tile[1])
|
153
|
+
|
154
|
+
tile_visit = tile_state["tile_visits"][zoom][tile]
|
155
|
+
if not tile_visit:
|
156
|
+
tile_visit["activity_ids"] = {activity_id}
|
157
|
+
else:
|
158
|
+
tile_visit["activity_ids"].add(activity_id)
|
159
|
+
|
160
|
+
first_time = tile_visit.get("first_time", None)
|
161
|
+
last_time = tile_visit.get("last_time", None)
|
162
|
+
if first_time is None or time < first_time:
|
163
|
+
tile_visit["first_id"] = activity_id
|
164
|
+
tile_visit["first_time"] = time
|
165
|
+
if last_time is None or time > last_time:
|
166
|
+
tile_visit["last_id"] = activity_id
|
167
|
+
tile_visit["last_time"] = time
|
168
|
+
|
169
|
+
activities_per_tile[tile].add(activity_id)
|
170
|
+
|
171
|
+
if new_tile_history_soa["activity_id"]:
|
172
|
+
tile_state["tile_history"][zoom] = pd.concat(
|
173
|
+
[tile_state["tile_history"][zoom], pd.DataFrame(new_tile_history_soa)]
|
174
|
+
)
|
175
|
+
|
176
|
+
# Move up one layer in the quad-tree.
|
177
|
+
activity_tiles["tile_x"] //= 2
|
178
|
+
activity_tiles["tile_y"] //= 2
|
179
|
+
|
180
|
+
|
161
181
|
def _tiles_from_points(
|
162
182
|
time_series: pd.DataFrame, zoom: int
|
163
183
|
) -> Iterator[tuple[datetime.datetime, int, int]]:
|
@@ -181,38 +201,19 @@ def _tiles_from_points(
|
|
181
201
|
yield (t1,) + interpolated
|
182
202
|
|
183
203
|
|
184
|
-
|
185
|
-
def __init__(self) -> None:
|
186
|
-
self.num_neighbors: dict[tuple[int, int], int] = {}
|
187
|
-
self.memberships: dict[tuple[int, int], tuple[int, int]] = {}
|
188
|
-
self.clusters: dict[tuple[int, int], list[tuple[int, int]]] = {}
|
189
|
-
self.cluster_evolution = pd.DataFrame()
|
190
|
-
self.square_start = 0
|
191
|
-
self.cluster_start = 0
|
192
|
-
self.max_square_size = 0
|
193
|
-
self.visited_tiles: set[tuple[int, int]] = set()
|
194
|
-
self.square_evolution = pd.DataFrame()
|
195
|
-
self.square_x: Optional[int] = None
|
196
|
-
self.square_y: Optional[int] = None
|
197
|
-
|
198
|
-
|
199
|
-
def compute_tile_evolution(
|
200
|
-
tile_visits_accessor: TileVisitAccessor, config: Config
|
201
|
-
) -> None:
|
204
|
+
def compute_tile_evolution(tile_state: TileState, config: Config) -> None:
|
202
205
|
for zoom in config.explorer_zoom_levels:
|
203
206
|
_compute_cluster_evolution(
|
204
|
-
|
205
|
-
|
207
|
+
tile_state["tile_history"][zoom],
|
208
|
+
tile_state["evolution_state"][zoom],
|
206
209
|
zoom,
|
207
210
|
)
|
208
211
|
_compute_square_history(
|
209
|
-
|
210
|
-
|
212
|
+
tile_state["tile_history"][zoom],
|
213
|
+
tile_state["evolution_state"][zoom],
|
211
214
|
zoom,
|
212
215
|
)
|
213
216
|
|
214
|
-
tile_visits_accessor.save()
|
215
|
-
|
216
217
|
|
217
218
|
def _compute_cluster_evolution(
|
218
219
|
tiles: pd.DataFrame, s: TileEvolutionState, zoom: int
|
@@ -61,7 +61,9 @@ class ActivityController:
|
|
61
61
|
|
62
62
|
new_tiles = {
|
63
63
|
zoom: sum(
|
64
|
-
self._tile_visit_accessor.
|
64
|
+
self._tile_visit_accessor.tile_state["tile_history"][zoom][
|
65
|
+
"activity_id"
|
66
|
+
]
|
65
67
|
== activity["id"]
|
66
68
|
)
|
67
69
|
for zoom in [14, 17]
|
@@ -100,7 +102,9 @@ class ActivityController:
|
|
100
102
|
time_series = privacy_zone.filter_time_series(time_series)
|
101
103
|
if len(time_series) == 0:
|
102
104
|
time_series = self._repository.get_time_series(id)
|
103
|
-
return make_sharepic(
|
105
|
+
return make_sharepic(
|
106
|
+
activity, time_series, self._config.sharepic_suppressed_fields
|
107
|
+
)
|
104
108
|
|
105
109
|
def render_day(self, year: int, month: int, day: int) -> dict:
|
106
110
|
meta = self._repository.meta
|
@@ -411,7 +415,11 @@ def pixels_in_bounds(bounds: PixelBounds) -> int:
|
|
411
415
|
return (bounds.x_max - bounds.x_min) * (bounds.y_max - bounds.y_min)
|
412
416
|
|
413
417
|
|
414
|
-
def make_sharepic(
|
418
|
+
def make_sharepic(
|
419
|
+
activity: ActivityMeta,
|
420
|
+
time_series: pd.DataFrame,
|
421
|
+
sharepic_suppressed_fields: list[str],
|
422
|
+
) -> bytes:
|
415
423
|
lat_lon_data = np.array([time_series["latitude"], time_series["longitude"]]).T
|
416
424
|
|
417
425
|
geo_bounds = get_bounds(lat_lon_data)
|
@@ -448,19 +456,27 @@ def make_sharepic(activity: ActivityMeta, time_series: pd.DataFrame) -> bytes:
|
|
448
456
|
draw.line(yx, fill="red", width=4)
|
449
457
|
|
450
458
|
draw.rectangle([0, img.height - 70, img.width, img.height], fill=(0, 0, 0, 128))
|
451
|
-
|
452
|
-
|
453
|
-
f"{activity['
|
454
|
-
f"{activity['
|
455
|
-
f"
|
456
|
-
|
457
|
-
|
459
|
+
|
460
|
+
facts = {
|
461
|
+
"kind": f"{activity['kind']}",
|
462
|
+
"start": f"{activity['start'].date()}",
|
463
|
+
"equipment": f"{activity['equipment']}",
|
464
|
+
"distance_km": f"\n{activity['distance_km']:.1f} km",
|
465
|
+
"elapsed_time": re.sub(r"^0 days ", "", f"{activity['elapsed_time']}"),
|
466
|
+
}
|
467
|
+
|
458
468
|
if activity.get("calories", 0) and not pd.isna(activity["calories"]):
|
459
|
-
facts
|
469
|
+
facts["calories"] = f"{activity['calories']:.0f} kcal"
|
460
470
|
if activity.get("steps", 0) and not pd.isna(activity["steps"]):
|
461
|
-
facts
|
471
|
+
facts["steps"] = f"{activity['steps']:.0f} steps"
|
472
|
+
|
473
|
+
facts = {
|
474
|
+
key: value
|
475
|
+
for key, value in facts.items()
|
476
|
+
if not key in sharepic_suppressed_fields
|
477
|
+
}
|
462
478
|
|
463
|
-
draw.text((35, img.height - 70 + 10), " ".join(facts), font_size=20)
|
479
|
+
draw.text((35, img.height - 70 + 10), " ".join(facts.values()), font_size=20)
|
464
480
|
|
465
481
|
# img_array = np.array(img) / 255
|
466
482
|
|
@@ -111,11 +111,7 @@
|
|
111
111
|
</div>
|
112
112
|
|
113
113
|
{% if heartrate_time_plot is defined %}
|
114
|
-
<
|
115
|
-
<div class="col">
|
116
|
-
<h2>Heart rate</h2>
|
117
|
-
</div>
|
118
|
-
</div>
|
114
|
+
<h2>Heart rate</h2>
|
119
115
|
|
120
116
|
<div class="row mb-3">
|
121
117
|
<div class="col-md-4">
|
@@ -133,13 +129,12 @@
|
|
133
129
|
</div>
|
134
130
|
{% endif %}
|
135
131
|
|
136
|
-
<
|
137
|
-
<div class="col">
|
138
|
-
<h2>Share picture</h2>
|
132
|
+
<h2>Share picture</h2>
|
139
133
|
|
140
|
-
|
141
|
-
|
142
|
-
|
134
|
+
<p><img src="{{ url_for('.sharepic', id=activity.id) }}" /></p>
|
135
|
+
|
136
|
+
<p>Not happy with the displayed data? <a href="{{ url_for('settings.sharepic') }}">Change share picture
|
137
|
+
settings</a>.</p>
|
143
138
|
|
144
139
|
{% if similar_activites|length > 0 %}
|
145
140
|
<div class="row mb-3">
|
@@ -22,6 +22,8 @@ from .summary.blueprint import make_summary_blueprint
|
|
22
22
|
from .tile.blueprint import make_tile_blueprint
|
23
23
|
from .upload.blueprint import make_upload_blueprint
|
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
|
|
@@ -71,9 +73,13 @@ def web_ui_main(
|
|
71
73
|
app.config["UPLOAD_FOLDER"] = "Activities"
|
72
74
|
app.secret_key = get_secret_key()
|
73
75
|
|
76
|
+
authenticator = Authenticator(config_accessor())
|
77
|
+
|
74
78
|
route_search(app, repository)
|
75
79
|
route_start(app, repository)
|
76
80
|
|
81
|
+
app.register_blueprint(make_auth_blueprint(authenticator), url_prefix="/auth")
|
82
|
+
|
77
83
|
app.register_blueprint(
|
78
84
|
make_activity_blueprint(
|
79
85
|
repository,
|
@@ -97,7 +103,7 @@ def web_ui_main(
|
|
97
103
|
make_heatmap_blueprint(repository, tile_visit_accessor), url_prefix="/heatmap"
|
98
104
|
)
|
99
105
|
app.register_blueprint(
|
100
|
-
make_settings_blueprint(config_accessor),
|
106
|
+
make_settings_blueprint(config_accessor, authenticator),
|
101
107
|
url_prefix="/settings",
|
102
108
|
)
|
103
109
|
app.register_blueprint(
|
@@ -110,7 +116,9 @@ def web_ui_main(
|
|
110
116
|
)
|
111
117
|
app.register_blueprint(make_tile_blueprint(), url_prefix="/tile")
|
112
118
|
app.register_blueprint(
|
113
|
-
make_upload_blueprint(
|
119
|
+
make_upload_blueprint(
|
120
|
+
repository, tile_visit_accessor, config_accessor(), authenticator
|
121
|
+
),
|
114
122
|
url_prefix="/upload",
|
115
123
|
)
|
116
124
|
|
@@ -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,49 @@
|
|
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
|
+
print(
|
18
|
+
f"Password={self._config.upload_password}, Session={session.get('is_authenticated', False)}"
|
19
|
+
)
|
20
|
+
return not self._config.upload_password or session.get(
|
21
|
+
"is_authenticated", False
|
22
|
+
)
|
23
|
+
|
24
|
+
def authenticate(self, password: str) -> None:
|
25
|
+
if password == self._config.upload_password:
|
26
|
+
session["is_authenticated"] = True
|
27
|
+
session.permanent = True
|
28
|
+
flash("Login successful.", category="success")
|
29
|
+
else:
|
30
|
+
flash("Incorrect password.", category="warning")
|
31
|
+
|
32
|
+
def logout(self) -> None:
|
33
|
+
session["is_authenticated"] = False
|
34
|
+
flash("Logout successful.", category="success")
|
35
|
+
|
36
|
+
|
37
|
+
def needs_authentication(authenticator: Authenticator) -> Callable:
|
38
|
+
def decorator(route: Callable) -> Callable:
|
39
|
+
@functools.wraps(route)
|
40
|
+
def wrapped_route(*args, **kwargs):
|
41
|
+
if authenticator.is_authenticated():
|
42
|
+
return route(*args, **kwargs)
|
43
|
+
else:
|
44
|
+
flash("You need to be logged in to view that site.", category="Warning")
|
45
|
+
return redirect(url_for("auth.index"))
|
46
|
+
|
47
|
+
return wrapped_route
|
48
|
+
|
49
|
+
return decorator
|
@@ -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(
|
@@ -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,9 @@ 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
|
+
tmp_path.rename(tile_count_cache_path)
|
114
127
|
return tile_counts
|
115
128
|
|
116
129
|
def _render_tile_image(
|
@@ -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,29 @@ 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
|
+
|
30
45
|
@blueprint.route("/equipment-offsets", methods=["GET", "POST"])
|
46
|
+
@needs_authentication(authenticator)
|
31
47
|
def equipment_offsets():
|
32
48
|
if request.method == "POST":
|
33
49
|
equipments = request.form.getlist("equipment")
|
@@ -39,6 +55,7 @@ def make_settings_blueprint(config_accessor: ConfigAccessor) -> Blueprint:
|
|
39
55
|
)
|
40
56
|
|
41
57
|
@blueprint.route("/heart-rate", methods=["GET", "POST"])
|
58
|
+
@needs_authentication(authenticator)
|
42
59
|
def heart_rate():
|
43
60
|
if request.method == "POST":
|
44
61
|
birth_year = int_or_none(request.form["birth_year"])
|
@@ -54,6 +71,7 @@ def make_settings_blueprint(config_accessor: ConfigAccessor) -> Blueprint:
|
|
54
71
|
)
|
55
72
|
|
56
73
|
@blueprint.route("/kinds-without-achievements", methods=["GET", "POST"])
|
74
|
+
@needs_authentication(authenticator)
|
57
75
|
def kinds_without_achievements():
|
58
76
|
if request.method == "POST":
|
59
77
|
kinds = request.form.getlist("kind")
|
@@ -64,6 +82,7 @@ def make_settings_blueprint(config_accessor: ConfigAccessor) -> Blueprint:
|
|
64
82
|
)
|
65
83
|
|
66
84
|
@blueprint.route("/metadata-extraction", methods=["GET", "POST"])
|
85
|
+
@needs_authentication(authenticator)
|
67
86
|
def metadata_extraction():
|
68
87
|
if request.method == "POST":
|
69
88
|
regexes = request.form.getlist("regex")
|
@@ -74,6 +93,7 @@ def make_settings_blueprint(config_accessor: ConfigAccessor) -> Blueprint:
|
|
74
93
|
)
|
75
94
|
|
76
95
|
@blueprint.route("/privacy-zones", methods=["GET", "POST"])
|
96
|
+
@needs_authentication(authenticator)
|
77
97
|
def privacy_zones():
|
78
98
|
if request.method == "POST":
|
79
99
|
zone_names = request.form.getlist("zone_name")
|
@@ -84,7 +104,19 @@ def make_settings_blueprint(config_accessor: ConfigAccessor) -> Blueprint:
|
|
84
104
|
**settings_controller.render_privacy_zones(),
|
85
105
|
)
|
86
106
|
|
107
|
+
@blueprint.route("/sharepic", methods=["GET", "POST"])
|
108
|
+
@needs_authentication(authenticator)
|
109
|
+
def sharepic():
|
110
|
+
if request.method == "POST":
|
111
|
+
names = request.form.getlist("name")
|
112
|
+
settings_controller.save_sharepic(names)
|
113
|
+
return render_template(
|
114
|
+
"settings/sharepic.html.j2",
|
115
|
+
**settings_controller.render_sharepic(),
|
116
|
+
)
|
117
|
+
|
87
118
|
@blueprint.route("/strava", methods=["GET", "POST"])
|
119
|
+
@needs_authentication(authenticator)
|
88
120
|
def strava():
|
89
121
|
if request.method == "POST":
|
90
122
|
strava_client_id = request.form["strava_client_id"]
|
@@ -98,6 +130,7 @@ def make_settings_blueprint(config_accessor: ConfigAccessor) -> Blueprint:
|
|
98
130
|
)
|
99
131
|
|
100
132
|
@blueprint.route("/strava-callback")
|
133
|
+
@needs_authentication(authenticator)
|
101
134
|
def strava_callback():
|
102
135
|
code = request.args.get("code", type=str)
|
103
136
|
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 %}
|
@@ -11,6 +11,15 @@
|
|
11
11
|
|
12
12
|
<div class="row mb-3">
|
13
13
|
<div class="row row-cols-1 row-cols-md-3 g-3">
|
14
|
+
<div class="col">
|
15
|
+
<div class="card">
|
16
|
+
<div class="card-body">
|
17
|
+
<h5 class="card-title">Admin password</h5>
|
18
|
+
<p class="card-text">Running a public instance and want to protect it?</p>
|
19
|
+
<a href="{{ url_for('.admin_password') }}" class="btn btn-primary">Set up admin password</a>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
</div>
|
14
23
|
<div class="col">
|
15
24
|
<div class="card">
|
16
25
|
<div class="card-body">
|
@@ -60,6 +69,15 @@
|
|
60
69
|
</div>
|
61
70
|
</div>
|
62
71
|
</div>
|
72
|
+
<div class="col">
|
73
|
+
<div class="card">
|
74
|
+
<div class="card-body">
|
75
|
+
<h5 class="card-title">Share picture</h5>
|
76
|
+
<p class="card-text">Select which metadata shall be displayed on share pictures.</p>
|
77
|
+
<a href="{{ url_for('.sharepic') }}" class="btn btn-primary">Set up share picture</a>
|
78
|
+
</div>
|
79
|
+
</div>
|
80
|
+
</div>
|
63
81
|
<div class="col">
|
64
82
|
<div class="card">
|
65
83
|
<div class="card-body">
|
@@ -0,0 +1,22 @@
|
|
1
|
+
{% extends "page.html.j2" %}
|
2
|
+
|
3
|
+
{% block container %}
|
4
|
+
|
5
|
+
<h1 class="mb-3">Share Picture</h1>
|
6
|
+
|
7
|
+
<p>What data fields shall be displayed on the share picture?</p>
|
8
|
+
|
9
|
+
<form method="POST">
|
10
|
+
{% for name, label, checked in names %}
|
11
|
+
<div class="form-check form-check-inline form-switch">
|
12
|
+
<input class="form-check-input" type="checkbox" role="switch" id="{{ name }}" name="name" value="{{ name }}"
|
13
|
+
{{ 'checked' if checked else '' }} />
|
14
|
+
<label class="form-check-label" for="{{ name }}">{{ label }}</label>
|
15
|
+
</div>
|
16
|
+
{% endfor %}
|
17
|
+
<div class="clearfix mb-3"></div>
|
18
|
+
<button type="submit" class="btn btn-primary">Save</button>
|
19
|
+
</form>
|
20
|
+
|
21
|
+
|
22
|
+
{% endblock %}
|
@@ -18,7 +18,7 @@ class SquarePlannerController:
|
|
18
18
|
self._repository = repository
|
19
19
|
self._tile_visit_accessor = tile_visit_accessor
|
20
20
|
|
21
|
-
self._tile_visits = self._tile_visit_accessor.
|
21
|
+
self._tile_visits = self._tile_visit_accessor.tile_state["tile_visits"]
|
22
22
|
|
23
23
|
def action_planner(
|
24
24
|
self, zoom: int, square_x: int, square_y: int, square_size: int
|
@@ -52,7 +52,7 @@
|
|
52
52
|
<div class="container">
|
53
53
|
<nav class="navbar navbar-expand-lg bg-body-secondary mb-3">
|
54
54
|
<div class="container-fluid">
|
55
|
-
<a class="navbar-brand" href="{{ url_for('index') }}">Geo
|
55
|
+
<a class="navbar-brand" href="{{ url_for('index') }}">Geo Activity Playground</a>
|
56
56
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
|
57
57
|
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
|
58
58
|
aria-expanded="false" aria-label="Toggle navigation">
|
@@ -5,32 +5,39 @@ from ...core.activities import ActivityRepository
|
|
5
5
|
from ...explorer.tile_visits import TileVisitAccessor
|
6
6
|
from .controller import UploadController
|
7
7
|
from geo_activity_playground.core.config import Config
|
8
|
+
from geo_activity_playground.webui.authenticator import Authenticator
|
9
|
+
from geo_activity_playground.webui.authenticator import needs_authentication
|
8
10
|
|
9
11
|
|
10
12
|
def make_upload_blueprint(
|
11
13
|
repository: ActivityRepository,
|
12
14
|
tile_visit_accessor: TileVisitAccessor,
|
13
15
|
config: Config,
|
16
|
+
authenticator: Authenticator,
|
14
17
|
) -> Blueprint:
|
15
18
|
blueprint = Blueprint("upload", __name__, template_folder="templates")
|
16
19
|
|
17
20
|
upload_controller = UploadController(repository, tile_visit_accessor, config)
|
18
21
|
|
19
22
|
@blueprint.route("/")
|
23
|
+
@needs_authentication(authenticator)
|
20
24
|
def index():
|
21
25
|
return render_template(
|
22
26
|
"upload/index.html.j2", **upload_controller.render_form()
|
23
27
|
)
|
24
28
|
|
25
29
|
@blueprint.route("/receive", methods=["POST"])
|
30
|
+
@needs_authentication(authenticator)
|
26
31
|
def receive():
|
27
32
|
return upload_controller.receive()
|
28
33
|
|
29
34
|
@blueprint.route("/refresh")
|
35
|
+
@needs_authentication(authenticator)
|
30
36
|
def reload():
|
31
37
|
return render_template("upload/reload.html.j2")
|
32
38
|
|
33
39
|
@blueprint.route("/execute-reload")
|
40
|
+
@needs_authentication(authenticator)
|
34
41
|
def execute_reload():
|
35
42
|
return upload_controller.execute_reload()
|
36
43
|
|
@@ -14,7 +14,7 @@ from geo_activity_playground.core.activities import build_activity_meta
|
|
14
14
|
from geo_activity_playground.core.config import Config
|
15
15
|
from geo_activity_playground.core.enrichment import enrich_activities
|
16
16
|
from geo_activity_playground.explorer.tile_visits import compute_tile_evolution
|
17
|
-
from geo_activity_playground.explorer.tile_visits import
|
17
|
+
from geo_activity_playground.explorer.tile_visits import compute_tile_visits_new
|
18
18
|
from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
|
19
19
|
from geo_activity_playground.importers.directory import get_file_hash
|
20
20
|
from geo_activity_playground.importers.directory import import_from_directory
|
@@ -45,7 +45,6 @@ class UploadController:
|
|
45
45
|
directories.sort()
|
46
46
|
return {
|
47
47
|
"directories": directories,
|
48
|
-
"has_upload": self._config.upload_password,
|
49
48
|
}
|
50
49
|
|
51
50
|
def receive(self) -> Response:
|
@@ -54,10 +53,6 @@ class UploadController:
|
|
54
53
|
flash("No file could be found. Did you select a file?", "warning")
|
55
54
|
return redirect("/upload")
|
56
55
|
|
57
|
-
if request.form["password"] != self._config.upload_password:
|
58
|
-
flash("Incorrect upload password!", "danger")
|
59
|
-
return redirect("/upload")
|
60
|
-
|
61
56
|
file = request.files["file"]
|
62
57
|
# If the user does not select a file, the browser submits an
|
63
58
|
# empty file without a filename.
|
@@ -120,5 +115,6 @@ def scan_for_activities(
|
|
120
115
|
repository.reload()
|
121
116
|
|
122
117
|
if len(repository) > 0:
|
123
|
-
|
124
|
-
compute_tile_evolution(tile_visit_accessor, config)
|
118
|
+
compute_tile_visits_new(repository, tile_visit_accessor)
|
119
|
+
compute_tile_evolution(tile_visit_accessor.tile_state, config)
|
120
|
+
tile_visit_accessor.save()
|
@@ -1,37 +1,21 @@
|
|
1
1
|
{% extends "page.html.j2" %}
|
2
2
|
|
3
3
|
{% block container %}
|
4
|
-
<
|
5
|
-
<div class="col">
|
6
|
-
<h1>Upload Activity</h1>
|
4
|
+
<h1>Upload Activity</h1>
|
7
5
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
<input type="file" name="file" id="file1" class="form-control">
|
13
|
-
</div>
|
14
|
-
<div class="mb-3">
|
15
|
-
<label for="directory" class="form-label">Target directory</label>
|
16
|
-
<select name="directory" id="directory" class="form-select" aria-label="Default select example">
|
17
|
-
{% for directory in directories %}
|
18
|
-
<option>{{ directory }}</option>
|
19
|
-
{% endfor %}
|
20
|
-
</select>
|
21
|
-
</div>
|
22
|
-
<div class="mb-3">
|
23
|
-
<label for="password" class="form-label">Password</label>
|
24
|
-
<input type="password" name="password" id="password" class="form-control">
|
25
|
-
</div>
|
26
|
-
<button type="submit" class="btn btn-primary">Upload</button>
|
27
|
-
</form>
|
28
|
-
{% else %}
|
29
|
-
<p>You don't have an upload password set. In order to use this feature, add the following to your configuration
|
30
|
-
file:</p>
|
31
|
-
<code><pre>[upload]
|
32
|
-
password = "your unique password here"</pre></code>
|
33
|
-
{% endif %}
|
6
|
+
<form method="post" enctype="multipart/form-data" action="{{ url_for('.receive') }}">
|
7
|
+
<div class="mb-3">
|
8
|
+
<label for="file1" class="form-label">Activity file</label>
|
9
|
+
<input type="file" name="file" id="file1" class="form-control">
|
34
10
|
</div>
|
35
|
-
|
36
|
-
|
11
|
+
<div class="mb-3">
|
12
|
+
<label for="directory" class="form-label">Target directory</label>
|
13
|
+
<select name="directory" id="directory" class="form-select" aria-label="Default select example">
|
14
|
+
{% for directory in directories %}
|
15
|
+
<option>{{ directory }}</option>
|
16
|
+
{% endfor %}
|
17
|
+
</select>
|
18
|
+
</div>
|
19
|
+
<button type="submit" class="btn btn-primary">Upload</button>
|
20
|
+
</form>
|
37
21
|
{% endblock %}
|
{geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.28.0.dist-info}/RECORD
RENAMED
@@ -1,8 +1,8 @@
|
|
1
1
|
geo_activity_playground/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
geo_activity_playground/__main__.py,sha256=
|
2
|
+
geo_activity_playground/__main__.py,sha256=MBZn9K1m3PofiPNTtpsSTVCyB_Gz95TjVP-nb9v_-JE,3989
|
3
3
|
geo_activity_playground/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
-
geo_activity_playground/core/activities.py,sha256=
|
5
|
-
geo_activity_playground/core/config.py,sha256=
|
4
|
+
geo_activity_playground/core/activities.py,sha256=yDjf3aVOC3LvnAwAvQUl_e7S3NXq_vKgccq0hGuFdLI,6609
|
5
|
+
geo_activity_playground/core/config.py,sha256=uys4O2OfXC6yNAsgbgm72WGIhy903zomr_SOwk4GNmA,4498
|
6
6
|
geo_activity_playground/core/coordinates.py,sha256=tDfr9mlXhK6E_MMIJ0vYWVCoH0Lq8uyuaqUgaa8i0jg,966
|
7
7
|
geo_activity_playground/core/enrichment.py,sha256=CwZhW-svgPAYbdx3n9kvKlTgcsiCaeuJfSRCC4JxX6g,7411
|
8
8
|
geo_activity_playground/core/heart_rate.py,sha256=IwMt58TpjOYqpAxtsj07zP2ttpN_J3GZeiv-qGhYyJc,1598
|
@@ -10,14 +10,14 @@ geo_activity_playground/core/heatmap.py,sha256=bRLQHzmTEsQbX8XWeg85x_lRGk272UoYR
|
|
10
10
|
geo_activity_playground/core/paths.py,sha256=AiYUJv46my_FGYbHZmSs5ZrqeE65GNdWEMmXZgunZrk,2150
|
11
11
|
geo_activity_playground/core/privacy_zones.py,sha256=4TumHsVUN1uW6RG3ArqTXDykPVipF98DCxVBe7YNdO8,512
|
12
12
|
geo_activity_playground/core/similarity.py,sha256=Jo8jRViuORCxdIGvyaflgsQhwu9S_jn10a450FRL18A,3159
|
13
|
-
geo_activity_playground/core/tasks.py,sha256=
|
13
|
+
geo_activity_playground/core/tasks.py,sha256=f3C2gCJiiqMS1eRQ-Hp5hxRbkwl60rotrYfzKsWdpSU,2937
|
14
14
|
geo_activity_playground/core/test_tiles.py,sha256=zce1FxNfsSpOQt66jMehdQRVoNdl-oiFydx6iVBHZXM,764
|
15
15
|
geo_activity_playground/core/test_time_conversion.py,sha256=Sh6nZA3uCTOdZTZa3yOijtR0m74QtZu2mcWXsDNnyQI,984
|
16
16
|
geo_activity_playground/core/tiles.py,sha256=KpzD-h3kNzZ2ieLt6f2xHilSF3lHyfaEXPnrGvlIAz0,3379
|
17
17
|
geo_activity_playground/core/time_conversion.py,sha256=9J6aTlqJhWvsknQkoECNL-CIG-8BKs6ZatJJ9XJnTsg,367
|
18
18
|
geo_activity_playground/explorer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
19
|
geo_activity_playground/explorer/grid_file.py,sha256=k6j6KBEk2a2BY-onE8SV5TJsERGGyOrlY4as__meWpA,3304
|
20
|
-
geo_activity_playground/explorer/tile_visits.py,sha256=
|
20
|
+
geo_activity_playground/explorer/tile_visits.py,sha256=IqkaGPq-xrmR81Ze31VL1v4NUxQ2YGQeQawvsjYe25s,12776
|
21
21
|
geo_activity_playground/explorer/video.py,sha256=ROAmV9shfJyqTgnXVD41KFORiwnRgVpEWenIq4hMCRM,4389
|
22
22
|
geo_activity_playground/importers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
23
23
|
geo_activity_playground/importers/activity_parsers.py,sha256=m2SpvGlTZ8F3gG6YB24_ZFrlOAbtqbfWi-GIYspeUco,10593
|
@@ -31,12 +31,15 @@ geo_activity_playground/importers/test_strava_api.py,sha256=4vX7wDr1a9aRh8myxNrI
|
|
31
31
|
geo_activity_playground/webui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
32
32
|
geo_activity_playground/webui/activity/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
33
33
|
geo_activity_playground/webui/activity/blueprint.py,sha256=upQzZa5sKApj_Fmu6PziFDboi7SBL5Zsi-tNSSNPlEE,1759
|
34
|
-
geo_activity_playground/webui/activity/controller.py,sha256=
|
34
|
+
geo_activity_playground/webui/activity/controller.py,sha256=YaWHXIfSYSeybEFY706s_m4uHbfJ9FHxqIYtqwR5sGQ,17955
|
35
35
|
geo_activity_playground/webui/activity/templates/activity/day.html.j2,sha256=r3qKl9uTzOko4R-ZzyYAZt1j61JSevYP4g0Yi06HHPg,2702
|
36
36
|
geo_activity_playground/webui/activity/templates/activity/lines.html.j2,sha256=5gB1aDjRgi_RventenRfC10_FtMT4ch_VuWvA9AMlBY,1121
|
37
37
|
geo_activity_playground/webui/activity/templates/activity/name.html.j2,sha256=RDLEt6ip8_ngmdLgaC5jg92Dk-F2umGwKkd8cWmvVko,2400
|
38
|
-
geo_activity_playground/webui/activity/templates/activity/show.html.j2,sha256=
|
39
|
-
geo_activity_playground/webui/app.py,sha256=
|
38
|
+
geo_activity_playground/webui/activity/templates/activity/show.html.j2,sha256=6k3_MC_AR_sSWVYvH53E5o6YFtE9HZD62NlPf90Rj8Q,5509
|
39
|
+
geo_activity_playground/webui/app.py,sha256=Vn8G6wf2YSKc_n7JBkulry51Gl57_UjrAW0TftXLQgM,4456
|
40
|
+
geo_activity_playground/webui/auth/blueprint.py,sha256=Lx-ZvMnfHLC1CMre1xPQI3k_pCtQoZvgRhtmafULzoE,812
|
41
|
+
geo_activity_playground/webui/auth/templates/auth/index.html.j2,sha256=ILQ5HvTEYc3OrtOAIFt1VrqWorVD70V9DC342znmP70,579
|
42
|
+
geo_activity_playground/webui/authenticator.py,sha256=jXwWJFuinSE-7sQxdi7s7WO9ifvERmeXAM5-hsAPmwQ,1546
|
40
43
|
geo_activity_playground/webui/calendar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
41
44
|
geo_activity_playground/webui/calendar/blueprint.py,sha256=rlnhgU2DWAcdLMRq7m77NzrM_aDyp4s3kuuQHuzjHhg,782
|
42
45
|
geo_activity_playground/webui/calendar/controller.py,sha256=QpSAkR2s1sbLSu6P_fNNTccgGglOzEH2PIv1XwKxeVY,2778
|
@@ -49,29 +52,31 @@ geo_activity_playground/webui/eddington/templates/eddington/index.html.j2,sha256
|
|
49
52
|
geo_activity_playground/webui/entry_controller.py,sha256=n9v4MriyL8kDR91LE9eeqc2tAvxyzFgoNMMXpr0qh4g,1906
|
50
53
|
geo_activity_playground/webui/equipment/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
51
54
|
geo_activity_playground/webui/equipment/blueprint.py,sha256=_NIhRJuJNbXpEd_nEPo01AqnUqPgo1vawFn7E3yoeng,636
|
52
|
-
geo_activity_playground/webui/equipment/controller.py,sha256=
|
55
|
+
geo_activity_playground/webui/equipment/controller.py,sha256=Sx9i2RCK7m4W6FgpDfRMewcH64VBQfUhHJdTSCwMqOU,4079
|
53
56
|
geo_activity_playground/webui/equipment/templates/equipment/index.html.j2,sha256=FEfxB4XwVYELAOdjVlSlprjJH_kLmE-pNWEEXdPqc6I,1778
|
54
57
|
geo_activity_playground/webui/explorer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
55
58
|
geo_activity_playground/webui/explorer/blueprint.py,sha256=EKnBs8llqT6Wy1uac18dF2epp3TebF9p3iGlSbj6Vl0,2337
|
56
|
-
geo_activity_playground/webui/explorer/controller.py,sha256
|
59
|
+
geo_activity_playground/webui/explorer/controller.py,sha256=-iMsuB05JgUYKahJxiOPZeNVmzr_Uw2psHwmaFiptkI,11614
|
57
60
|
geo_activity_playground/webui/explorer/templates/explorer/index.html.j2,sha256=cm9pWY0vB84DtkTH-LBvSzfLU1FnmxQ2ECyw3Bl7dTo,6945
|
58
61
|
geo_activity_playground/webui/heatmap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
59
62
|
geo_activity_playground/webui/heatmap/blueprint.py,sha256=bjQu-HL3QN5UpJ6tHOifhcLGlPr_hIKvaRu78md4JqM,1470
|
60
|
-
geo_activity_playground/webui/heatmap/heatmap_controller.py,sha256=
|
63
|
+
geo_activity_playground/webui/heatmap/heatmap_controller.py,sha256=Bn7FO_ciMEXghSwz2tEQ8fJsbK8AXaHTPNgnGuQ7an8,7353
|
61
64
|
geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2,sha256=YLeu6P4djl8G4qAXR6DhetseqrbOodN7aN4coocknc4,1875
|
62
65
|
geo_activity_playground/webui/search_controller.py,sha256=PzMf7b8tiJKZIZoPvQ9A2hOrzoKV9cS3jq05w2fK94c,532
|
63
|
-
geo_activity_playground/webui/settings/blueprint.py,sha256=
|
64
|
-
geo_activity_playground/webui/settings/controller.py,sha256=
|
66
|
+
geo_activity_playground/webui/settings/blueprint.py,sha256=boKeKkG4bbSA7HvgpaqlUXzrcCnOPwys6KWw-Dp5sAE,5416
|
67
|
+
geo_activity_playground/webui/settings/controller.py,sha256=v14oKvI1QzWn0g41Sm4NA_g9q4SUVZ_bO9SUOZuPAaY,9121
|
68
|
+
geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2,sha256=VYwddpObD1RpeTH5Dm4y7VtmT7kwURDCIjxyzJeq08c,495
|
65
69
|
geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2,sha256=ltaYwFe8S8Mi72ddmIp1vwqlu8MEXXjBGfbpN2WBTC4,1728
|
66
70
|
geo_activity_playground/webui/settings/templates/settings/heart-rate.html.j2,sha256=UPT3MegRgSeff36lhCo0l3ZwhqNSIg5gM6h2s32GkCY,4255
|
67
|
-
geo_activity_playground/webui/settings/templates/settings/index.html.j2,sha256=
|
71
|
+
geo_activity_playground/webui/settings/templates/settings/index.html.j2,sha256=rXKq2v42J0eW5OSWx7VU6EFT9jZkbzKTW600Y0OqBtY,4035
|
68
72
|
geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2,sha256=IdUfXon1Pu8zX3NirKb28ypshLHOvZRpz2T4bJrzrak,1067
|
69
73
|
geo_activity_playground/webui/settings/templates/settings/metadata-extraction.html.j2,sha256=Ppa8O-zRJznbeCsF4YQj37_HM9nOW8fyTi66jvWvHmA,2285
|
70
74
|
geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2,sha256=7BxFvCaVJOEqbImyK5vxCmhh-NGSFaRa9ARhqjZeYJ0,3093
|
75
|
+
geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2,sha256=qZkfEpd4CtKKMaSSVadqvNEgMRYLV-0X-pw5-nJvukk,678
|
71
76
|
geo_activity_playground/webui/settings/templates/settings/strava.html.j2,sha256=FrXgT-m1PgvsQWo9kMKpk8QenKeifSDBCZFqKgsHRxQ,1827
|
72
77
|
geo_activity_playground/webui/square_planner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
73
78
|
geo_activity_playground/webui/square_planner/blueprint.py,sha256=r2VkSM547chX85g6c1BQ8NC-tkdqGdYp-2ZALBiiDTc,1320
|
74
|
-
geo_activity_playground/webui/square_planner/controller.py,sha256=
|
79
|
+
geo_activity_playground/webui/square_planner/controller.py,sha256=vV7Wd4Kt1YPOI-UVoqLtIqUb3nke9PJQwWynlA31Lto,3483
|
75
80
|
geo_activity_playground/webui/square_planner/templates/square_planner/index.html.j2,sha256=aIB0ql5qW4HXfp0ENksYYOk9vTgBitwyHJX5W7bqkeY,6512
|
76
81
|
geo_activity_playground/webui/static/android-chrome-192x192.png,sha256=yxZgo8Jw4hrgOgrn3tvi9G0AXWGFD29kjCuxC07WoT4,17610
|
77
82
|
geo_activity_playground/webui/static/android-chrome-384x384.png,sha256=bgeqAdyvDZBMch7rVi3qSawf0Zr4Go0EG8Ws_B8NApY,49297
|
@@ -90,18 +95,18 @@ geo_activity_playground/webui/summary/blueprint.py,sha256=kzQ6MDOycQKfDcVoEUmL7H
|
|
90
95
|
geo_activity_playground/webui/summary/controller.py,sha256=ZOrwfrKjpc8hecUYImBvesKXZi06obfR1yhQkVTeWzw,8981
|
91
96
|
geo_activity_playground/webui/summary/templates/summary/index.html.j2,sha256=rsII1eMY-xNugh8A9SecnEcDZqkEOWYIfiHAGroQYuM,4442
|
92
97
|
geo_activity_playground/webui/templates/home.html.j2,sha256=FjEwr9kt_3qu_evIHpa7F_oGAINN8W2Z1T_j56ugJ5c,2406
|
93
|
-
geo_activity_playground/webui/templates/page.html.j2,sha256=
|
98
|
+
geo_activity_playground/webui/templates/page.html.j2,sha256=8DkXaXx_cc9FvfI6eo9z3fbeNDRcvJC2NXcHXs8ytSU,9588
|
94
99
|
geo_activity_playground/webui/templates/search.html.j2,sha256=FvNRoDfUlSzXjM_tqZY_fDhuhUDgbPaY73q56gdvF1A,1130
|
95
100
|
geo_activity_playground/webui/tile/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
96
101
|
geo_activity_playground/webui/tile/blueprint.py,sha256=cK0o2Z3BrLycgF9zw0F8s9qF-JaYDbF5Gog-GXDtUZ8,943
|
97
102
|
geo_activity_playground/webui/tile/controller.py,sha256=PISh4vKs27b-LxFfTARtr5RAwHFresA1Kw1MDcERSRU,1221
|
98
103
|
geo_activity_playground/webui/upload/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
99
|
-
geo_activity_playground/webui/upload/blueprint.py,sha256=
|
100
|
-
geo_activity_playground/webui/upload/controller.py,sha256=
|
101
|
-
geo_activity_playground/webui/upload/templates/upload/index.html.j2,sha256=
|
104
|
+
geo_activity_playground/webui/upload/blueprint.py,sha256=xX9scEmleN_dL03jfhWRh5yI1WsFyhxUFiS_Ul2HWy4,1428
|
105
|
+
geo_activity_playground/webui/upload/controller.py,sha256=disRtrlvPiqsIFt9UaAokgtRtXCvosg7bXkAnN_qaxk,4102
|
106
|
+
geo_activity_playground/webui/upload/templates/upload/index.html.j2,sha256=I1Ix8tDS3YBdi-HdaNfjkzYXVVCjfUTe5PFTnap1ydc,775
|
102
107
|
geo_activity_playground/webui/upload/templates/upload/reload.html.j2,sha256=YZWX5eDeNyqKJdQAywDBcU8DZBm22rRBbZqFjrFrCvQ,556
|
103
|
-
geo_activity_playground-0.
|
104
|
-
geo_activity_playground-0.
|
105
|
-
geo_activity_playground-0.
|
106
|
-
geo_activity_playground-0.
|
107
|
-
geo_activity_playground-0.
|
108
|
+
geo_activity_playground-0.28.0.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
|
109
|
+
geo_activity_playground-0.28.0.dist-info/METADATA,sha256=0ZheRNfyebSkkcFUph4A3Wo3EheMmw0c0U2fi9C-MWc,1665
|
110
|
+
geo_activity_playground-0.28.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
111
|
+
geo_activity_playground-0.28.0.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
|
112
|
+
geo_activity_playground-0.28.0.dist-info/RECORD,,
|
{geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.28.0.dist-info}/LICENSE
RENAMED
File without changes
|
File without changes
|
File without changes
|