geo-activity-playground 0.42.0__py3-none-any.whl → 0.43.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/alembic/script.py.mako +0 -6
- geo_activity_playground/alembic/versions/da2cba03b71d_add_photos.py +40 -0
- geo_activity_playground/alembic/versions/script.py.mako +6 -0
- geo_activity_playground/core/activities.py +3 -1
- geo_activity_playground/core/datamodel.py +48 -22
- geo_activity_playground/core/enrichment.py +4 -2
- geo_activity_playground/core/meta_search.py +78 -34
- geo_activity_playground/core/missing_values.py +5 -2
- geo_activity_playground/core/paths.py +2 -0
- geo_activity_playground/webui/app.py +21 -7
- geo_activity_playground/webui/blueprints/photo_blueprint.py +198 -0
- geo_activity_playground/webui/search_util.py +23 -7
- geo_activity_playground/webui/templates/activity/show.html.j2 +43 -10
- geo_activity_playground/webui/templates/eddington/distance.html.j2 +1 -2
- geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +1 -2
- geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +18 -15
- geo_activity_playground/webui/templates/heatmap/index.html.j2 +1 -2
- geo_activity_playground/webui/templates/page.html.j2 +8 -0
- geo_activity_playground/webui/templates/photo/map.html.j2 +45 -0
- geo_activity_playground/webui/templates/photo/new.html.j2 +13 -0
- geo_activity_playground/webui/templates/search/index.html.j2 +1 -2
- geo_activity_playground/webui/templates/search_form.html.j2 +47 -22
- geo_activity_playground/webui/templates/summary/index.html.j2 +12 -10
- {geo_activity_playground-0.42.0.dist-info → geo_activity_playground-0.43.0.dist-info}/METADATA +2 -1
- {geo_activity_playground-0.42.0.dist-info → geo_activity_playground-0.43.0.dist-info}/RECORD +28 -24
- {geo_activity_playground-0.42.0.dist-info → geo_activity_playground-0.43.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.42.0.dist-info → geo_activity_playground-0.43.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.42.0.dist-info → geo_activity_playground-0.43.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
from typing import Sequence
|
2
|
+
from typing import Union
|
3
|
+
|
4
|
+
import sqlalchemy as sa
|
5
|
+
from alembic import op
|
6
|
+
|
7
|
+
|
8
|
+
# revision identifiers, used by Alembic.
|
9
|
+
revision: str = "da2cba03b71d"
|
10
|
+
down_revision: Union[str, None] = "38882503dc7c"
|
11
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
12
|
+
depends_on: Union[str, Sequence[str], None] = None
|
13
|
+
|
14
|
+
|
15
|
+
def upgrade() -> None:
|
16
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
17
|
+
op.create_table(
|
18
|
+
"photos",
|
19
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
20
|
+
sa.Column("filename", sa.String(), nullable=False),
|
21
|
+
sa.Column("time", sa.DateTime(), nullable=False),
|
22
|
+
sa.Column("latitude", sa.Float(), nullable=False),
|
23
|
+
sa.Column("longitude", sa.Float(), nullable=False),
|
24
|
+
sa.Column("activity_id", sa.Integer(), nullable=False),
|
25
|
+
sa.ForeignKeyConstraint(["activity_id"], ["activities.id"], name="activity_id"),
|
26
|
+
sa.PrimaryKeyConstraint("id"),
|
27
|
+
)
|
28
|
+
with op.batch_alter_table("tags", schema=None) as batch_op:
|
29
|
+
batch_op.create_unique_constraint("tags_tag", ["tag"])
|
30
|
+
|
31
|
+
# ### end Alembic commands ###
|
32
|
+
|
33
|
+
|
34
|
+
def downgrade() -> None:
|
35
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
36
|
+
with op.batch_alter_table("tags", schema=None) as batch_op:
|
37
|
+
batch_op.drop_constraint("tags_tag", type_="unique")
|
38
|
+
|
39
|
+
op.drop_table("photos")
|
40
|
+
# ### end Alembic commands ###
|
@@ -24,7 +24,9 @@ logger = logging.getLogger(__name__)
|
|
24
24
|
|
25
25
|
class ActivityRepository:
|
26
26
|
def __len__(self) -> int:
|
27
|
-
return
|
27
|
+
return DB.session.scalars(
|
28
|
+
sqlalchemy.select(sqlalchemy.func.count()).select_from(Activity)
|
29
|
+
).one()
|
28
30
|
|
29
31
|
def has_activity(self, activity_id: int) -> bool:
|
30
32
|
return bool(
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import datetime
|
2
2
|
import json
|
3
3
|
import logging
|
4
|
+
import pathlib
|
4
5
|
from typing import Any
|
5
6
|
from typing import Optional
|
6
7
|
from typing import TypedDict
|
@@ -117,6 +118,10 @@ class Activity(DB.Model):
|
|
117
118
|
secondary=activity_tag_association_table, back_populates="activities"
|
118
119
|
)
|
119
120
|
|
121
|
+
photos: Mapped[list["Photo"]] = relationship(
|
122
|
+
back_populates="activity", cascade="all, delete-orphan"
|
123
|
+
)
|
124
|
+
|
120
125
|
def __str__(self) -> str:
|
121
126
|
return f"{self.start} {self.name}"
|
122
127
|
|
@@ -179,7 +184,7 @@ def get_or_make_tag(tag: str) -> Tag:
|
|
179
184
|
return tag
|
180
185
|
|
181
186
|
|
182
|
-
def query_activity_meta() -> pd.DataFrame:
|
187
|
+
def query_activity_meta(clauses: list = []) -> pd.DataFrame:
|
183
188
|
rows = DB.session.execute(
|
184
189
|
sqlalchemy.select(
|
185
190
|
Activity.id,
|
@@ -206,31 +211,33 @@ def query_activity_meta() -> pd.DataFrame:
|
|
206
211
|
)
|
207
212
|
.join(Activity.equipment)
|
208
213
|
.join(Activity.kind)
|
214
|
+
.where(*clauses)
|
209
215
|
.order_by(Activity.start)
|
210
216
|
).all()
|
211
217
|
df = pd.DataFrame(rows)
|
212
218
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
df.loc[mask,
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
219
|
+
if len(df):
|
220
|
+
for old, new in [
|
221
|
+
("elapsed_time", "average_speed_elapsed_kmh"),
|
222
|
+
("moving_time", "average_speed_moving_kmh"),
|
223
|
+
]:
|
224
|
+
df[new] = pd.NA
|
225
|
+
mask = df[old].dt.total_seconds() > 0
|
226
|
+
df.loc[mask, new] = df.loc[mask, "distance_km"] / (
|
227
|
+
df.loc[mask, old].dt.total_seconds() / 3_600
|
228
|
+
)
|
229
|
+
|
230
|
+
df["date"] = df["start"].dt.date
|
231
|
+
df["year"] = df["start"].dt.year
|
232
|
+
df["month"] = df["start"].dt.month
|
233
|
+
df["day"] = df["start"].dt.day
|
234
|
+
df["week"] = df["start"].dt.isocalendar().week
|
235
|
+
df["day_of_week"] = df["start"].dt.day_of_week
|
236
|
+
df["iso_year"] = df["start"].dt.isocalendar().year
|
237
|
+
df["hours"] = df["elapsed_time"].dt.total_seconds() / 3_600
|
238
|
+
df["hours_moving"] = df["moving_time"].dt.total_seconds() / 3_600
|
239
|
+
|
240
|
+
df.index = df["id"]
|
234
241
|
|
235
242
|
return df
|
236
243
|
|
@@ -363,3 +370,22 @@ class PlotSpec(DB.Model):
|
|
363
370
|
return json.dumps(
|
364
371
|
{key: getattr(self, key) for key in self.FIELDS if getattr(self, key)}
|
365
372
|
)
|
373
|
+
|
374
|
+
|
375
|
+
class Photo(DB.Model):
|
376
|
+
__tablename__ = "photos"
|
377
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
378
|
+
|
379
|
+
filename: Mapped[str] = mapped_column(sa.String, nullable=False)
|
380
|
+
time: Mapped[datetime.datetime] = mapped_column(sa.DateTime, nullable=False)
|
381
|
+
latitude: Mapped[float] = mapped_column(sa.Float, nullable=False)
|
382
|
+
longitude: Mapped[float] = mapped_column(sa.Float, nullable=False)
|
383
|
+
|
384
|
+
activity_id: Mapped[int] = mapped_column(
|
385
|
+
ForeignKey("activities.id", name="activity_id"), nullable=False
|
386
|
+
)
|
387
|
+
activity: Mapped["Activity"] = relationship(back_populates="photos")
|
388
|
+
|
389
|
+
@property
|
390
|
+
def path(self) -> pathlib.Path:
|
391
|
+
return pathlib.Path(self.filename)
|
@@ -108,8 +108,10 @@ def populate_database_from_extracted(config: Config) -> None:
|
|
108
108
|
def update_via_time_series(
|
109
109
|
activity: Activity, time_series: pd.DataFrame
|
110
110
|
) -> ActivityMeta:
|
111
|
-
activity.start = time_series["time"].iloc[0]
|
112
|
-
activity.elapsed_time =
|
111
|
+
activity.start = some(time_series["time"].iloc[0])
|
112
|
+
activity.elapsed_time = some(
|
113
|
+
time_series["time"].iloc[-1] - time_series["time"].iloc[0]
|
114
|
+
)
|
113
115
|
activity.distance_km = (
|
114
116
|
time_series["distance_km"].iloc[-1] - time_series["distance_km"].iloc[0]
|
115
117
|
)
|
@@ -2,21 +2,33 @@ import dataclasses
|
|
2
2
|
import datetime
|
3
3
|
import re
|
4
4
|
import urllib.parse
|
5
|
+
from collections.abc import Sequence
|
5
6
|
from typing import Optional
|
6
7
|
|
7
8
|
import dateutil.parser
|
8
9
|
import numpy as np
|
9
10
|
import pandas as pd
|
11
|
+
import sqlalchemy
|
12
|
+
|
13
|
+
from .datamodel import Activity
|
14
|
+
from .datamodel import DB
|
15
|
+
from .datamodel import Equipment
|
16
|
+
from .datamodel import Kind
|
17
|
+
from .datamodel import query_activity_meta
|
18
|
+
from .datamodel import Tag
|
10
19
|
|
11
20
|
|
12
21
|
@dataclasses.dataclass
|
13
22
|
class SearchQuery:
|
14
|
-
equipment: list[
|
15
|
-
kind: list[
|
23
|
+
equipment: list[Equipment] = dataclasses.field(default_factory=list)
|
24
|
+
kind: list[Kind] = dataclasses.field(default_factory=list)
|
25
|
+
tag: list[Tag] = dataclasses.field(default_factory=list)
|
16
26
|
name: Optional[str] = None
|
17
27
|
name_case_sensitive: bool = False
|
18
28
|
start_begin: Optional[datetime.date] = None
|
19
29
|
start_end: Optional[datetime.date] = None
|
30
|
+
distance_km_min: Optional[float] = None
|
31
|
+
distance_km_max: Optional[float] = None
|
20
32
|
|
21
33
|
def __str__(self) -> str:
|
22
34
|
bits = []
|
@@ -25,10 +37,14 @@ class SearchQuery:
|
|
25
37
|
if self.equipment:
|
26
38
|
bits.append(
|
27
39
|
"equipment is "
|
28
|
-
+ (" or ".join(f"“{equipment}”" for equipment in self.equipment))
|
40
|
+
+ (" or ".join(f"“{equipment.name}”" for equipment in self.equipment))
|
29
41
|
)
|
30
42
|
if self.kind:
|
31
|
-
bits.append(
|
43
|
+
bits.append(
|
44
|
+
"kind is " + (" or ".join(f"“{kind.name}”" for kind in self.kind))
|
45
|
+
)
|
46
|
+
if self.tag:
|
47
|
+
bits.append("tag is " + (" or ".join(f"“{tag.tag}”" for tag in self.tag)))
|
32
48
|
if self.start_begin:
|
33
49
|
bits.append(f"after “{self.start_begin.isoformat()}”")
|
34
50
|
if self.start_end:
|
@@ -43,27 +59,38 @@ class SearchQuery:
|
|
43
59
|
or self.name
|
44
60
|
or self.start_begin
|
45
61
|
or self.start_end
|
62
|
+
or self.tag
|
63
|
+
or self.distance_km_min
|
64
|
+
or self.distance_km_max
|
46
65
|
)
|
47
66
|
|
48
67
|
def to_primitives(self) -> dict:
|
49
68
|
return {
|
50
|
-
"equipment": self.equipment,
|
51
|
-
"kind": self.kind,
|
69
|
+
"equipment": [equipment.id for equipment in self.equipment],
|
70
|
+
"kind": [kind.id for kind in self.kind],
|
71
|
+
"tag": [tag.id for tag in self.tag],
|
52
72
|
"name": self.name or "",
|
53
73
|
"name_case_sensitive": self.name_case_sensitive,
|
54
74
|
"start_begin": _format_optional_date(self.start_begin),
|
55
75
|
"start_end": _format_optional_date(self.start_end),
|
76
|
+
"distance_km_min": self.distance_km_min,
|
77
|
+
"distance_km_max": self.distance_km_max,
|
56
78
|
}
|
57
79
|
|
58
80
|
@classmethod
|
59
81
|
def from_primitives(cls, d: dict) -> "SearchQuery":
|
60
82
|
return cls(
|
61
|
-
equipment=
|
62
|
-
|
83
|
+
equipment=[
|
84
|
+
DB.session.get_one(Equipment, id) for id in d.get("equipment", [])
|
85
|
+
],
|
86
|
+
kind=[DB.session.get_one(Kind, id) for id in d.get("kind", [])],
|
87
|
+
tag=[DB.session.get_one(Tag, id) for id in d.get("tag", [])],
|
63
88
|
name=d.get("name", None),
|
64
89
|
name_case_sensitive=d.get("name_case_sensitive", False),
|
65
90
|
start_begin=_parse_date_or_none(d.get("start_begin", None)),
|
66
91
|
start_end=_parse_date_or_none(d.get("start_end", None)),
|
92
|
+
distance_km_min=d.get("distance_km_min", None),
|
93
|
+
distance_km_max=d.get("distance_km_max", None),
|
67
94
|
)
|
68
95
|
|
69
96
|
def to_jinja(self) -> dict:
|
@@ -74,9 +101,11 @@ class SearchQuery:
|
|
74
101
|
def to_url_str(self) -> str:
|
75
102
|
variables = []
|
76
103
|
for equipment in self.equipment:
|
77
|
-
variables.append(("equipment", equipment))
|
104
|
+
variables.append(("equipment", equipment.id))
|
78
105
|
for kind in self.kind:
|
79
|
-
variables.append(("kind", kind))
|
106
|
+
variables.append(("kind", kind.id))
|
107
|
+
for tag in self.tag:
|
108
|
+
variables.append(("tag", tag.id))
|
80
109
|
if self.name:
|
81
110
|
variables.append(("name", self.name))
|
82
111
|
if self.name_case_sensitive:
|
@@ -85,6 +114,10 @@ class SearchQuery:
|
|
85
114
|
variables.append(("start_begin", self.start_begin.isoformat()))
|
86
115
|
if self.start_end:
|
87
116
|
variables.append(("start_end", self.start_end.isoformat()))
|
117
|
+
if self.distance_km_min:
|
118
|
+
variables.append(("distance_km_min", self.distance_km_min))
|
119
|
+
if self.distance_km_max:
|
120
|
+
variables.append(("distance_km_max", self.distance_km_max))
|
88
121
|
|
89
122
|
return "&".join(
|
90
123
|
f"{key}={urllib.parse.quote_plus(value)}" for key, value in variables
|
@@ -94,36 +127,47 @@ class SearchQuery:
|
|
94
127
|
def apply_search_query(
|
95
128
|
activity_meta: pd.DataFrame, search_query: SearchQuery
|
96
129
|
) -> pd.DataFrame:
|
97
|
-
|
130
|
+
|
131
|
+
filter_clauses = []
|
98
132
|
|
99
133
|
if search_query.equipment:
|
100
|
-
|
134
|
+
filter_clauses.append(
|
135
|
+
sqlalchemy.or_(
|
136
|
+
*[
|
137
|
+
Activity.equipment == equipment
|
138
|
+
for equipment in search_query.equipment
|
139
|
+
]
|
140
|
+
)
|
141
|
+
)
|
142
|
+
|
101
143
|
if search_query.kind:
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
activity_name,
|
110
|
-
0 if search_query.name_case_sensitive else re.IGNORECASE,
|
111
|
-
)
|
112
|
-
)
|
113
|
-
for activity_name in activity_meta["name"]
|
114
|
-
],
|
115
|
-
index=activity_meta.index,
|
144
|
+
filter_clauses.append(
|
145
|
+
sqlalchemy.or_(*[Activity.kind == kind for kind in search_query.kind])
|
146
|
+
)
|
147
|
+
|
148
|
+
if search_query.tag:
|
149
|
+
filter_clauses.append(
|
150
|
+
sqlalchemy.or_(*[Activity.tags.contains(tag) for tag in search_query.tag])
|
116
151
|
)
|
117
|
-
|
118
|
-
|
119
|
-
|
152
|
+
|
153
|
+
if search_query.name:
|
154
|
+
filter_clauses.append(
|
155
|
+
Activity.name.contains(search_query.name)
|
156
|
+
if search_query.name_case_sensitive
|
157
|
+
else Activity.name.icontains(search_query.name)
|
120
158
|
)
|
121
|
-
mask &= start_begin <= activity_meta["start"]
|
122
|
-
if search_query.start_end is not None:
|
123
|
-
start_end = datetime.datetime.combine(search_query.start_end, datetime.time.max)
|
124
|
-
mask &= activity_meta["start"] <= start_end
|
125
159
|
|
126
|
-
|
160
|
+
if search_query.start_begin:
|
161
|
+
filter_clauses.append(Activity.start <= search_query.start_begin)
|
162
|
+
if search_query.start_end:
|
163
|
+
filter_clauses.append(Activity.start < search_query.start_end)
|
164
|
+
|
165
|
+
if search_query.distance_km_min:
|
166
|
+
filter_clauses.append(Activity.distance_km >= search_query.distance_km_min)
|
167
|
+
if search_query.distance_km_max:
|
168
|
+
filter_clauses.append(Activity.distance_km <= search_query.distance_km_max)
|
169
|
+
|
170
|
+
return query_activity_meta(filter_clauses)
|
127
171
|
|
128
172
|
|
129
173
|
def _format_optional_date(date: Optional[datetime.date]) -> str:
|
@@ -1,13 +1,16 @@
|
|
1
|
+
from typing import Any
|
1
2
|
from typing import Optional
|
2
|
-
from typing import Union
|
3
3
|
|
4
4
|
import numpy as np
|
5
|
+
import pandas as pd
|
5
6
|
|
6
7
|
|
7
|
-
def some(value) -> Optional[
|
8
|
+
def some(value: Any) -> Optional[Any]:
|
8
9
|
if value is None:
|
9
10
|
return None
|
10
11
|
elif np.isnan(value):
|
11
12
|
return None
|
13
|
+
elif pd.isna(value):
|
14
|
+
return None
|
12
15
|
else:
|
13
16
|
return value
|
@@ -53,6 +53,7 @@ _strava_last_activity_date_path = _cache_dir / "strava-last-activity-date.json"
|
|
53
53
|
_new_config_file = pathlib.Path("config.json")
|
54
54
|
_activity_meta_override_dir = pathlib.Path("Metadata Override")
|
55
55
|
_time_series_dir = pathlib.Path("Time Series")
|
56
|
+
_photos_dir = pathlib.Path("Photos")
|
56
57
|
|
57
58
|
|
58
59
|
cache_dir = dir_wrapper(_cache_dir)
|
@@ -65,6 +66,7 @@ tiles_per_time_series = dir_wrapper(_tiles_per_time_series)
|
|
65
66
|
strava_api_dir = dir_wrapper(_strava_api_dir)
|
66
67
|
activity_meta_override_dir = dir_wrapper(_activity_meta_override_dir)
|
67
68
|
time_series_dir = dir_wrapper(_time_series_dir)
|
69
|
+
PHOTOS_DIR = dir_wrapper(_photos_dir)
|
68
70
|
|
69
71
|
activities_file = file_wrapper(_activities_file)
|
70
72
|
strava_dynamic_config_path = file_wrapper(_strava_dynamic_config_path)
|
@@ -8,6 +8,7 @@ import secrets
|
|
8
8
|
import shutil
|
9
9
|
import urllib.parse
|
10
10
|
|
11
|
+
import sqlalchemy
|
11
12
|
from flask import Flask
|
12
13
|
from flask import request
|
13
14
|
from flask_alembic import Alembic
|
@@ -17,6 +18,10 @@ from ..core.config import ConfigAccessor
|
|
17
18
|
from ..core.config import import_old_config
|
18
19
|
from ..core.config import import_old_strava_config
|
19
20
|
from ..core.datamodel import DB
|
21
|
+
from ..core.datamodel import Equipment
|
22
|
+
from ..core.datamodel import Kind
|
23
|
+
from ..core.datamodel import Photo
|
24
|
+
from ..core.datamodel import Tag
|
20
25
|
from ..core.heart_rate import HeartRateZoneComputer
|
21
26
|
from ..core.raster_map import GrayscaleImageTransform
|
22
27
|
from ..core.raster_map import IdentityImageTransform
|
@@ -33,6 +38,7 @@ from .blueprints.entry_views import register_entry_views
|
|
33
38
|
from .blueprints.equipment_blueprint import make_equipment_blueprint
|
34
39
|
from .blueprints.explorer_blueprint import make_explorer_blueprint
|
35
40
|
from .blueprints.heatmap_blueprint import make_heatmap_blueprint
|
41
|
+
from .blueprints.photo_blueprint import make_photo_blueprint
|
36
42
|
from .blueprints.plot_builder_blueprint import make_plot_builder_blueprint
|
37
43
|
from .blueprints.search_blueprint import make_search_blueprint
|
38
44
|
from .blueprints.settings_blueprint import make_settings_blueprint
|
@@ -146,6 +152,7 @@ def web_ui_main(
|
|
146
152
|
"/heatmap": make_heatmap_blueprint(
|
147
153
|
repository, tile_visit_accessor, config_accessor(), search_query_history
|
148
154
|
),
|
155
|
+
"/photo": make_photo_blueprint(config_accessor, authenticator, flasher),
|
149
156
|
"/plot-builder": make_plot_builder_blueprint(
|
150
157
|
repository, flasher, authenticator
|
151
158
|
),
|
@@ -178,15 +185,22 @@ def web_ui_main(
|
|
178
185
|
"version": _try_get_version(),
|
179
186
|
"num_activities": len(repository),
|
180
187
|
"map_tile_attribution": config_accessor().map_tile_attribution,
|
181
|
-
"search_query_favorites": search_query_history.prepare_favorites(),
|
182
|
-
"search_query_last": search_query_history.prepare_last(),
|
188
|
+
# "search_query_favorites": search_query_history.prepare_favorites(),
|
189
|
+
# "search_query_last": search_query_history.prepare_last(),
|
183
190
|
"request_url": urllib.parse.quote_plus(request.url),
|
184
191
|
}
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
192
|
+
variables["equipments_avail"] = DB.session.scalars(
|
193
|
+
sqlalchemy.select(Equipment).order_by(Equipment.name)
|
194
|
+
).all()
|
195
|
+
variables["kinds_avail"] = DB.session.scalars(
|
196
|
+
sqlalchemy.select(Kind).order_by(Kind.name)
|
197
|
+
).all()
|
198
|
+
variables["tags_avail"] = DB.session.scalars(
|
199
|
+
sqlalchemy.select(Tag).order_by(Tag.tag)
|
200
|
+
).all()
|
201
|
+
variables["photo_count"] = DB.session.scalar(
|
202
|
+
sqlalchemy.select(sqlalchemy.func.count()).select_from(Photo)
|
203
|
+
)
|
190
204
|
return variables
|
191
205
|
|
192
206
|
app.run(host=host, port=port)
|