geovisio 2.7.0__py3-none-any.whl → 2.8.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.
- geovisio/__init__.py +11 -3
- geovisio/admin_cli/__init__.py +3 -1
- geovisio/admin_cli/cleanup.py +2 -2
- geovisio/admin_cli/user.py +75 -0
- geovisio/config_app.py +87 -4
- geovisio/templates/main.html +2 -2
- geovisio/templates/viewer.html +3 -3
- geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/da/LC_MESSAGES/messages.po +850 -0
- geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/de/LC_MESSAGES/messages.po +235 -2
- geovisio/translations/el/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/el/LC_MESSAGES/messages.po +685 -0
- geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/en/LC_MESSAGES/messages.po +244 -153
- geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/eo/LC_MESSAGES/messages.po +790 -0
- geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.po +40 -3
- geovisio/translations/hu/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/hu/LC_MESSAGES/messages.po +773 -0
- geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/it/LC_MESSAGES/messages.po +875 -0
- geovisio/translations/ja/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ja/LC_MESSAGES/messages.po +719 -0
- geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/messages.pot +225 -148
- geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/nl/LC_MESSAGES/messages.po +24 -16
- geovisio/translations/pl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/pl/LC_MESSAGES/messages.po +727 -0
- geovisio/translations/zh_Hant/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/zh_Hant/LC_MESSAGES/messages.po +719 -0
- geovisio/utils/auth.py +80 -8
- geovisio/utils/link.py +3 -2
- geovisio/utils/model_query.py +55 -0
- geovisio/utils/pictures.py +29 -62
- geovisio/utils/semantics.py +120 -0
- geovisio/utils/sequences.py +30 -23
- geovisio/utils/tokens.py +5 -3
- geovisio/utils/upload_set.py +87 -64
- geovisio/utils/website.py +50 -0
- geovisio/web/annotations.py +17 -0
- geovisio/web/auth.py +9 -5
- geovisio/web/collections.py +235 -63
- geovisio/web/configuration.py +17 -1
- geovisio/web/docs.py +99 -54
- geovisio/web/items.py +233 -100
- geovisio/web/map.py +129 -31
- geovisio/web/pages.py +240 -0
- geovisio/web/params.py +17 -0
- geovisio/web/prepare.py +165 -0
- geovisio/web/stac.py +17 -4
- geovisio/web/tokens.py +14 -4
- geovisio/web/upload_set.py +19 -10
- geovisio/web/users.py +176 -44
- geovisio/workers/runner_pictures.py +75 -50
- {geovisio-2.7.0.dist-info → geovisio-2.8.0.dist-info}/METADATA +6 -5
- geovisio-2.8.0.dist-info/RECORD +89 -0
- {geovisio-2.7.0.dist-info → geovisio-2.8.0.dist-info}/WHEEL +1 -1
- geovisio-2.7.0.dist-info/RECORD +0 -66
- {geovisio-2.7.0.dist-info → geovisio-2.8.0.dist-info}/LICENSE +0 -0
geovisio/web/map.py
CHANGED
|
@@ -5,11 +5,12 @@ import io
|
|
|
5
5
|
from typing import Optional, Dict, Any, Tuple, List, Union
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
from flask import Blueprint, current_app, send_file, request, jsonify, url_for
|
|
8
|
-
from flask_babel import gettext as _
|
|
8
|
+
from flask_babel import gettext as _, get_locale
|
|
9
9
|
from geovisio.utils import auth, db
|
|
10
10
|
from geovisio.utils.auth import Account
|
|
11
11
|
from geovisio.web import params
|
|
12
12
|
from geovisio.web.utils import user_dependant_response
|
|
13
|
+
from geovisio.web.configuration import _get_translated
|
|
13
14
|
from geovisio import errors
|
|
14
15
|
from psycopg import sql
|
|
15
16
|
|
|
@@ -32,7 +33,9 @@ def get_style_json(forUser: Optional[Union[UUID, str]] = None):
|
|
|
32
33
|
tilesUrl = tilesUrl.replace("11111111", "{x}").replace("22222222", "{y}").replace("33333333", "{z}")
|
|
33
34
|
|
|
34
35
|
# Display sequence on all zooms if user tiles, after grid on general tiles
|
|
35
|
-
sequenceOpacity =
|
|
36
|
+
sequenceOpacity = (
|
|
37
|
+
["interpolate", ["linear"], ["zoom"], ZOOM_GRID_SEQUENCES + 0.25, 0, ZOOM_GRID_SEQUENCES + 1, 1] if forUser is None else 1
|
|
38
|
+
)
|
|
36
39
|
|
|
37
40
|
layers = [
|
|
38
41
|
{
|
|
@@ -69,35 +72,74 @@ def get_style_json(forUser: Optional[Union[UUID, str]] = None):
|
|
|
69
72
|
layers.append(
|
|
70
73
|
{
|
|
71
74
|
"id": f"{sourceId}_grid",
|
|
72
|
-
"type": "
|
|
75
|
+
"type": "circle",
|
|
73
76
|
"source": sourceId,
|
|
74
77
|
"source-layer": "grid",
|
|
78
|
+
"layout": {
|
|
79
|
+
"circle-sort-key": ["get", "coef"],
|
|
80
|
+
},
|
|
75
81
|
"paint": {
|
|
76
|
-
"
|
|
77
|
-
"fill-opacity": [
|
|
82
|
+
"circle-radius": [
|
|
78
83
|
"interpolate",
|
|
79
84
|
["linear"],
|
|
80
85
|
["zoom"],
|
|
81
|
-
0,
|
|
82
86
|
1,
|
|
87
|
+
# The match get coef rule allows to hide circle if coef is set to 0
|
|
88
|
+
["match", ["get", "coef"], 0, 0, 1],
|
|
83
89
|
ZOOM_GRID_SEQUENCES - 2,
|
|
84
|
-
|
|
90
|
+
["match", ["get", "coef"], 0, 0, 6],
|
|
91
|
+
ZOOM_GRID_SEQUENCES - 1,
|
|
92
|
+
["match", ["get", "coef"], 0, 0, 2.5],
|
|
85
93
|
ZOOM_GRID_SEQUENCES,
|
|
86
|
-
0
|
|
87
|
-
ZOOM_GRID_SEQUENCES +
|
|
94
|
+
["match", ["get", "coef"], 0, 0, 4],
|
|
95
|
+
ZOOM_GRID_SEQUENCES + 1,
|
|
96
|
+
["match", ["get", "coef"], 0, 0, 7],
|
|
97
|
+
],
|
|
98
|
+
"circle-color": ["interpolate", ["linear"], ["get", "coef"], 0, "#FFA726", 0.5, "#E65100", 1, "#3E2723"],
|
|
99
|
+
"circle-opacity": [
|
|
100
|
+
"interpolate",
|
|
101
|
+
["linear"],
|
|
102
|
+
["zoom"],
|
|
103
|
+
ZOOM_GRID_SEQUENCES - 2,
|
|
104
|
+
0.5,
|
|
105
|
+
ZOOM_GRID_SEQUENCES - 1,
|
|
106
|
+
1,
|
|
107
|
+
ZOOM_GRID_SEQUENCES + 0.75,
|
|
108
|
+
1,
|
|
109
|
+
ZOOM_GRID_SEQUENCES + 1,
|
|
88
110
|
0,
|
|
89
111
|
],
|
|
90
112
|
},
|
|
91
113
|
}
|
|
92
114
|
)
|
|
93
115
|
|
|
116
|
+
apiSum = current_app.config["API_SUMMARY"]
|
|
117
|
+
userLang = get_locale().language
|
|
118
|
+
|
|
94
119
|
style = {
|
|
95
120
|
"version": 8,
|
|
96
|
-
"name":
|
|
121
|
+
"name": _get_translated(apiSum.name, userLang)["label"],
|
|
122
|
+
"metadata": {
|
|
123
|
+
"panoramax:fields": {
|
|
124
|
+
"sequences": ["id", "account_id", "model", "type", "date", "gps_accuracy", "h_pixel_density"],
|
|
125
|
+
"pictures": ["id", "account_id", "ts", "heading", "sequences", "type", "model", "gps_accuracy", "h_pixel_density"],
|
|
126
|
+
}
|
|
127
|
+
},
|
|
97
128
|
"sources": {sourceId: {"type": "vector", "tiles": [tilesUrl], "minzoom": 0, "maxzoom": ZOOM_PICTURES}},
|
|
98
129
|
"layers": layers,
|
|
99
130
|
}
|
|
100
131
|
|
|
132
|
+
if forUser is None:
|
|
133
|
+
style["metadata"]["panoramax:fields"]["grid"] = [
|
|
134
|
+
"id",
|
|
135
|
+
"nb_pictures",
|
|
136
|
+
"nb_360_pictures",
|
|
137
|
+
"nb_flat_pictures",
|
|
138
|
+
"coef",
|
|
139
|
+
"coef_360_pictures",
|
|
140
|
+
"coef_flat_pictures",
|
|
141
|
+
]
|
|
142
|
+
|
|
101
143
|
return jsonify(style)
|
|
102
144
|
|
|
103
145
|
|
|
@@ -173,34 +215,44 @@ def getStyle():
|
|
|
173
215
|
def getTile(z: int, x: int, y: int, format: str):
|
|
174
216
|
"""Get pictures and sequences as vector tiles
|
|
175
217
|
|
|
176
|
-
Vector tiles contains different layers based on zoom level :
|
|
218
|
+
Vector tiles contains different layers based on zoom level : grid, sequences or pictures.
|
|
219
|
+
|
|
220
|
+
Layer "grid":
|
|
221
|
+
- Available on zoom levels 0 to 7 (excluded)
|
|
222
|
+
- Available properties:
|
|
223
|
+
- id
|
|
224
|
+
- nb_pictures
|
|
225
|
+
- nb_360_pictures (number of 360° pictures)
|
|
226
|
+
- nb_flat_pictures (number of flat pictures)
|
|
227
|
+
- coef (value from 0 to 1, relative quantity of available pictures)
|
|
228
|
+
- coef_360_pictures (value from 0 to 1, relative quantity of available 360° pictures)
|
|
229
|
+
- coef_flat_pictures (value from 0 to 1, relative quantity of available flat pictures)
|
|
177
230
|
|
|
178
231
|
Layer "sequences":
|
|
179
|
-
- Available on zoom levels >= 6
|
|
232
|
+
- Available on zoom levels >= 7 (and simplified version on zoom >= 6 and < 7)
|
|
180
233
|
- Available properties:
|
|
181
234
|
- id (sequence ID)
|
|
182
235
|
- account_id
|
|
183
236
|
- model (camera make and model)
|
|
184
237
|
- type (flat or equirectangular)
|
|
185
238
|
- date (capture date, as YYYY-MM-DD)
|
|
239
|
+
- gps_accuracy (95% confidence interval of GPS position precision, in meters)
|
|
240
|
+
- h_pixel_density (number of pixels on horizon per field of view degree)
|
|
186
241
|
|
|
187
242
|
Layer "pictures":
|
|
188
|
-
- Available on zoom levels >=
|
|
243
|
+
- Available on zoom levels >= 15
|
|
189
244
|
- Available properties:
|
|
190
245
|
- id (picture ID)
|
|
191
246
|
- account_id
|
|
192
247
|
- ts (picture date/time)
|
|
193
248
|
- heading (picture heading in degrees)
|
|
194
|
-
- sequences (list of sequences ID this pictures belongs to)
|
|
195
249
|
- type (flat or equirectangular)
|
|
250
|
+
- hidden (picture visibility, true or false)
|
|
196
251
|
- model (camera make and model)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
- id
|
|
202
|
-
- nb_pictures
|
|
203
|
-
- coef (value from 0 to 1, relative quantity of available pictures)
|
|
252
|
+
- gps_accuracy (95% confidence interval of GPS position precision, in meters)
|
|
253
|
+
- h_pixel_density (number of pixels on horizon per field of view degree)
|
|
254
|
+
- sequences (list of sequences ID this pictures belongs to)
|
|
255
|
+
- first_sequence (sequence ID, first from the list)
|
|
204
256
|
|
|
205
257
|
---
|
|
206
258
|
tags:
|
|
@@ -210,7 +262,7 @@ def getTile(z: int, x: int, y: int, format: str):
|
|
|
210
262
|
parameters:
|
|
211
263
|
- name: z
|
|
212
264
|
in: path
|
|
213
|
-
description: Zoom level (6 to
|
|
265
|
+
description: Zoom level (6 to 15)
|
|
214
266
|
required: true
|
|
215
267
|
schema:
|
|
216
268
|
type: number
|
|
@@ -294,16 +346,30 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
|
|
|
294
346
|
#
|
|
295
347
|
|
|
296
348
|
grid_fields = [
|
|
297
|
-
sql.SQL("ST_AsMVTGeom(ST_Transform(geom, 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom"),
|
|
349
|
+
sql.SQL("ST_AsMVTGeom(ST_Transform(ST_Centroid(geom), 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom"),
|
|
298
350
|
sql.SQL("id"),
|
|
299
351
|
sql.SQL("nb_pictures"),
|
|
352
|
+
sql.SQL("nb_360_pictures"),
|
|
353
|
+
sql.SQL("nb_pictures - nb_360_pictures AS nb_flat_pictures"),
|
|
300
354
|
sql.SQL(
|
|
301
|
-
"""
|
|
302
|
-
((CASE WHEN nb_pictures <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid)
|
|
355
|
+
"""((CASE WHEN nb_pictures = 0 THEN 0 WHEN nb_pictures <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid)
|
|
303
356
|
THEN nb_pictures::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid) * 0.5
|
|
304
357
|
ELSE 0.5 + nb_pictures::float / (SELECT MAX(nb_pictures) FROM pictures_grid) * 0.5
|
|
305
|
-
END) * 10)::int / 10::float AS coef
|
|
306
|
-
|
|
358
|
+
END) * 10)::int / 10::float AS coef"""
|
|
359
|
+
),
|
|
360
|
+
sql.SQL(
|
|
361
|
+
"""((CASE WHEN nb_360_pictures = 0 THEN 0
|
|
362
|
+
WHEN nb_360_pictures <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) from pictures_grid)
|
|
363
|
+
THEN nb_360_pictures::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) from pictures_grid) * 0.5
|
|
364
|
+
ELSE 0.5 + nb_360_pictures::float / (SELECT MAX(nb_360_pictures) FROM pictures_grid) * 0.5
|
|
365
|
+
END) * 10)::int / 10::float AS coef_360_pictures"""
|
|
366
|
+
),
|
|
367
|
+
sql.SQL(
|
|
368
|
+
"""((CASE WHEN (nb_pictures - nb_360_pictures) = 0 THEN 0
|
|
369
|
+
WHEN (nb_pictures - nb_360_pictures) <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) from pictures_grid)
|
|
370
|
+
THEN (nb_pictures - nb_360_pictures)::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) from pictures_grid) * 0.5
|
|
371
|
+
ELSE 0.5 + (nb_pictures - nb_360_pictures)::float / (SELECT MAX((nb_pictures - nb_360_pictures)) FROM pictures_grid) * 0.5
|
|
372
|
+
END) * 10)::int / 10::float AS coef_flat_pictures"""
|
|
307
373
|
),
|
|
308
374
|
]
|
|
309
375
|
sequences_fields = [
|
|
@@ -322,6 +388,8 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
|
|
|
322
388
|
sql.SQL("computed_model AS model"),
|
|
323
389
|
sql.SQL("computed_type AS type"),
|
|
324
390
|
sql.SQL("computed_capture_date AS date"),
|
|
391
|
+
sql.SQL("computed_gps_accuracy AS gps_accuracy"),
|
|
392
|
+
sql.SQL("computed_h_pixel_density AS h_pixel_density"),
|
|
325
393
|
]
|
|
326
394
|
)
|
|
327
395
|
|
|
@@ -351,15 +419,18 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
|
|
|
351
419
|
ST_AsMVTGeom(ST_Transform(p.geom, 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom,
|
|
352
420
|
p.id, p.ts, p.heading, p.account_id,
|
|
353
421
|
NULLIF(p.status != 'ready' OR s.status != 'ready', FALSE) AS hidden,
|
|
354
|
-
array_to_json(ARRAY_AGG(sp.seq_id)) AS sequences,
|
|
355
422
|
p.metadata->>'type' AS type,
|
|
356
|
-
TRIM(CONCAT(p.metadata->>'make', ' ', p.metadata->>'model')) AS model
|
|
423
|
+
TRIM(CONCAT(p.metadata->>'make', ' ', p.metadata->>'model')) AS model,
|
|
424
|
+
gps_accuracy_m AS gps_accuracy,
|
|
425
|
+
h_pixel_density,
|
|
426
|
+
array_to_json(ARRAY_AGG(sp.seq_id)) AS sequences,
|
|
427
|
+
MIN(sp.seq_id::varchar) AS first_sequence
|
|
357
428
|
FROM pictures p
|
|
358
429
|
LEFT JOIN sequences_pictures sp ON p.id = sp.pic_id
|
|
359
430
|
LEFT JOIN sequences s ON s.id = sp.seq_id
|
|
360
431
|
WHERE
|
|
361
432
|
{pictures_filter}
|
|
362
|
-
GROUP BY 1, 2, 3, 4, 5, 6
|
|
433
|
+
GROUP BY 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
|
|
363
434
|
) mvtgeompics
|
|
364
435
|
) mvtpictures
|
|
365
436
|
"""
|
|
@@ -468,7 +539,34 @@ def getUserStyle(userId: UUID):
|
|
|
468
539
|
@user_dependant_response(True)
|
|
469
540
|
def getUserTile(userId: UUID, z: int, x: int, y: int, format: str):
|
|
470
541
|
"""Get pictures and sequences as vector tiles for a specific user.
|
|
471
|
-
|
|
542
|
+
|
|
543
|
+
Vector tiles contains different layers based on zoom level : sequences, lowzoom_360pictures or pictures.
|
|
544
|
+
|
|
545
|
+
Layer "sequences":
|
|
546
|
+
- Available on all zoom levels
|
|
547
|
+
- Available properties:
|
|
548
|
+
- id (sequence ID)
|
|
549
|
+
- account_id
|
|
550
|
+
- model (camera make and model)
|
|
551
|
+
- type (flat or equirectangular)
|
|
552
|
+
- date (capture date, as YYYY-MM-DD)
|
|
553
|
+
- gps_accuracy (95% confidence interval of GPS position precision, in meters)
|
|
554
|
+
- h_pixel_density (number of pixels on horizon per field of view degree)
|
|
555
|
+
|
|
556
|
+
Layer "pictures":
|
|
557
|
+
- Available on zoom levels >= 15
|
|
558
|
+
- Available properties:
|
|
559
|
+
- id (picture ID)
|
|
560
|
+
- account_id
|
|
561
|
+
- ts (picture date/time)
|
|
562
|
+
- heading (picture heading in degrees)
|
|
563
|
+
- type (flat or equirectangular)
|
|
564
|
+
- hidden (picture visibility, true or false)
|
|
565
|
+
- model (camera make and model)
|
|
566
|
+
- gps_accuracy (95% confidence interval of GPS position precision, in meters)
|
|
567
|
+
- h_pixel_density (number of pixels on horizon per field of view degree)
|
|
568
|
+
- sequences (list of sequences ID this pictures belongs to)
|
|
569
|
+
- first_sequence (sequence ID, first from the list)
|
|
472
570
|
|
|
473
571
|
---
|
|
474
572
|
tags:
|
geovisio/web/pages.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
from flask import current_app, request, url_for, Blueprint
|
|
2
|
+
from pydantic import BaseModel, ConfigDict
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import List
|
|
5
|
+
from geovisio.utils import db, auth
|
|
6
|
+
from geovisio.utils.link import Link, make_link
|
|
7
|
+
from geovisio.errors import InvalidAPIUsage
|
|
8
|
+
from flask_babel import gettext as _
|
|
9
|
+
from psycopg.sql import SQL
|
|
10
|
+
|
|
11
|
+
bp = Blueprint("pages", __name__, url_prefix="/api")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PageName(Enum):
|
|
15
|
+
end_user_license_agreement = "end-user-license-agreement"
|
|
16
|
+
terms_of_service = "terms-of-service"
|
|
17
|
+
end_user_license_agreement_summary = "end-user-license-agreement-summary"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PageLanguage(BaseModel):
|
|
21
|
+
"""A specific language for the page"""
|
|
22
|
+
|
|
23
|
+
language: str
|
|
24
|
+
"""The language (as ISO 639-2 code)"""
|
|
25
|
+
|
|
26
|
+
links: List[Link]
|
|
27
|
+
"""Link to page content"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PageSummary(BaseModel):
|
|
31
|
+
"""Page summary"""
|
|
32
|
+
|
|
33
|
+
name: PageName
|
|
34
|
+
"""Page name"""
|
|
35
|
+
languages: List[PageLanguage]
|
|
36
|
+
"""Available translations"""
|
|
37
|
+
|
|
38
|
+
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def check_page_name(v: str) -> PageName:
|
|
42
|
+
try:
|
|
43
|
+
return PageName(v)
|
|
44
|
+
except ValueError:
|
|
45
|
+
raise InvalidAPIUsage(_("Page name is not recognized"), status_code=400)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@bp.route("/pages/<page>", methods=["GET"])
|
|
49
|
+
def getPageLanguages(page):
|
|
50
|
+
"""List available languages for a single page
|
|
51
|
+
---
|
|
52
|
+
tags:
|
|
53
|
+
- Configuration
|
|
54
|
+
parameters:
|
|
55
|
+
- name: page
|
|
56
|
+
in: path
|
|
57
|
+
description: Page name
|
|
58
|
+
required: true
|
|
59
|
+
schema:
|
|
60
|
+
$ref: '#/components/schemas/GeoVisioPageName'
|
|
61
|
+
responses:
|
|
62
|
+
200:
|
|
63
|
+
description: the languages list
|
|
64
|
+
content:
|
|
65
|
+
application/json:
|
|
66
|
+
schema:
|
|
67
|
+
$ref: '#/components/schemas/GeoVisioPageSummary'
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
name = check_page_name(page)
|
|
71
|
+
langs = [d[0] for d in db.fetchall(current_app, SQL("SELECT lang FROM pages WHERE name = %(name)s"), {"name": name.value})]
|
|
72
|
+
|
|
73
|
+
# If page doesn't exist yet, send empty list of languages
|
|
74
|
+
if langs is None or len(langs) == 0:
|
|
75
|
+
langs = []
|
|
76
|
+
|
|
77
|
+
summary = PageSummary(
|
|
78
|
+
name=name,
|
|
79
|
+
languages=[PageLanguage(language=l, links=[make_link(rel="self", route="pages.getPage", page=name.value, lang=l)]) for l in langs],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
summary.model_dump_json(exclude_none=True),
|
|
84
|
+
200,
|
|
85
|
+
{
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@bp.route("/pages/<page>/<lang>", methods=["GET"])
|
|
92
|
+
def getPage(page, lang):
|
|
93
|
+
"""Get page HTML content for a certain language
|
|
94
|
+
---
|
|
95
|
+
tags:
|
|
96
|
+
- Configuration
|
|
97
|
+
parameters:
|
|
98
|
+
- name: page
|
|
99
|
+
in: path
|
|
100
|
+
description: Page name
|
|
101
|
+
required: true
|
|
102
|
+
schema:
|
|
103
|
+
$ref: '#/components/schemas/GeoVisioPageName'
|
|
104
|
+
- name: lang
|
|
105
|
+
in: path
|
|
106
|
+
description: Language ISO 639-2 code
|
|
107
|
+
required: true
|
|
108
|
+
schema:
|
|
109
|
+
type: string
|
|
110
|
+
responses:
|
|
111
|
+
200:
|
|
112
|
+
description: the HTML content for this page
|
|
113
|
+
content:
|
|
114
|
+
text/html:
|
|
115
|
+
schema:
|
|
116
|
+
type: string
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
page = check_page_name(page)
|
|
120
|
+
page_content = db.fetchone(
|
|
121
|
+
current_app,
|
|
122
|
+
SQL("SELECT content FROM pages WHERE name = %(name)s AND lang = %(lang)s"),
|
|
123
|
+
{"name": page.value, "lang": lang},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if page_content is None:
|
|
127
|
+
raise InvalidAPIUsage(_("Page not available in language %(l)s", l=lang), status_code=404)
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
page_content[0],
|
|
131
|
+
200,
|
|
132
|
+
{
|
|
133
|
+
"Content-Type": "text/html",
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@bp.route("/pages/<page>/<lang>", methods=["POST", "PUT"])
|
|
139
|
+
@auth.login_required()
|
|
140
|
+
def postPage(page, lang, account):
|
|
141
|
+
"""Save HTML content for a certain language of a page.
|
|
142
|
+
|
|
143
|
+
This call is only available for account with admin role.
|
|
144
|
+
---
|
|
145
|
+
tags:
|
|
146
|
+
- Configuration
|
|
147
|
+
parameters:
|
|
148
|
+
- name: page
|
|
149
|
+
in: path
|
|
150
|
+
description: Page name
|
|
151
|
+
required: true
|
|
152
|
+
schema:
|
|
153
|
+
$ref: '#/components/schemas/GeoVisioPageName'
|
|
154
|
+
- name: lang
|
|
155
|
+
in: path
|
|
156
|
+
description: Language ISO 639-2 code
|
|
157
|
+
required: true
|
|
158
|
+
schema:
|
|
159
|
+
type: string
|
|
160
|
+
security:
|
|
161
|
+
- bearerToken: []
|
|
162
|
+
- cookieAuth: []
|
|
163
|
+
requestBody:
|
|
164
|
+
content:
|
|
165
|
+
text/html:
|
|
166
|
+
schema:
|
|
167
|
+
type: string
|
|
168
|
+
responses:
|
|
169
|
+
200:
|
|
170
|
+
description: Successfully saved
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
name = check_page_name(page)
|
|
174
|
+
|
|
175
|
+
if not account.can_edit_pages():
|
|
176
|
+
raise InvalidAPIUsage(_("You must be logged-in as admin to edit pages"), 403)
|
|
177
|
+
if request.content_type != "text/html":
|
|
178
|
+
raise InvalidAPIUsage(_("Page content must be HTML (with " "Content-Type: text/html" " header set)"), 400)
|
|
179
|
+
|
|
180
|
+
with db.execute(
|
|
181
|
+
current_app,
|
|
182
|
+
SQL(
|
|
183
|
+
"""
|
|
184
|
+
INSERT INTO pages (name, lang, content)
|
|
185
|
+
VALUES (%(name)s, %(lang)s, %(content)s)
|
|
186
|
+
ON CONFLICT (name, lang) DO UPDATE SET content=EXCLUDED.content
|
|
187
|
+
"""
|
|
188
|
+
),
|
|
189
|
+
{"name": name.value, "lang": lang, "content": request.get_data(as_text=True)},
|
|
190
|
+
) as res:
|
|
191
|
+
if not res.rowcount:
|
|
192
|
+
raise InvalidAPIUsage(_("Could not update page content"), 500)
|
|
193
|
+
|
|
194
|
+
return "", 200
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@bp.route("/pages/<page>/<lang>", methods=["DELETE"])
|
|
198
|
+
@auth.login_required()
|
|
199
|
+
def deletePage(page, lang, account):
|
|
200
|
+
"""Delete HTML content for a certain language of a page.
|
|
201
|
+
|
|
202
|
+
This call is only available for account with admin role.
|
|
203
|
+
---
|
|
204
|
+
tags:
|
|
205
|
+
- Configuration
|
|
206
|
+
parameters:
|
|
207
|
+
- name: page
|
|
208
|
+
in: path
|
|
209
|
+
description: Page name
|
|
210
|
+
required: true
|
|
211
|
+
schema:
|
|
212
|
+
$ref: '#/components/schemas/GeoVisioPageName'
|
|
213
|
+
- name: lang
|
|
214
|
+
in: path
|
|
215
|
+
description: Language ISO 639-2 code
|
|
216
|
+
required: true
|
|
217
|
+
schema:
|
|
218
|
+
type: string
|
|
219
|
+
security:
|
|
220
|
+
- bearerToken: []
|
|
221
|
+
- cookieAuth: []
|
|
222
|
+
responses:
|
|
223
|
+
200:
|
|
224
|
+
description: Successfully deleted
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
name = check_page_name(page)
|
|
228
|
+
|
|
229
|
+
if not account.can_edit_pages():
|
|
230
|
+
raise InvalidAPIUsage(_("You must be logged-in as admin to edit pages"), 403)
|
|
231
|
+
|
|
232
|
+
with db.execute(
|
|
233
|
+
current_app, SQL("DELETE FROM pages WHERE name = %(name)s AND lang = %(lang)s"), {"name": name.value, "lang": lang}
|
|
234
|
+
) as res:
|
|
235
|
+
if res.rowcount == 0:
|
|
236
|
+
raise InvalidAPIUsage(_("Page not available in language %(l)s", l=lang), status_code=404)
|
|
237
|
+
elif not res.rowcount:
|
|
238
|
+
raise InvalidAPIUsage(_("Could not delete page content"), 500)
|
|
239
|
+
|
|
240
|
+
return "", 200
|
geovisio/web/params.py
CHANGED
|
@@ -358,6 +358,23 @@ def parse_filter(value: Optional[str]) -> Optional[sql.SQL]:
|
|
|
358
358
|
return None
|
|
359
359
|
|
|
360
360
|
|
|
361
|
+
def parse_picture_heading(heading: Optional[str]) -> Optional[int]:
|
|
362
|
+
if heading is None:
|
|
363
|
+
return None
|
|
364
|
+
try:
|
|
365
|
+
heading = int(heading)
|
|
366
|
+
if heading < 0 or heading > 360:
|
|
367
|
+
raise ValueError()
|
|
368
|
+
return heading
|
|
369
|
+
except ValueError:
|
|
370
|
+
raise errors.InvalidAPIUsage(
|
|
371
|
+
_(
|
|
372
|
+
"Heading is not valid, should be an integer in degrees from 0° to 360°. North is 0°, East = 90°, South = 180° and West = 270°."
|
|
373
|
+
),
|
|
374
|
+
status_code=400,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
361
378
|
class _FilterAstUpdated(Evaluator):
|
|
362
379
|
"""
|
|
363
380
|
We alter the parsed AST in order to always query for 'hidden' pictures when we query for 'deleted' ones
|