geovisio 2.10.0__py3-none-any.whl → 2.11.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 +3 -1
- geovisio/admin_cli/user.py +7 -2
- geovisio/config_app.py +21 -7
- geovisio/translations/be/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/be/LC_MESSAGES/messages.po +886 -0
- geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/da/LC_MESSAGES/messages.po +96 -5
- geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/de/LC_MESSAGES/messages.po +171 -132
- geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/en/LC_MESSAGES/messages.po +169 -146
- geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/eo/LC_MESSAGES/messages.po +3 -2
- geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.po +3 -2
- geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/it/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/messages.pot +159 -138
- geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/nl/LC_MESSAGES/messages.po +44 -2
- geovisio/translations/oc/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/oc/LC_MESSAGES/messages.po +9 -6
- geovisio/translations/pt/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/pt/LC_MESSAGES/messages.po +944 -0
- geovisio/translations/pt_BR/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/pt_BR/LC_MESSAGES/messages.po +942 -0
- geovisio/translations/sv/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/sv/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/tr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/tr/LC_MESSAGES/messages.po +927 -0
- geovisio/translations/uk/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/uk/LC_MESSAGES/messages.po +920 -0
- geovisio/utils/annotations.py +7 -4
- geovisio/utils/auth.py +33 -0
- geovisio/utils/cql2.py +20 -3
- geovisio/utils/pictures.py +16 -18
- geovisio/utils/sequences.py +104 -75
- geovisio/utils/upload_set.py +20 -10
- geovisio/utils/users.py +18 -0
- geovisio/web/annotations.py +96 -3
- geovisio/web/collections.py +169 -76
- geovisio/web/configuration.py +12 -0
- geovisio/web/docs.py +17 -3
- geovisio/web/items.py +129 -72
- geovisio/web/map.py +92 -54
- geovisio/web/pages.py +48 -4
- geovisio/web/params.py +56 -11
- geovisio/web/pictures.py +3 -3
- geovisio/web/prepare.py +4 -2
- geovisio/web/queryables.py +57 -0
- geovisio/web/stac.py +8 -2
- geovisio/web/upload_set.py +83 -26
- geovisio/web/users.py +85 -4
- geovisio/web/utils.py +24 -6
- {geovisio-2.10.0.dist-info → geovisio-2.11.0.dist-info}/METADATA +3 -2
- {geovisio-2.10.0.dist-info → geovisio-2.11.0.dist-info}/RECORD +58 -46
- {geovisio-2.10.0.dist-info → geovisio-2.11.0.dist-info}/WHEEL +0 -0
- {geovisio-2.10.0.dist-info → geovisio-2.11.0.dist-info}/licenses/LICENSE +0 -0
geovisio/web/map.py
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import io
|
|
5
5
|
from typing import Optional, Dict, Any, Tuple, List, Union
|
|
6
6
|
from uuid import UUID
|
|
7
|
-
from
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from flask import Blueprint, current_app, send_file, request, jsonify, url_for, g
|
|
8
9
|
from flask_babel import gettext as _, get_locale
|
|
9
10
|
from geovisio.utils import auth, db
|
|
10
11
|
from geovisio.utils.auth import Account
|
|
@@ -13,6 +14,7 @@ from geovisio.web.utils import user_dependant_response
|
|
|
13
14
|
from geovisio.web.configuration import _get_translated
|
|
14
15
|
from geovisio import errors
|
|
15
16
|
from psycopg import sql
|
|
17
|
+
import psycopg
|
|
16
18
|
|
|
17
19
|
bp = Blueprint("map", __name__, url_prefix="/api")
|
|
18
20
|
|
|
@@ -139,6 +141,14 @@ def get_style_json(forUser: Optional[Union[UUID, str]] = None):
|
|
|
139
141
|
"coef_360_pictures",
|
|
140
142
|
"coef_flat_pictures",
|
|
141
143
|
]
|
|
144
|
+
if _has_non_public_fields():
|
|
145
|
+
style["metadata"]["panoramax:fields"]["grid"].extend(
|
|
146
|
+
[
|
|
147
|
+
"logged_coef",
|
|
148
|
+
"logged_coef_360_pictures",
|
|
149
|
+
"logged_coef_flat_pictures",
|
|
150
|
+
]
|
|
151
|
+
)
|
|
142
152
|
|
|
143
153
|
return jsonify(style)
|
|
144
154
|
|
|
@@ -211,7 +221,6 @@ def getStyle():
|
|
|
211
221
|
|
|
212
222
|
|
|
213
223
|
@bp.route("/map/<int:z>/<int:x>/<int:y>.<format>")
|
|
214
|
-
@user_dependant_response(False)
|
|
215
224
|
def getTile(z: int, x: int, y: int, format: str):
|
|
216
225
|
"""Get pictures and sequences as vector tiles
|
|
217
226
|
|
|
@@ -227,6 +236,9 @@ def getTile(z: int, x: int, y: int, format: str):
|
|
|
227
236
|
- coef (value from 0 to 1, relative quantity of available pictures)
|
|
228
237
|
- coef_360_pictures (value from 0 to 1, relative quantity of available 360° pictures)
|
|
229
238
|
- coef_flat_pictures (value from 0 to 1, relative quantity of available flat pictures)
|
|
239
|
+
- logged_coef (value from 0 to 1, relative quantity of available pictures for logged users)
|
|
240
|
+
- logged_coef_360_pictures (value from 0 to 1, relative quantity of available 360° pictures for logged users)
|
|
241
|
+
- logged_coef_flat_pictures (value from 0 to 1, relative quantity of available flat pictures for logged users)
|
|
230
242
|
|
|
231
243
|
Layer "sequences":
|
|
232
244
|
- Available on zoom levels >= 7 (and simplified version on zoom >= 6 and < 7)
|
|
@@ -296,6 +308,26 @@ def getTile(z: int, x: int, y: int, format: str):
|
|
|
296
308
|
return _getTile(z, x, y, format, onlyForUser=None)
|
|
297
309
|
|
|
298
310
|
|
|
311
|
+
def _has_non_public_fields():
|
|
312
|
+
"""Check if the database has the `nb_non_public_pictures` field."""
|
|
313
|
+
if current_app.config["API_REGISTRATION_IS_OPEN"] is True:
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
@lru_cache(maxsize=100)
|
|
317
|
+
def check_db():
|
|
318
|
+
"""This function can be dropped in the next version (and only use the `API_REGISTRATION_IS_OPEN` config).
|
|
319
|
+
We do it because the pictures_grid materialized view is expensive to compute and we want the API to work during the schema migration.
|
|
320
|
+
We also cache it to not slow down every query, and eventually (after the limit is reached or the API is restarted), the API will start returning the non-public fields.
|
|
321
|
+
"""
|
|
322
|
+
try:
|
|
323
|
+
db.fetchone(current_app, "SELECT nb_non_public_pictures FROM pictures_grid LIMIT 1")
|
|
324
|
+
except psycopg.errors.UndefinedColumn:
|
|
325
|
+
return False
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
return check_db()
|
|
329
|
+
|
|
330
|
+
|
|
299
331
|
def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_filter: Optional[sql.SQL]) -> Tuple[sql.Composed, Dict]:
|
|
300
332
|
"""Returns appropriate SQL query according to given zoom"""
|
|
301
333
|
|
|
@@ -320,10 +352,10 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
|
|
|
320
352
|
if additional_filter:
|
|
321
353
|
sequences_filter.append(additional_filter)
|
|
322
354
|
filter_str = additional_filter.as_string(None)
|
|
323
|
-
if "
|
|
355
|
+
if "visibility" in filter_str:
|
|
324
356
|
# hack to have a coherent filter between the APIs
|
|
325
|
-
# if asked for status='hidden', we want both hidden pics and hidden sequences
|
|
326
|
-
pic_additional_filter_str = filter_str.replace("s.
|
|
357
|
+
# if asked for visibility <> 'anyone' (status='hidden' in API), we want both hidden pics and hidden sequences
|
|
358
|
+
pic_additional_filter_str = filter_str.replace("s.visibility", "p.visibility")
|
|
327
359
|
pic_additional_filter = sql.SQL(pic_additional_filter_str) # type: ignore
|
|
328
360
|
pictures_filter.append(sql.SQL("(") + sql.SQL(" OR ").join([pic_additional_filter, additional_filter]) + sql.SQL(")"))
|
|
329
361
|
|
|
@@ -334,56 +366,65 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
|
|
|
334
366
|
params["account"] = onlyForUser
|
|
335
367
|
|
|
336
368
|
# Not logged-in requests -> only show "ready" pics/sequences
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
pictures_filter.append(sql.SQL("s
|
|
343
|
-
|
|
369
|
+
if current_app.config["API_REGISTRATION_IS_OPEN"] is False or onlyForUser:
|
|
370
|
+
# for instances that supports logged-only data, we cannot add the tiles in a public cache (since non authenticated users could access this cache)
|
|
371
|
+
g.user_dependant_response = True
|
|
372
|
+
params["account_to_query"] = auth.get_current_account_id()
|
|
373
|
+
sequences_filter.append(sql.SQL("is_sequence_visible_by_user(s, %(account_to_query)s)"))
|
|
374
|
+
pictures_filter.append(sql.SQL("is_picture_visible_by_user(p, %(account_to_query)s)"))
|
|
375
|
+
pictures_filter.append(sql.SQL("is_sequence_visible_by_user(s, %(account_to_query)s)"))
|
|
376
|
+
else:
|
|
377
|
+
# for public instances, we can add the tiles in a public cache, and we only show the 'anyone' visibility
|
|
378
|
+
g.user_dependant_response = False
|
|
379
|
+
sequences_filter.append(sql.SQL("s.visibility = 'anyone'"))
|
|
380
|
+
pictures_filter.append(sql.SQL("s.visibility = 'anyone'"))
|
|
381
|
+
pictures_filter.append(sql.SQL("p.visibility = 'anyone'"))
|
|
382
|
+
|
|
383
|
+
sequences_filter.append(sql.SQL("s.status = 'ready'"))
|
|
384
|
+
pictures_filter.append(sql.SQL("p.preparing_status = 'prepared'"))
|
|
385
|
+
pictures_filter.append(sql.SQL("s.status = 'ready'"))
|
|
344
386
|
#############################################################
|
|
345
387
|
# SQL Result columns/fields
|
|
346
388
|
#
|
|
347
389
|
|
|
390
|
+
grid_coef_field = """((CASE WHEN {count_field} = 0
|
|
391
|
+
THEN 0
|
|
392
|
+
WHEN {count_field} <= (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY {count_field}) FILTER (WHERE {count_field} > 0) FROM pictures_grid)
|
|
393
|
+
THEN
|
|
394
|
+
{count_field}::float / (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY {count_field}) FILTER (WHERE {count_field} > 0) FROM pictures_grid) * 0.5
|
|
395
|
+
ELSE
|
|
396
|
+
0.5 + {count_field}::float / (SELECT MAX({count_field}) FROM pictures_grid) * 0.5
|
|
397
|
+
END) * 10)::int / 10::float AS {coef_field}"""
|
|
398
|
+
|
|
348
399
|
grid_fields = [
|
|
349
400
|
sql.SQL("ST_AsMVTGeom(ST_Transform(ST_Centroid(geom), 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom"),
|
|
350
401
|
sql.SQL("id"),
|
|
351
402
|
sql.SQL("nb_pictures"),
|
|
352
403
|
sql.SQL("nb_360_pictures"),
|
|
353
404
|
sql.SQL("nb_pictures - nb_360_pictures AS nb_flat_pictures"),
|
|
354
|
-
sql.SQL(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
WHEN nb_pictures <= (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) FILTER (WHERE nb_pictures > 0) FROM pictures_grid)
|
|
358
|
-
THEN
|
|
359
|
-
nb_pictures::float / (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) FILTER (WHERE nb_pictures > 0) FROM pictures_grid) * 0.5
|
|
360
|
-
ELSE
|
|
361
|
-
0.5 + nb_pictures::float / (SELECT MAX(nb_pictures) FROM pictures_grid) * 0.5
|
|
362
|
-
END) * 10)::int / 10::float AS coef"""
|
|
363
|
-
),
|
|
364
|
-
sql.SQL(
|
|
365
|
-
"""((CASE WHEN nb_360_pictures = 0
|
|
366
|
-
THEN 0
|
|
367
|
-
WHEN (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) FILTER (WHERE nb_360_pictures > 0) FROM pictures_grid) = 0
|
|
368
|
-
THEN 0
|
|
369
|
-
WHEN nb_360_pictures <= (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) FILTER (WHERE nb_360_pictures > 0) FROM pictures_grid)
|
|
370
|
-
THEN nb_360_pictures::float / (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) FILTER (WHERE nb_360_pictures > 0) FROM pictures_grid) * 0.5
|
|
371
|
-
ELSE
|
|
372
|
-
0.5 + nb_360_pictures::float / (SELECT MAX(nb_360_pictures) FROM pictures_grid) * 0.5
|
|
373
|
-
END) * 10)::int / 10::float AS coef_360_pictures"""
|
|
374
|
-
),
|
|
375
|
-
sql.SQL(
|
|
376
|
-
"""((CASE WHEN (nb_pictures - nb_360_pictures) = 0
|
|
377
|
-
THEN 0
|
|
378
|
-
WHEN (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) FILTER (WHERE (nb_pictures - nb_360_pictures) > 0) FROM pictures_grid) = 0
|
|
379
|
-
THEN 0
|
|
380
|
-
WHEN (nb_pictures - nb_360_pictures) <= (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) FILTER (WHERE (nb_pictures - nb_360_pictures) > 0) FROM pictures_grid)
|
|
381
|
-
THEN (nb_pictures - nb_360_pictures)::float / (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) FILTER (WHERE (nb_pictures - nb_360_pictures) > 0) FROM pictures_grid) * 0.5
|
|
382
|
-
ELSE
|
|
383
|
-
0.5 + (nb_pictures - nb_360_pictures)::float / (SELECT MAX((nb_pictures - nb_360_pictures)) FROM pictures_grid) * 0.5
|
|
384
|
-
END) * 10)::int / 10::float AS coef_flat_pictures"""
|
|
385
|
-
),
|
|
405
|
+
sql.SQL(grid_coef_field.format(count_field="nb_pictures", coef_field="coef")),
|
|
406
|
+
sql.SQL(grid_coef_field.format(count_field="nb_360_pictures", coef_field="coef_360_pictures")),
|
|
407
|
+
sql.SQL(grid_coef_field.format(count_field="(nb_pictures - nb_360_pictures)", coef_field="coef_flat_pictures")),
|
|
386
408
|
]
|
|
409
|
+
if _has_non_public_fields():
|
|
410
|
+
# we also add non-public pictures
|
|
411
|
+
grid_fields.extend(
|
|
412
|
+
[
|
|
413
|
+
sql.SQL(grid_coef_field.format(count_field="(nb_non_public_pictures + nb_pictures)", coef_field="logged_coef")),
|
|
414
|
+
sql.SQL(
|
|
415
|
+
grid_coef_field.format(
|
|
416
|
+
count_field="(nb_non_public_360_pictures + nb_360_pictures)", coef_field="logged_coef_360_pictures"
|
|
417
|
+
)
|
|
418
|
+
),
|
|
419
|
+
sql.SQL(
|
|
420
|
+
grid_coef_field.format(
|
|
421
|
+
count_field="((nb_non_public_360_pictures + nb_360_pictures) - (nb_non_public_360_pictures + nb_360_pictures))",
|
|
422
|
+
coef_field="logged_coef_flat_pictures",
|
|
423
|
+
)
|
|
424
|
+
),
|
|
425
|
+
]
|
|
426
|
+
)
|
|
427
|
+
|
|
387
428
|
sequences_fields = [
|
|
388
429
|
sql.SQL("ST_AsMVTGeom(ST_Transform(geom, 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom"),
|
|
389
430
|
sql.SQL("id"),
|
|
@@ -396,7 +437,7 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
|
|
|
396
437
|
sequences_fields.extend(
|
|
397
438
|
[
|
|
398
439
|
sql.SQL("account_id"),
|
|
399
|
-
sql.SQL("NULLIF(
|
|
440
|
+
sql.SQL("NULLIF(visibility != 'anyone', FALSE) AS hidden"),
|
|
400
441
|
sql.SQL("computed_model AS model"),
|
|
401
442
|
sql.SQL("computed_type AS type"),
|
|
402
443
|
sql.SQL("computed_capture_date AS date"),
|
|
@@ -412,8 +453,7 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
|
|
|
412
453
|
# Full pictures + sequences (z15+)
|
|
413
454
|
if z >= ZOOM_PICTURES:
|
|
414
455
|
query = sql.SQL(
|
|
415
|
-
"""
|
|
416
|
-
SELECT mvtsequences.mvt || mvtpictures.mvt
|
|
456
|
+
"""SELECT mvtsequences.mvt || mvtpictures.mvt
|
|
417
457
|
FROM (
|
|
418
458
|
SELECT ST_AsMVT(mvtgeomseqs.*, 'sequences') AS mvt
|
|
419
459
|
FROM (
|
|
@@ -430,7 +470,7 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
|
|
|
430
470
|
SELECT
|
|
431
471
|
ST_AsMVTGeom(ST_Transform(p.geom, 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom,
|
|
432
472
|
p.id, p.ts, p.heading, p.account_id,
|
|
433
|
-
NULLIF(p.
|
|
473
|
+
NULLIF(p.visibility = 'owner-only' OR s.visibility = 'owner-only', FALSE) AS hidden,
|
|
434
474
|
p.metadata->>'type' AS type,
|
|
435
475
|
TRIM(CONCAT(p.metadata->>'make', ' ', p.metadata->>'model')) AS model,
|
|
436
476
|
gps_accuracy_m AS gps_accuracy,
|
|
@@ -455,8 +495,7 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
|
|
|
455
495
|
# Full sequences (z7-14.9 and z0-14.9 for specific users)
|
|
456
496
|
elif z >= ZOOM_GRID_SEQUENCES + 1 or onlyForUser:
|
|
457
497
|
query = sql.SQL(
|
|
458
|
-
"""
|
|
459
|
-
SELECT ST_AsMVT(mvtsequences.*, 'sequences') AS mvt
|
|
498
|
+
"""SELECT ST_AsMVT(mvtsequences.*, 'sequences') AS mvt
|
|
460
499
|
FROM (
|
|
461
500
|
SELECT
|
|
462
501
|
{sequences_fields}
|
|
@@ -470,8 +509,7 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
|
|
|
470
509
|
# Sequences + grid (z6-6.9)
|
|
471
510
|
elif z >= ZOOM_GRID_SEQUENCES:
|
|
472
511
|
query = sql.SQL(
|
|
473
|
-
"""
|
|
474
|
-
SELECT mvtsequences.mvt || mvtgrid.mvt
|
|
512
|
+
"""SELECT mvtsequences.mvt || mvtgrid.mvt
|
|
475
513
|
FROM (
|
|
476
514
|
SELECT ST_AsMVT(mvtgeomseqs.*, 'sequences') AS mvt
|
|
477
515
|
FROM (
|
|
@@ -502,8 +540,7 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
|
|
|
502
540
|
# Grid overview (all users + z0-5.9)
|
|
503
541
|
else:
|
|
504
542
|
query = sql.SQL(
|
|
505
|
-
"""
|
|
506
|
-
SELECT ST_AsMVT(mvtgrid.*, 'grid') AS mvt
|
|
543
|
+
"""SELECT ST_AsMVT(mvtgrid.*, 'grid') AS mvt
|
|
507
544
|
FROM (
|
|
508
545
|
SELECT
|
|
509
546
|
{grid_fields}
|
|
@@ -634,6 +671,7 @@ def getUserTile(userId: UUID, z: int, x: int, y: int, format: str):
|
|
|
634
671
|
|
|
635
672
|
@bp.route("/users/me/map/style.json")
|
|
636
673
|
@auth.login_required_with_redirect()
|
|
674
|
+
@user_dependant_response(False)
|
|
637
675
|
def getMyStyle(account: Account):
|
|
638
676
|
"""Get vector tiles style.
|
|
639
677
|
|
geovisio/web/pages.py
CHANGED
|
@@ -180,11 +180,9 @@ def postPage(page, lang, account):
|
|
|
180
180
|
with db.execute(
|
|
181
181
|
current_app,
|
|
182
182
|
SQL(
|
|
183
|
-
"""
|
|
184
|
-
INSERT INTO pages (name, lang, content)
|
|
183
|
+
"""INSERT INTO pages (name, lang, content)
|
|
185
184
|
VALUES (%(name)s, %(lang)s, %(content)s)
|
|
186
|
-
ON CONFLICT (name, lang) DO UPDATE SET content=EXCLUDED.content
|
|
187
|
-
"""
|
|
185
|
+
ON CONFLICT (name, lang) DO UPDATE SET content=EXCLUDED.content"""
|
|
188
186
|
),
|
|
189
187
|
{"name": name.value, "lang": lang, "content": request.get_data(as_text=True)},
|
|
190
188
|
) as res:
|
|
@@ -238,3 +236,49 @@ def deletePage(page, lang, account):
|
|
|
238
236
|
raise InvalidAPIUsage(_("Could not delete page content"), 500)
|
|
239
237
|
|
|
240
238
|
return "", 200
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@bp.route("/pages/<page>/publish-change", methods=["POST", "PUT"])
|
|
242
|
+
@auth.login_required()
|
|
243
|
+
def postPageUpdate(page, account):
|
|
244
|
+
"""Act that there is a new version of a page on major changes.
|
|
245
|
+
For pages that needs acceptance, the users can thus be notified of the changes.
|
|
246
|
+
---
|
|
247
|
+
tags:
|
|
248
|
+
- Configuration
|
|
249
|
+
parameters:
|
|
250
|
+
- name: page
|
|
251
|
+
in: path
|
|
252
|
+
description: Page name
|
|
253
|
+
required: true
|
|
254
|
+
schema:
|
|
255
|
+
$ref: '#/components/schemas/GeoVisioPageName'
|
|
256
|
+
security:
|
|
257
|
+
- bearerToken: []
|
|
258
|
+
- cookieAuth: []
|
|
259
|
+
requestBody:
|
|
260
|
+
content:
|
|
261
|
+
application/json: {}
|
|
262
|
+
responses:
|
|
263
|
+
200:
|
|
264
|
+
description: Successfully saved
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
name = check_page_name(page)
|
|
268
|
+
|
|
269
|
+
if not account.can_edit_pages():
|
|
270
|
+
raise InvalidAPIUsage(_("You must be logged-in as admin to edit pages"), 403)
|
|
271
|
+
|
|
272
|
+
with db.execute(
|
|
273
|
+
current_app,
|
|
274
|
+
SQL(
|
|
275
|
+
"""UPDATE pages
|
|
276
|
+
SET updated_at = NOW()
|
|
277
|
+
WHERE name = %(name)s"""
|
|
278
|
+
),
|
|
279
|
+
{"name": name.value},
|
|
280
|
+
) as res:
|
|
281
|
+
if not res.rowcount:
|
|
282
|
+
raise InvalidAPIUsage(_("Could not publish page changes"), 500)
|
|
283
|
+
|
|
284
|
+
return "", 200
|
geovisio/web/params.py
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
from uuid import UUID
|
|
2
|
-
|
|
3
|
-
from geovisio import errors
|
|
4
2
|
import dateutil.parser
|
|
5
3
|
from dateutil import tz
|
|
6
4
|
from dateutil.parser import parse as dateparser
|
|
7
5
|
import datetime
|
|
6
|
+
from enum import Enum
|
|
8
7
|
import re
|
|
9
8
|
from werkzeug.datastructures import MultiDict
|
|
10
9
|
from typing import Optional, Tuple, Any, List
|
|
11
10
|
from pygeofilter import ast
|
|
12
11
|
from pygeofilter.backends.evaluator import Evaluator, handle
|
|
13
12
|
from psycopg import sql
|
|
13
|
+
from flask_babel import gettext as _
|
|
14
|
+
from flask import current_app
|
|
15
|
+
from geovisio import errors
|
|
14
16
|
from geovisio.utils.sequences import STAC_FIELD_MAPPINGS, STAC_FIELD_TO_SQL_FILTER
|
|
15
17
|
from geovisio.utils.fields import SortBy, SQLDirection, SortByField
|
|
16
|
-
from flask_babel import gettext as _
|
|
17
18
|
from geovisio.utils import items as utils_items
|
|
18
|
-
|
|
19
19
|
from geovisio.utils.cql2 import parse_cql2_filter
|
|
20
20
|
|
|
21
21
|
|
|
@@ -329,16 +329,20 @@ def parse_list(value: Optional[Any], tryFallbacks: bool = True, paramName: Optio
|
|
|
329
329
|
def parse_collection_filter(value: Optional[str]) -> Optional[sql.SQL]:
|
|
330
330
|
"""Reads STAC filter parameter and sends SQL condition back.
|
|
331
331
|
|
|
332
|
+
Note: if more search filters are added, don't forget to add them to the qeryables endpoint (in queryables.py)
|
|
333
|
+
|
|
332
334
|
>>> parse_collection_filter('')
|
|
333
335
|
|
|
334
336
|
>>> parse_collection_filter("updated >= '2023-12-31'")
|
|
335
337
|
SQL("(s.updated_at >= '2023-12-31')")
|
|
336
338
|
>>> parse_collection_filter("updated >= '2023-12-31' AND created < '2023-10-31'")
|
|
337
339
|
SQL("((s.updated_at >= '2023-12-31') AND (s.inserted_at < '2023-10-31'))")
|
|
338
|
-
>>> parse_collection_filter("status IN ('deleted','ready')") #
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
340
|
+
>>> parse_collection_filter("status IN ('deleted','ready')") # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
341
|
+
Traceback (most recent call last):
|
|
342
|
+
geovisio.errors.InvalidAPIUsage: The status filter is not supported anymore, use the `show_deleted` parameter instead if you need to query deleted collections
|
|
343
|
+
>>> parse_collection_filter("status = 'deleted' OR status = 'ready'") # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
344
|
+
Traceback (most recent call last):
|
|
345
|
+
geovisio.errors.InvalidAPIUsage: The status filter is not supported anymore, use the `show_deleted` parameter instead if you need to query deleted collections
|
|
342
346
|
>>> parse_collection_filter('invalid = 10') # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
343
347
|
Traceback (most recent call last):
|
|
344
348
|
geovisio.errors.InvalidAPIUsage: Unsupported filter parameter
|
|
@@ -346,6 +350,18 @@ def parse_collection_filter(value: Optional[str]) -> Optional[sql.SQL]:
|
|
|
346
350
|
Traceback (most recent call last):
|
|
347
351
|
geovisio.errors.InvalidAPIUsage: Unsupported filter parameter
|
|
348
352
|
"""
|
|
353
|
+
if value and "status" in value:
|
|
354
|
+
if "'hidden'" in value:
|
|
355
|
+
raise errors.InvalidAPIUsage(
|
|
356
|
+
_("The status filter is not supported anymore, use the `visibility` filter instead"),
|
|
357
|
+
status_code=400,
|
|
358
|
+
)
|
|
359
|
+
raise errors.InvalidAPIUsage(
|
|
360
|
+
_(
|
|
361
|
+
"The status filter is not supported anymore, use the `show_deleted` parameter instead if you need to query deleted collections"
|
|
362
|
+
),
|
|
363
|
+
status_code=400,
|
|
364
|
+
)
|
|
349
365
|
return parse_cql2_filter(value, STAC_FIELD_TO_SQL_FILTER, ast_updater=_alterFilterAst)
|
|
350
366
|
|
|
351
367
|
|
|
@@ -373,12 +389,16 @@ class _FilterAstUpdated(Evaluator):
|
|
|
373
389
|
The rational here is that for non-owned pictures/sequences, when a pictures/sequence is 'hidden' it should be advertised as 'deleted'.
|
|
374
390
|
|
|
375
391
|
This is especially important for crawler like the meta-catalog, since they should also delete the sequence/picture when it is hidden
|
|
392
|
+
|
|
393
|
+
We also need to maintain retrocompatibility, since the `hidden` status has been replaced by a `visibility` field.
|
|
376
394
|
"""
|
|
377
395
|
|
|
378
396
|
@handle(ast.Equal)
|
|
379
397
|
def eq(self, node, lhs, rhs):
|
|
380
398
|
if lhs == ast.Attribute("status") and rhs == "deleted":
|
|
381
|
-
return ast.Or(node, ast.
|
|
399
|
+
return ast.Or(node, ast.NotEqual(ast.Attribute("visibility"), "anyone")) # type: ignore
|
|
400
|
+
if lhs == ast.Attribute("status") and rhs == "hidden":
|
|
401
|
+
return ast.NotEqual(ast.Attribute("visibility"), "anyone") # type: ignore
|
|
382
402
|
return node
|
|
383
403
|
|
|
384
404
|
@handle(ast.Or)
|
|
@@ -387,8 +407,10 @@ class _FilterAstUpdated(Evaluator):
|
|
|
387
407
|
|
|
388
408
|
@handle(ast.In)
|
|
389
409
|
def in_(self, node, lhs, *options):
|
|
390
|
-
if "deleted" in node.sub_nodes:
|
|
391
|
-
node.sub_nodes
|
|
410
|
+
if "deleted" in node.sub_nodes or "hidden" in node.sub_nodes:
|
|
411
|
+
if "hidden" in node.sub_nodes:
|
|
412
|
+
node.sub_nodes.remove("hidden")
|
|
413
|
+
return ast.Or(node, ast.NotEqual(ast.Attribute("visibility"), "anyone"))
|
|
392
414
|
return node
|
|
393
415
|
|
|
394
416
|
def adopt(self, node, *sub_args):
|
|
@@ -419,6 +441,12 @@ def _parse_sorty_by(value: Optional[str], field_mapping_func, SortByCls):
|
|
|
419
441
|
return SortByCls(fields=orders)
|
|
420
442
|
|
|
421
443
|
|
|
444
|
+
def parse_boolean(value: Optional[str]) -> Optional[bool]:
|
|
445
|
+
if value is None:
|
|
446
|
+
return None
|
|
447
|
+
return value.lower() == "true"
|
|
448
|
+
|
|
449
|
+
|
|
422
450
|
def parse_collection_sortby(value: Optional[str]) -> Optional[SortBy]:
|
|
423
451
|
"""Reads STAC/OGC sortby parameter, and sends a SQL ORDER BY string.
|
|
424
452
|
|
|
@@ -526,3 +554,20 @@ def as_uuid(value: str, error: str) -> UUID:
|
|
|
526
554
|
return UUID(value)
|
|
527
555
|
except ValueError:
|
|
528
556
|
raise errors.InvalidAPIUsage(error)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
class Visibility(Enum):
|
|
560
|
+
"""Represent the visibility of picture"""
|
|
561
|
+
|
|
562
|
+
anyone = "anyone"
|
|
563
|
+
logged_only = "logged-only"
|
|
564
|
+
owner_only = "owner-only"
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def check_visibility(visibility: Visibility | str):
|
|
568
|
+
"""Check if the visibility is valid."""
|
|
569
|
+
|
|
570
|
+
if visibility == Visibility.logged_only or visibility == "logged-only":
|
|
571
|
+
if current_app.config["API_REGISTRATION_IS_OPEN"] is True:
|
|
572
|
+
return False
|
|
573
|
+
return True
|
geovisio/web/pictures.py
CHANGED
|
@@ -44,7 +44,7 @@ def getPictureHD(pictureId, format):
|
|
|
44
44
|
metadata = utils.pictures.checkPictureStatus(fses, pictureId)
|
|
45
45
|
|
|
46
46
|
external_url = utils.pictures.getPublicHDPictureExternalUrl(pictureId, format)
|
|
47
|
-
if external_url and metadata["status"] == "ready":
|
|
47
|
+
if external_url and metadata["status"] == "ready" and metadata["visibility"] == "anyone":
|
|
48
48
|
return redirect(external_url)
|
|
49
49
|
|
|
50
50
|
try:
|
|
@@ -89,7 +89,7 @@ def getPictureSD(pictureId, format):
|
|
|
89
89
|
metadata = utils.pictures.checkPictureStatus(fses, pictureId)
|
|
90
90
|
|
|
91
91
|
external_url = utils.pictures.getPublicDerivatePictureExternalUrl(pictureId, format, "sd.jpg")
|
|
92
|
-
if external_url and metadata["status"] == "ready":
|
|
92
|
+
if external_url and metadata["status"] == "ready" and metadata["visibility"] == "anyone":
|
|
93
93
|
return redirect(external_url)
|
|
94
94
|
|
|
95
95
|
try:
|
|
@@ -178,7 +178,7 @@ def getPictureTile(pictureId, col, row, format):
|
|
|
178
178
|
|
|
179
179
|
metadata = utils.pictures.checkPictureStatus(fses, pictureId)
|
|
180
180
|
external_url = utils.pictures.getPublicDerivatePictureExternalUrl(pictureId, format, f"tiles/{col}_{row}.jpg")
|
|
181
|
-
if external_url and metadata["status"] == "ready":
|
|
181
|
+
if external_url and metadata["status"] == "ready" and metadata["visibility"] == "anyone":
|
|
182
182
|
return redirect(external_url)
|
|
183
183
|
|
|
184
184
|
picPath = f"{utils.pictures.getPictureFolderPath(pictureId)}/tiles/{col}_{row}.jpg"
|
geovisio/web/prepare.py
CHANGED
|
@@ -71,10 +71,12 @@ def prepareItem(collectionId, itemId, account=None):
|
|
|
71
71
|
"""SELECT 1
|
|
72
72
|
FROM pictures p
|
|
73
73
|
JOIN sequences_pictures sp ON p.id = sp.pic_id
|
|
74
|
+
JOIN sequences s ON s.id = sp.seq_id
|
|
74
75
|
WHERE
|
|
75
76
|
p.id = %(pic)s
|
|
76
77
|
AND sp.seq_id = %(seq)s
|
|
77
|
-
AND (p
|
|
78
|
+
AND (is_picture_visible_by_user(p, %(acc)s))
|
|
79
|
+
AND (is_sequence_visible_by_user(s, %(acc)s))"""
|
|
78
80
|
),
|
|
79
81
|
{"pic": itemId, "seq": collectionId, "acc": accountId},
|
|
80
82
|
).fetchone()
|
|
@@ -138,7 +140,7 @@ def prepareCollection(collectionId, account=None):
|
|
|
138
140
|
FROM sequences
|
|
139
141
|
WHERE
|
|
140
142
|
id = %(seq)s
|
|
141
|
-
AND (
|
|
143
|
+
AND is_sequence_visible_by_user(sequences, %(acc)s)"""
|
|
142
144
|
),
|
|
143
145
|
{"seq": collectionId, "acc": accountId},
|
|
144
146
|
).fetchone()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import flask
|
|
2
|
+
|
|
3
|
+
bp = flask.Blueprint("queryables", __name__, url_prefix="/api")
|
|
4
|
+
|
|
5
|
+
ITEMS_QUERYABLES = {
|
|
6
|
+
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
|
7
|
+
"$id": "https://stac-api.example.com/queryables",
|
|
8
|
+
"type": "object",
|
|
9
|
+
"title": "Queryables for Panoramax STAC API",
|
|
10
|
+
"description": "Queryable names for Panoramax STAC API Item Search filter.",
|
|
11
|
+
"properties": {
|
|
12
|
+
"semantics": {
|
|
13
|
+
"description": "Tag to represent the presence of semantics. Only support the IS NOT NULL operator for the moment, to search for all items with at least one semantic tag.",
|
|
14
|
+
"type": "string",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
"patternProperties": {
|
|
18
|
+
"^semantics\\.(.+)$": {
|
|
19
|
+
"description": "Specific semantic tag. The semantic tag `key` should be after the prefix `semantics.`",
|
|
20
|
+
"type": "string",
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"additionalProperties": False,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
COLLECTION_QUERYABLES = {
|
|
27
|
+
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
|
28
|
+
"$id": "https://stac-api.example.com/queryables",
|
|
29
|
+
"type": "object",
|
|
30
|
+
"title": "Queryables for Panoramax STAC API",
|
|
31
|
+
"description": "Queryable names for Panoramax STAC API Item Search filter.",
|
|
32
|
+
"properties": {
|
|
33
|
+
"created": {
|
|
34
|
+
"description": "Created date of the collection. The filter can be either a date or a datetime",
|
|
35
|
+
"type": "string",
|
|
36
|
+
"anyOf": [{"format": "date-time"}, {"format": "date"}],
|
|
37
|
+
},
|
|
38
|
+
"updated": {
|
|
39
|
+
"description": "Update date of the collection. The filter can be either a date or a datetime",
|
|
40
|
+
"type": "string",
|
|
41
|
+
"anyOf": [{"format": "date-time"}, {"format": "date"}],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
"additionalProperties": False,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@bp.route("/queryables")
|
|
49
|
+
def search_queryables():
|
|
50
|
+
"""List of queryables for search as defined by https://github.com/stac-api-extensions/filter?tab=readme-ov-file#queryables"""
|
|
51
|
+
return flask.jsonify(ITEMS_QUERYABLES), {"Cache-Control": "public, max-age=3600"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@bp.route("/collections/queryables")
|
|
55
|
+
def collection_queryables():
|
|
56
|
+
"""List of queryables for collection-search as defined by https://github.com/stac-api-extensions/filter?tab=readme-ov-file#queryables"""
|
|
57
|
+
return flask.jsonify(COLLECTION_QUERYABLES), {"Cache-Control": "public, max-age=3600"}
|
geovisio/web/stac.py
CHANGED
|
@@ -144,6 +144,12 @@ def getLanding():
|
|
|
144
144
|
"href": mapUrl,
|
|
145
145
|
"title": "Pictures and sequences vector tiles",
|
|
146
146
|
},
|
|
147
|
+
{
|
|
148
|
+
"title": "Queryables",
|
|
149
|
+
"href": url_for("queryables.search_queryables", _external=True),
|
|
150
|
+
"rel": "http://www.opengis.net/def/rel/ogc/1.0/queryables",
|
|
151
|
+
"type": "application/schema+json",
|
|
152
|
+
},
|
|
147
153
|
{
|
|
148
154
|
"rel": "xyz-style",
|
|
149
155
|
"type": "application/json",
|
|
@@ -348,7 +354,6 @@ def getUserCatalog(userId, userIdMatchesAccount=False):
|
|
|
348
354
|
collection_request.pagination_filter = parse_collection_filter(request.args.get("page"))
|
|
349
355
|
|
|
350
356
|
userName = None
|
|
351
|
-
meta_collection = None
|
|
352
357
|
with db.cursor(current_app, row_factory=dict_row) as cursor:
|
|
353
358
|
userName = cursor.execute("SELECT name FROM accounts WHERE id = %s", [userId]).fetchone()
|
|
354
359
|
|
|
@@ -359,8 +364,9 @@ def getUserCatalog(userId, userIdMatchesAccount=False):
|
|
|
359
364
|
datasetBounds = get_dataset_bounds(
|
|
360
365
|
cursor.connection,
|
|
361
366
|
collection_request.sort_by,
|
|
362
|
-
additional_filters=SQL("s.account_id = %(account)s"),
|
|
367
|
+
additional_filters=SQL("s.account_id = %(account)s AND is_sequence_visible_by_user(s, %(account_to_query)s)"),
|
|
363
368
|
additional_filters_params={"account": userId},
|
|
369
|
+
account_to_query_id=auth.get_current_account_id(),
|
|
364
370
|
)
|
|
365
371
|
|
|
366
372
|
if datasetBounds is None:
|