geovisio 2.6.0__py3-none-any.whl → 2.7.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 +36 -7
- geovisio/admin_cli/db.py +1 -4
- geovisio/config_app.py +40 -1
- geovisio/db_migrations.py +24 -3
- geovisio/templates/main.html +13 -13
- geovisio/templates/viewer.html +3 -3
- geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/de/LC_MESSAGES/messages.po +667 -0
- geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/en/LC_MESSAGES/messages.po +730 -0
- geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/es/LC_MESSAGES/messages.po +778 -0
- geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fi/LC_MESSAGES/messages.po +589 -0
- geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.po +814 -0
- geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ko/LC_MESSAGES/messages.po +685 -0
- geovisio/translations/messages.pot +686 -0
- geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/nl/LC_MESSAGES/messages.po +594 -0
- geovisio/utils/__init__.py +1 -1
- geovisio/utils/auth.py +50 -11
- geovisio/utils/db.py +65 -0
- geovisio/utils/excluded_areas.py +83 -0
- geovisio/utils/extent.py +30 -0
- geovisio/utils/fields.py +1 -1
- geovisio/utils/filesystems.py +0 -1
- geovisio/utils/link.py +14 -0
- geovisio/utils/params.py +20 -0
- geovisio/utils/pictures.py +92 -68
- geovisio/utils/reports.py +171 -0
- geovisio/utils/sequences.py +264 -126
- geovisio/utils/tokens.py +37 -42
- geovisio/utils/upload_set.py +654 -0
- geovisio/web/auth.py +37 -37
- geovisio/web/collections.py +286 -302
- geovisio/web/configuration.py +14 -0
- geovisio/web/docs.py +241 -14
- geovisio/web/excluded_areas.py +377 -0
- geovisio/web/items.py +156 -108
- geovisio/web/map.py +20 -20
- geovisio/web/params.py +69 -26
- geovisio/web/pictures.py +14 -31
- geovisio/web/reports.py +399 -0
- geovisio/web/rss.py +13 -7
- geovisio/web/stac.py +129 -134
- geovisio/web/tokens.py +98 -109
- geovisio/web/upload_set.py +768 -0
- geovisio/web/users.py +100 -73
- geovisio/web/utils.py +28 -9
- geovisio/workers/runner_pictures.py +252 -204
- {geovisio-2.6.0.dist-info → geovisio-2.7.0.dist-info}/METADATA +16 -13
- geovisio-2.7.0.dist-info/RECORD +66 -0
- geovisio-2.6.0.dist-info/RECORD +0 -41
- {geovisio-2.6.0.dist-info → geovisio-2.7.0.dist-info}/LICENSE +0 -0
- {geovisio-2.6.0.dist-info → geovisio-2.7.0.dist-info}/WHEEL +0 -0
geovisio/web/items.py
CHANGED
|
@@ -7,7 +7,7 @@ from psycopg.types.json import Jsonb
|
|
|
7
7
|
from werkzeug.datastructures import MultiDict
|
|
8
8
|
from uuid import UUID
|
|
9
9
|
from geovisio import errors, utils
|
|
10
|
-
from geovisio.utils import auth
|
|
10
|
+
from geovisio.utils import auth, db
|
|
11
11
|
from geovisio.utils.pictures import cleanupExif
|
|
12
12
|
from geovisio.web.params import (
|
|
13
13
|
as_latitude,
|
|
@@ -21,8 +21,7 @@ from geovisio.web.params import (
|
|
|
21
21
|
parse_distance_range,
|
|
22
22
|
)
|
|
23
23
|
from geovisio.utils.fields import Bounds
|
|
24
|
-
|
|
25
|
-
import psycopg
|
|
24
|
+
import hashlib
|
|
26
25
|
from psycopg.rows import dict_row
|
|
27
26
|
from psycopg.sql import SQL
|
|
28
27
|
from geovisio.web.utils import (
|
|
@@ -36,9 +35,10 @@ from geovisio.web.utils import (
|
|
|
36
35
|
STAC_VERSION,
|
|
37
36
|
)
|
|
38
37
|
from flask import current_app, request, url_for, Blueprint
|
|
38
|
+
from flask_babel import gettext as _, get_locale
|
|
39
39
|
from geopic_tag_reader.writer import writePictureMetadata, PictureMetadata
|
|
40
|
+
import sentry_sdk
|
|
40
41
|
|
|
41
|
-
from geovisio.workers import runner_pictures
|
|
42
42
|
|
|
43
43
|
bp = Blueprint("stac_items", __name__, url_prefix="/api")
|
|
44
44
|
|
|
@@ -59,6 +59,25 @@ def dbPictureToStacItem(seqId, dbPic):
|
|
|
59
59
|
The equivalent in STAC Item format
|
|
60
60
|
"""
|
|
61
61
|
|
|
62
|
+
sensorDim = None
|
|
63
|
+
visibleArea = None
|
|
64
|
+
if dbPic["metadata"].get("crop") is not None:
|
|
65
|
+
sensorDim = [dbPic["metadata"]["crop"].get("fullWidth"), dbPic["metadata"]["crop"].get("fullHeight")]
|
|
66
|
+
visibleArea = [
|
|
67
|
+
dbPic["metadata"]["crop"].get("left"),
|
|
68
|
+
dbPic["metadata"]["crop"].get("top"),
|
|
69
|
+
int(dbPic["metadata"]["crop"].get("fullWidth", "0"))
|
|
70
|
+
- int(dbPic["metadata"]["crop"].get("width", "0"))
|
|
71
|
+
- int(dbPic["metadata"]["crop"].get("left", "0")),
|
|
72
|
+
int(dbPic["metadata"]["crop"].get("fullHeight", "0"))
|
|
73
|
+
- int(dbPic["metadata"]["crop"].get("height", "0"))
|
|
74
|
+
- int(dbPic["metadata"]["crop"].get("top", "0")),
|
|
75
|
+
]
|
|
76
|
+
if None in sensorDim:
|
|
77
|
+
sensorDim = None
|
|
78
|
+
if None in visibleArea or visibleArea == [0, 0, 0, 0]:
|
|
79
|
+
visibleArea = None
|
|
80
|
+
|
|
62
81
|
item = removeNoneInDict(
|
|
63
82
|
{
|
|
64
83
|
"type": "Feature",
|
|
@@ -72,7 +91,7 @@ def dbPictureToStacItem(seqId, dbPic):
|
|
|
72
91
|
"bbox": dbPic["geojson"]["coordinates"] + dbPic["geojson"]["coordinates"],
|
|
73
92
|
"providers": cleanNoneInList(
|
|
74
93
|
[
|
|
75
|
-
{"name": dbPic["account_name"], "roles": ["producer"]},
|
|
94
|
+
{"name": dbPic["account_name"], "roles": ["producer"], "id": str(dbPic["account_id"])},
|
|
76
95
|
(
|
|
77
96
|
{"name": dbPic["exif"]["Exif.Image.Artist"], "roles": ["producer"]}
|
|
78
97
|
if dbPic["exif"].get("Exif.Image.Artist") is not None
|
|
@@ -95,10 +114,12 @@ def dbPictureToStacItem(seqId, dbPic):
|
|
|
95
114
|
"camera_model": dbPic["metadata"].get("model"),
|
|
96
115
|
"focal_length": dbPic["metadata"].get("focal_length"),
|
|
97
116
|
"field_of_view": dbPic["metadata"].get("field_of_view"),
|
|
117
|
+
"sensor_array_dimensions": sensorDim,
|
|
118
|
+
"visible_area": visibleArea,
|
|
98
119
|
}
|
|
99
120
|
)
|
|
100
121
|
if "metadata" in dbPic
|
|
101
|
-
and any(True for f in dbPic["metadata"] if f in ["make", "model", "focal_length", "field_of_view"])
|
|
122
|
+
and any(True for f in dbPic["metadata"] if f in ["make", "model", "focal_length", "field_of_view", "crop"])
|
|
102
123
|
else {}
|
|
103
124
|
),
|
|
104
125
|
"pers:pitch": dbPic["metadata"].get("pitch"),
|
|
@@ -330,31 +351,31 @@ def getCollectionItems(collectionId):
|
|
|
330
351
|
try:
|
|
331
352
|
limit = int(limit)
|
|
332
353
|
if limit < 1 or limit > 10000:
|
|
333
|
-
raise errors.InvalidAPIUsage("limit parameter should be an integer between 1 and 10000", status_code=400)
|
|
354
|
+
raise errors.InvalidAPIUsage(_("limit parameter should be an integer between 1 and 10000"), status_code=400)
|
|
334
355
|
except ValueError:
|
|
335
|
-
raise errors.InvalidAPIUsage("limit parameter should be a valid, positive integer (between 1 and 10000)", status_code=400)
|
|
356
|
+
raise errors.InvalidAPIUsage(_("limit parameter should be a valid, positive integer (between 1 and 10000)"), status_code=400)
|
|
336
357
|
sql_limit = SQL("LIMIT %(limit)s")
|
|
337
358
|
params["limit"] = limit
|
|
338
359
|
|
|
339
360
|
if withPicture and startAfterRank:
|
|
340
|
-
raise errors.InvalidAPIUsage(
|
|
361
|
+
raise errors.InvalidAPIUsage(_("`startAfterRank` and `withPicture` are mutually exclusive parameters"))
|
|
341
362
|
|
|
342
363
|
# Check if rank is valid
|
|
343
364
|
if startAfterRank is not None:
|
|
344
365
|
try:
|
|
345
366
|
startAfterRank = int(startAfterRank)
|
|
346
367
|
if startAfterRank < 1:
|
|
347
|
-
raise errors.InvalidAPIUsage("startAfterRank parameter should be a positive integer (starting from 1)", status_code=400)
|
|
368
|
+
raise errors.InvalidAPIUsage(_("startAfterRank parameter should be a positive integer (starting from 1)"), status_code=400)
|
|
348
369
|
except ValueError:
|
|
349
|
-
raise errors.InvalidAPIUsage("startAfterRank parameter should be a valid, positive integer", status_code=400)
|
|
370
|
+
raise errors.InvalidAPIUsage(_("startAfterRank parameter should be a valid, positive integer"), status_code=400)
|
|
350
371
|
|
|
351
372
|
filters.append(SQL("rank > %(start_after_rank)s"))
|
|
352
373
|
params["start_after_rank"] = startAfterRank
|
|
353
374
|
|
|
354
375
|
paginated = startAfterRank is not None or limit is not None or withPicture is not None
|
|
355
376
|
|
|
356
|
-
with
|
|
357
|
-
with conn.cursor() as cursor:
|
|
377
|
+
with current_app.pool.connection() as conn:
|
|
378
|
+
with conn.cursor(row_factory=dict_row) as cursor:
|
|
358
379
|
# check on sequence
|
|
359
380
|
seqMeta = cursor.execute(
|
|
360
381
|
"SELECT s.id "
|
|
@@ -367,12 +388,14 @@ def getCollectionItems(collectionId):
|
|
|
367
388
|
).fetchone()
|
|
368
389
|
|
|
369
390
|
if seqMeta is None:
|
|
370
|
-
raise errors.InvalidAPIUsage("Collection doesn't exist", status_code=404)
|
|
391
|
+
raise errors.InvalidAPIUsage(_("Collection doesn't exist"), status_code=404)
|
|
371
392
|
|
|
372
393
|
maxRank = seqMeta.get("max_rank")
|
|
373
394
|
|
|
374
395
|
if startAfterRank is not None and startAfterRank >= maxRank:
|
|
375
|
-
raise errors.InvalidAPIUsage(
|
|
396
|
+
raise errors.InvalidAPIUsage(
|
|
397
|
+
_("No more items in this collection (last available rank is %(r)s)", r=maxRank), status_code=404
|
|
398
|
+
)
|
|
376
399
|
|
|
377
400
|
if withPicture is not None:
|
|
378
401
|
withPicture = as_uuid(withPicture, "withPicture should be a valid UUID")
|
|
@@ -381,7 +404,7 @@ def getCollectionItems(collectionId):
|
|
|
381
404
|
params={"id": withPicture, "seq": collectionId},
|
|
382
405
|
).fetchone()
|
|
383
406
|
if not pic:
|
|
384
|
-
raise errors.InvalidAPIUsage(
|
|
407
|
+
raise errors.InvalidAPIUsage(_("Picture with id %(p)s does not exists", p=withPicture))
|
|
385
408
|
rank = get_first_rank_of_page(pic["rank"], limit)
|
|
386
409
|
|
|
387
410
|
filters.append(SQL("rank >= %(start_after_rank)s"))
|
|
@@ -393,6 +416,7 @@ def getCollectionItems(collectionId):
|
|
|
393
416
|
p.id, p.ts, p.heading, p.metadata, p.inserted_at, p.status,
|
|
394
417
|
ST_AsGeoJSON(p.geom)::json AS geojson,
|
|
395
418
|
a.name AS account_name,
|
|
419
|
+
p.account_id AS account_id,
|
|
396
420
|
sp.rank, p.exif,
|
|
397
421
|
CASE WHEN LAG(p.status) OVER othpics = 'ready' THEN LAG(p.id) OVER othpics END AS prevpic,
|
|
398
422
|
CASE WHEN LAG(p.status) OVER othpics = 'ready' THEN ST_AsGeoJSON(LAG(p.geom) OVER othpics)::json END AS prevpicgeojson,
|
|
@@ -545,8 +569,8 @@ def _getPictureItemById(collectionId, itemId):
|
|
|
545
569
|
schema:
|
|
546
570
|
type: string
|
|
547
571
|
"""
|
|
548
|
-
with
|
|
549
|
-
with conn.cursor() as cursor:
|
|
572
|
+
with current_app.pool.connection() as conn:
|
|
573
|
+
with conn.cursor(row_factory=dict_row) as cursor:
|
|
550
574
|
# Check if there is a logged user
|
|
551
575
|
account = auth.get_current_account()
|
|
552
576
|
accountId = account.id if account else None
|
|
@@ -557,6 +581,7 @@ def _getPictureItemById(collectionId, itemId):
|
|
|
557
581
|
SELECT
|
|
558
582
|
p.id, sp.rank, ST_AsGeoJSON(p.geom)::json AS geojson, p.heading, p.ts, p.metadata,
|
|
559
583
|
p.inserted_at, p.status, accounts.name AS account_name,
|
|
584
|
+
p.account_id AS account_id,
|
|
560
585
|
spl.prevpic, spl.prevpicgeojson, spl.nextpic, spl.nextpicgeojson, p.exif,
|
|
561
586
|
relp.related_pics
|
|
562
587
|
FROM pictures p
|
|
@@ -665,7 +690,7 @@ def getCollectionItem(collectionId, itemId):
|
|
|
665
690
|
|
|
666
691
|
stacItem = _getPictureItemById(collectionId, itemId)
|
|
667
692
|
if stacItem is None:
|
|
668
|
-
raise errors.InvalidAPIUsage("Item doesn't exist", status_code=404)
|
|
693
|
+
raise errors.InvalidAPIUsage(_("Item doesn't exist"), status_code=404)
|
|
669
694
|
|
|
670
695
|
account = auth.get_current_account()
|
|
671
696
|
picStatusToHttpCode = {
|
|
@@ -724,7 +749,7 @@ def searchItems():
|
|
|
724
749
|
args: MultiDict[str, str]
|
|
725
750
|
if request.method == "POST":
|
|
726
751
|
if request.headers.get("Content-Type") != "application/json":
|
|
727
|
-
raise errors.InvalidAPIUsage("Search using POST method should have a JSON body", status_code=400)
|
|
752
|
+
raise errors.InvalidAPIUsage(_("Search using POST method should have a JSON body"), status_code=400)
|
|
728
753
|
args = MultiDict(request.json)
|
|
729
754
|
else:
|
|
730
755
|
args = request.args
|
|
@@ -733,7 +758,7 @@ def searchItems():
|
|
|
733
758
|
if args.get("limit") is not None:
|
|
734
759
|
limit = args.get("limit", type=int)
|
|
735
760
|
if limit is None or limit < 1 or limit > 10000:
|
|
736
|
-
raise errors.InvalidAPIUsage("Parameter limit must be either empty or a number between 1 and 10000", status_code=400)
|
|
761
|
+
raise errors.InvalidAPIUsage(_("Parameter limit must be either empty or a number between 1 and 10000"), status_code=400)
|
|
737
762
|
else:
|
|
738
763
|
sqlParams["limit"] = limit
|
|
739
764
|
else:
|
|
@@ -789,7 +814,7 @@ def searchItems():
|
|
|
789
814
|
place_fov_tolerance = args.get("place_fov_tolerance", type=int, default=30)
|
|
790
815
|
if place_fov_tolerance < 2 or place_fov_tolerance > 180:
|
|
791
816
|
raise errors.InvalidAPIUsage(
|
|
792
|
-
"Parameter place_fov_tolerance must be either empty or a number between 2 and 180", status_code=400
|
|
817
|
+
_("Parameter place_fov_tolerance must be either empty or a number between 2 and 180"), status_code=400
|
|
793
818
|
)
|
|
794
819
|
else:
|
|
795
820
|
sqlParams["placefov"] = place_fov_tolerance / 2
|
|
@@ -811,7 +836,7 @@ def searchItems():
|
|
|
811
836
|
try:
|
|
812
837
|
intersects = json.loads(args["intersects"])
|
|
813
838
|
except:
|
|
814
|
-
raise errors.InvalidAPIUsage("Parameter intersects should contain a valid GeoJSON Geometry (not a Feature)", status_code=400)
|
|
839
|
+
raise errors.InvalidAPIUsage(_("Parameter intersects should contain a valid GeoJSON Geometry (not a Feature)"), status_code=400)
|
|
815
840
|
if intersects["type"] == "Point":
|
|
816
841
|
sqlWhere.append(SQL("p.geom && ST_Expand(ST_GeomFromGeoJSON(%(geom)s), 0.000001)"))
|
|
817
842
|
else:
|
|
@@ -827,7 +852,7 @@ def searchItems():
|
|
|
827
852
|
try:
|
|
828
853
|
sqlParams["ids"] = [UUID(j) for j in parse_list(args.get("ids"), paramName="ids")]
|
|
829
854
|
except:
|
|
830
|
-
raise errors.InvalidAPIUsage("Parameter ids should be a JSON array of strings", status_code=400)
|
|
855
|
+
raise errors.InvalidAPIUsage(_("Parameter ids should be a JSON array of strings"), status_code=400)
|
|
831
856
|
|
|
832
857
|
# Collections
|
|
833
858
|
if args.get("collections") is not None:
|
|
@@ -839,7 +864,7 @@ def searchItems():
|
|
|
839
864
|
try:
|
|
840
865
|
sqlParams["collections"] = [UUID(j) for j in parse_list(args["collections"], paramName="collections")]
|
|
841
866
|
except:
|
|
842
|
-
raise errors.InvalidAPIUsage("Parameter collections should be a JSON array of strings", status_code=400)
|
|
867
|
+
raise errors.InvalidAPIUsage(_("Parameter collections should be a JSON array of strings"), status_code=400)
|
|
843
868
|
|
|
844
869
|
# To speed up search, if it's a search by id and on only one id, we use the same code as /collections/:cid/items/:id
|
|
845
870
|
if args.get("ids") is not None and args:
|
|
@@ -847,10 +872,10 @@ def searchItems():
|
|
|
847
872
|
if ids and len(ids) == 1:
|
|
848
873
|
picture_id = ids[0]
|
|
849
874
|
|
|
850
|
-
with
|
|
875
|
+
with current_app.pool.connection() as conn, conn.cursor() as cursor:
|
|
851
876
|
seq = cursor.execute("SELECT seq_id FROM sequences_pictures WHERE pic_id = %s", [picture_id]).fetchone()
|
|
852
877
|
if not seq:
|
|
853
|
-
raise errors.InvalidAPIUsage("Picture doesn't exist", status_code=404)
|
|
878
|
+
raise errors.InvalidAPIUsage(_("Picture doesn't exist"), status_code=404)
|
|
854
879
|
|
|
855
880
|
item = _getPictureItemById(seq[0], UUID(picture_id))
|
|
856
881
|
features = [item] if item else []
|
|
@@ -863,16 +888,17 @@ def searchItems():
|
|
|
863
888
|
#
|
|
864
889
|
# Database query
|
|
865
890
|
#
|
|
866
|
-
with
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
"""
|
|
891
|
+
with db.cursor(current_app, timeout=30000, row_factory=dict_row) as cursor:
|
|
892
|
+
query = SQL(
|
|
893
|
+
"""
|
|
870
894
|
SELECT * FROM (
|
|
871
895
|
SELECT
|
|
872
896
|
p.id, p.ts, p.heading, p.metadata, p.inserted_at,
|
|
873
897
|
ST_AsGeoJSON(p.geom)::json AS geojson,
|
|
874
898
|
sp.seq_id, sp.rank AS rank,
|
|
875
|
-
accounts.name AS account_name,
|
|
899
|
+
accounts.name AS account_name,
|
|
900
|
+
p.account_id AS account_id,
|
|
901
|
+
p.exif
|
|
876
902
|
FROM pictures p
|
|
877
903
|
LEFT JOIN sequences_pictures sp ON p.id = sp.pic_id
|
|
878
904
|
LEFT JOIN sequences s ON s.id = sp.seq_id
|
|
@@ -900,24 +926,24 @@ LEFT JOIN LATERAL (
|
|
|
900
926
|
LIMIT 1
|
|
901
927
|
) next on true
|
|
902
928
|
;
|
|
903
|
-
|
|
904
|
-
|
|
929
|
+
"""
|
|
930
|
+
).format(sqlWhere=SQL(" AND ").join(sqlWhere), sqlSubQueryWhere=SQL(" AND ").join(sqlSubQueryWhere), orderBy=order_by)
|
|
905
931
|
|
|
906
|
-
|
|
932
|
+
records = cursor.execute(query, sqlParams)
|
|
907
933
|
|
|
908
|
-
|
|
934
|
+
items = [dbPictureToStacItem(str(dbPic["seq_id"]), dbPic) for dbPic in records]
|
|
909
935
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
936
|
+
return (
|
|
937
|
+
{
|
|
938
|
+
"type": "FeatureCollection",
|
|
939
|
+
"features": items,
|
|
940
|
+
"links": [
|
|
941
|
+
get_root_link(),
|
|
942
|
+
],
|
|
943
|
+
},
|
|
944
|
+
200,
|
|
945
|
+
{"Content-Type": "application/geo+json"},
|
|
946
|
+
)
|
|
921
947
|
|
|
922
948
|
|
|
923
949
|
@bp.route("/collections/<uuid:collectionId>/items", methods=["POST"])
|
|
@@ -952,18 +978,18 @@ def postCollectionItem(collectionId, account=None):
|
|
|
952
978
|
"""
|
|
953
979
|
|
|
954
980
|
if not request.headers.get("Content-Type", "").startswith("multipart/form-data"):
|
|
955
|
-
raise errors.InvalidAPIUsage("Content type should be multipart/form-data", status_code=415)
|
|
981
|
+
raise errors.InvalidAPIUsage(_("Content type should be multipart/form-data"), status_code=415)
|
|
956
982
|
|
|
957
983
|
# Check if position was given
|
|
958
984
|
if request.form.get("position") is None:
|
|
959
|
-
raise errors.InvalidAPIUsage('Missing "position" parameter', status_code=400)
|
|
985
|
+
raise errors.InvalidAPIUsage(_('Missing "position" parameter'), status_code=400)
|
|
960
986
|
else:
|
|
961
987
|
try:
|
|
962
988
|
position = int(request.form["position"])
|
|
963
989
|
if position <= 0:
|
|
964
990
|
raise ValueError()
|
|
965
991
|
except ValueError:
|
|
966
|
-
raise errors.InvalidAPIUsage("Position in sequence should be a positive integer", status_code=400)
|
|
992
|
+
raise errors.InvalidAPIUsage(_("Position in sequence should be a positive integer"), status_code=400)
|
|
967
993
|
|
|
968
994
|
# Check if datetime was given
|
|
969
995
|
ext_mtd = PictureMetadata()
|
|
@@ -977,11 +1003,11 @@ def postCollectionItem(collectionId, account=None):
|
|
|
977
1003
|
lon, lat = request.form.get("override_longitude"), request.form.get("override_latitude")
|
|
978
1004
|
if lon is not None or lat is not None:
|
|
979
1005
|
if lat is None:
|
|
980
|
-
raise errors.InvalidAPIUsage("Longitude cannot be overridden alone, override_latitude also needs to be set")
|
|
1006
|
+
raise errors.InvalidAPIUsage(_("Longitude cannot be overridden alone, override_latitude also needs to be set"))
|
|
981
1007
|
if lon is None:
|
|
982
|
-
raise errors.InvalidAPIUsage("Latitude cannot be overridden alone, override_longitude also needs to be set")
|
|
983
|
-
lon = as_longitude(lon, error=
|
|
984
|
-
lat = as_latitude(lat, error=
|
|
1008
|
+
raise errors.InvalidAPIUsage(_("Latitude cannot be overridden alone, override_longitude also needs to be set"))
|
|
1009
|
+
lon = as_longitude(lon, error=_("For parameter `override_longitude`, `%(v)s` is not a valid longitude", v=lon))
|
|
1010
|
+
lat = as_latitude(lat, error=_("For parameter `override_latitude`, `%(v)s` is not a valid latitude", v=lat))
|
|
985
1011
|
ext_mtd.longitude = lon
|
|
986
1012
|
ext_mtd.latitude = lat
|
|
987
1013
|
|
|
@@ -1000,70 +1026,88 @@ def postCollectionItem(collectionId, account=None):
|
|
|
1000
1026
|
if request.form.get("isBlurred") is None or request.form.get("isBlurred") in ["true", "false"]:
|
|
1001
1027
|
isBlurred = request.form.get("isBlurred") == "true"
|
|
1002
1028
|
else:
|
|
1003
|
-
raise errors.InvalidAPIUsage("Picture blur status should be either unset, true or false", status_code=400)
|
|
1029
|
+
raise errors.InvalidAPIUsage(_("Picture blur status should be either unset, true or false"), status_code=400)
|
|
1004
1030
|
|
|
1005
1031
|
# Check if a picture file was given
|
|
1006
1032
|
if "picture" not in request.files:
|
|
1007
|
-
raise errors.InvalidAPIUsage("No picture file was sent", status_code=400)
|
|
1033
|
+
raise errors.InvalidAPIUsage(_("No picture file was sent"), status_code=400)
|
|
1008
1034
|
else:
|
|
1009
1035
|
picture = request.files["picture"]
|
|
1010
1036
|
|
|
1011
1037
|
# Check file validity
|
|
1012
1038
|
if not (picture.filename != "" and "." in picture.filename and picture.filename.rsplit(".", 1)[1].lower() in ["jpg", "jpeg"]):
|
|
1013
|
-
raise errors.InvalidAPIUsage("Picture file is either missing or in an unsupported format (should be jpg)", status_code=400)
|
|
1039
|
+
raise errors.InvalidAPIUsage(_("Picture file is either missing or in an unsupported format (should be jpg)"), status_code=400)
|
|
1014
1040
|
|
|
1015
|
-
with
|
|
1016
|
-
with conn.cursor() as cursor:
|
|
1041
|
+
with db.conn(current_app) as conn:
|
|
1042
|
+
with conn.transaction(), conn.cursor() as cursor:
|
|
1017
1043
|
# Check if sequence exists
|
|
1018
|
-
seq = cursor.execute("SELECT
|
|
1019
|
-
if not seq
|
|
1020
|
-
raise errors.InvalidAPIUsage(
|
|
1044
|
+
seq = cursor.execute("SELECT account_id, status FROM sequences WHERE id = %s", [collectionId]).fetchone()
|
|
1045
|
+
if not seq:
|
|
1046
|
+
raise errors.InvalidAPIUsage(_("Collection %(s)s wasn't found in database", s=collectionId), status_code=404)
|
|
1047
|
+
|
|
1048
|
+
# Account associated to picture doesn't match current user
|
|
1049
|
+
if account is not None and account.id != str(seq[0]):
|
|
1050
|
+
raise errors.InvalidAPIUsage(_("You're not authorized to add picture to this collection"), status_code=403)
|
|
1051
|
+
|
|
1052
|
+
# Check if sequence has not been deleted
|
|
1053
|
+
status = seq[1]
|
|
1054
|
+
if status == "deleted":
|
|
1055
|
+
raise errors.InvalidAPIUsage(_("The collection has been deleted, impossible to add pictures to it"), status_code=404)
|
|
1021
1056
|
|
|
1022
1057
|
# Compute various metadata
|
|
1023
1058
|
accountId = accountIdOrDefault(account)
|
|
1024
1059
|
raw_pic = picture.read()
|
|
1025
1060
|
filesize = len(raw_pic)
|
|
1026
1061
|
|
|
1062
|
+
with sentry_sdk.start_span(description="computing md5"):
|
|
1063
|
+
# we save the content hash md5 as uuid since md5 is 128bit and uuid are efficiently handled in postgres
|
|
1064
|
+
md5 = hashlib.md5(raw_pic).digest()
|
|
1065
|
+
md5 = UUID(bytes=md5)
|
|
1066
|
+
|
|
1027
1067
|
additionalMetadata = {
|
|
1028
1068
|
"blurredByAuthor": isBlurred,
|
|
1029
1069
|
"originalFileName": os.path.basename(picture.filename),
|
|
1030
1070
|
"originalFileSize": filesize,
|
|
1071
|
+
"originalContentMd5": md5,
|
|
1031
1072
|
}
|
|
1032
1073
|
|
|
1033
1074
|
# Update picture metadata if needed
|
|
1034
|
-
|
|
1075
|
+
with sentry_sdk.start_span(description="overwriting metadata"):
|
|
1076
|
+
updated_picture = writePictureMetadata(raw_pic, ext_mtd)
|
|
1035
1077
|
|
|
1036
1078
|
# Insert picture into database
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1079
|
+
with sentry_sdk.start_span(description="Insert picture in db"):
|
|
1080
|
+
try:
|
|
1081
|
+
picId = utils.pictures.insertNewPictureInDatabase(
|
|
1082
|
+
conn, collectionId, position, updated_picture, accountId, additionalMetadata, lang=get_locale().language
|
|
1083
|
+
)
|
|
1084
|
+
except utils.pictures.PicturePositionConflict:
|
|
1085
|
+
raise errors.InvalidAPIUsage(_("Picture at given position already exist"), status_code=409)
|
|
1086
|
+
except utils.pictures.MetadataReadingError as e:
|
|
1087
|
+
raise errors.InvalidAPIUsage(_("Impossible to parse picture metadata"), payload={"details": {"error": e.details}})
|
|
1088
|
+
except utils.pictures.InvalidMetadataValue as e:
|
|
1089
|
+
raise errors.InvalidAPIUsage(_("Picture has invalid metadata"), payload={"details": {"error": e.details}})
|
|
1045
1090
|
|
|
1046
1091
|
# Save file into appropriate filesystem
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
)
|
|
1092
|
+
with sentry_sdk.start_span(description="Saving picture"):
|
|
1093
|
+
try:
|
|
1094
|
+
utils.pictures.saveRawPicture(picId, updated_picture, isBlurred)
|
|
1095
|
+
except:
|
|
1096
|
+
logging.exception("Picture wasn't correctly saved in filesystem")
|
|
1097
|
+
raise errors.InvalidAPIUsage(_("Picture wasn't correctly saved in filesystem"), status_code=500)
|
|
1098
|
+
|
|
1099
|
+
current_app.background_processor.process_pictures()
|
|
1100
|
+
|
|
1101
|
+
# Return picture metadata
|
|
1102
|
+
return (
|
|
1103
|
+
getCollectionItem(collectionId, picId)[0],
|
|
1104
|
+
202,
|
|
1105
|
+
{
|
|
1106
|
+
"Content-Type": "application/json",
|
|
1107
|
+
"Access-Control-Expose-Headers": "Location", # Needed for allowing web browsers access Location header
|
|
1108
|
+
"Location": url_for("stac_items.getCollectionItem", _external=True, collectionId=collectionId, itemId=picId),
|
|
1109
|
+
},
|
|
1110
|
+
)
|
|
1067
1111
|
|
|
1068
1112
|
|
|
1069
1113
|
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>", methods=["PATCH"])
|
|
@@ -1121,7 +1165,7 @@ def patchCollectionItem(collectionId, itemId, account):
|
|
|
1121
1165
|
visible = metadata.get("visible")
|
|
1122
1166
|
if visible is not None:
|
|
1123
1167
|
if visible not in ["true", "false"]:
|
|
1124
|
-
raise errors.InvalidAPIUsage("Picture visibility parameter (visible) should be either unset, true or false", status_code=400)
|
|
1168
|
+
raise errors.InvalidAPIUsage(_("Picture visibility parameter (visible) should be either unset, true or false"), status_code=400)
|
|
1125
1169
|
visible = visible == "true"
|
|
1126
1170
|
|
|
1127
1171
|
# Check if heading is valid
|
|
@@ -1133,7 +1177,9 @@ def patchCollectionItem(collectionId, itemId, account):
|
|
|
1133
1177
|
raise ValueError()
|
|
1134
1178
|
except ValueError:
|
|
1135
1179
|
raise errors.InvalidAPIUsage(
|
|
1136
|
-
|
|
1180
|
+
_(
|
|
1181
|
+
"Heading is not valid, should be an integer in degrees from 0° to 360°. North is 0°, East = 90°, South = 180° and West = 270°."
|
|
1182
|
+
),
|
|
1137
1183
|
status_code=400,
|
|
1138
1184
|
)
|
|
1139
1185
|
|
|
@@ -1142,17 +1188,17 @@ def patchCollectionItem(collectionId, itemId, account):
|
|
|
1142
1188
|
return getCollectionItem(collectionId, itemId)
|
|
1143
1189
|
|
|
1144
1190
|
# Check if picture exists and if given account is authorized to edit
|
|
1145
|
-
with
|
|
1146
|
-
with conn.cursor() as cursor:
|
|
1191
|
+
with db.conn(current_app) as conn:
|
|
1192
|
+
with conn.transaction(), conn.cursor(row_factory=dict_row) as cursor:
|
|
1147
1193
|
pic = cursor.execute("SELECT status, account_id FROM pictures WHERE id = %s", [itemId]).fetchone()
|
|
1148
1194
|
|
|
1149
1195
|
# Picture not found
|
|
1150
1196
|
if not pic:
|
|
1151
|
-
raise errors.InvalidAPIUsage(
|
|
1197
|
+
raise errors.InvalidAPIUsage(_("Picture %(p)s wasn't found in database", p=itemId), status_code=404)
|
|
1152
1198
|
|
|
1153
1199
|
# Account associated to picture doesn't match current user
|
|
1154
1200
|
if account is not None and account.id != str(pic["account_id"]):
|
|
1155
|
-
raise errors.InvalidAPIUsage("You're not authorized to edit this picture", status_code=403)
|
|
1201
|
+
raise errors.InvalidAPIUsage(_("You're not authorized to edit this picture"), status_code=403)
|
|
1156
1202
|
|
|
1157
1203
|
sqlUpdates = []
|
|
1158
1204
|
sqlParams = {"id": itemId, "account": account.id}
|
|
@@ -1162,7 +1208,12 @@ def patchCollectionItem(collectionId, itemId, account):
|
|
|
1162
1208
|
if oldStatus not in ["ready", "hidden"]:
|
|
1163
1209
|
# Picture is in a preparing/broken/... state so no edit possible
|
|
1164
1210
|
raise errors.InvalidAPIUsage(
|
|
1165
|
-
|
|
1211
|
+
_(
|
|
1212
|
+
"Picture %(p)s is in %(s)s state, its visibility can't be changed for now",
|
|
1213
|
+
p=itemId,
|
|
1214
|
+
s=oldStatus,
|
|
1215
|
+
),
|
|
1216
|
+
status_code=400,
|
|
1166
1217
|
)
|
|
1167
1218
|
|
|
1168
1219
|
newStatus = None
|
|
@@ -1194,10 +1245,9 @@ def patchCollectionItem(collectionId, itemId, account):
|
|
|
1194
1245
|
).format(updates=SQL(", ").join(sqlUpdates)),
|
|
1195
1246
|
sqlParams,
|
|
1196
1247
|
)
|
|
1197
|
-
conn.commit()
|
|
1198
1248
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1249
|
+
# Redirect response to a classic GET
|
|
1250
|
+
return getCollectionItem(collectionId, itemId)
|
|
1201
1251
|
|
|
1202
1252
|
|
|
1203
1253
|
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>", methods=["DELETE"])
|
|
@@ -1229,26 +1279,24 @@ def deleteCollectionItem(collectionId, itemId, account):
|
|
|
1229
1279
|
"""
|
|
1230
1280
|
|
|
1231
1281
|
# Check if picture exists and if given account is authorized to edit
|
|
1232
|
-
with
|
|
1233
|
-
with conn.cursor() as cursor:
|
|
1282
|
+
with db.conn(current_app) as conn:
|
|
1283
|
+
with conn.transaction(), conn.cursor() as cursor:
|
|
1234
1284
|
pic = cursor.execute("SELECT status, account_id FROM pictures WHERE id = %s", [itemId]).fetchone()
|
|
1235
1285
|
|
|
1236
1286
|
# Picture not found
|
|
1237
1287
|
if not pic:
|
|
1238
|
-
raise errors.InvalidAPIUsage(
|
|
1288
|
+
raise errors.InvalidAPIUsage(_("Picture %(p)s wasn't found in database", p=itemId), status_code=404)
|
|
1239
1289
|
|
|
1240
1290
|
# Account associated to picture doesn't match current user
|
|
1241
1291
|
if account is not None and account.id != str(pic[1]):
|
|
1242
|
-
raise errors.InvalidAPIUsage("You're not authorized to edit this picture", status_code=403)
|
|
1292
|
+
raise errors.InvalidAPIUsage(_("You're not authorized to edit this picture"), status_code=403)
|
|
1243
1293
|
|
|
1244
1294
|
cursor.execute("DELETE FROM pictures WHERE id = %s", [itemId])
|
|
1245
1295
|
|
|
1246
1296
|
# delete images
|
|
1247
1297
|
utils.pictures.removeAllFiles(itemId)
|
|
1248
1298
|
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
return "", 204
|
|
1299
|
+
return "", 204
|
|
1252
1300
|
|
|
1253
1301
|
|
|
1254
1302
|
def _getHDJpgPictureURL(picId: str, status: Optional[str]):
|