geovisio 2.9.0__py3-none-any.whl → 2.10.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 +6 -1
- geovisio/config_app.py +5 -5
- geovisio/translations/ar/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ar/LC_MESSAGES/messages.po +818 -0
- geovisio/translations/br/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/da/LC_MESSAGES/messages.po +4 -3
- geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/de/LC_MESSAGES/messages.po +55 -2
- geovisio/translations/el/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/en/LC_MESSAGES/messages.po +193 -139
- geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/eo/LC_MESSAGES/messages.po +53 -4
- geovisio/translations/es/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/fi/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.po +91 -3
- geovisio/translations/hu/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/it/LC_MESSAGES/messages.po +63 -3
- geovisio/translations/ja/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/ko/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/messages.pot +185 -129
- geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/nl/LC_MESSAGES/messages.po +292 -63
- geovisio/translations/oc/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/oc/LC_MESSAGES/messages.po +818 -0
- geovisio/translations/pl/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/sv/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/sv/LC_MESSAGES/messages.po +4 -3
- geovisio/translations/ti/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ti/LC_MESSAGES/messages.po +762 -0
- geovisio/translations/zh_Hant/LC_MESSAGES/messages.po +1 -1
- geovisio/utils/annotations.py +14 -17
- geovisio/utils/auth.py +14 -13
- geovisio/utils/cql2.py +2 -2
- geovisio/utils/fields.py +14 -2
- geovisio/utils/items.py +44 -0
- geovisio/utils/model_query.py +2 -2
- geovisio/utils/pic_shape.py +1 -1
- geovisio/utils/pictures.py +111 -18
- geovisio/utils/semantics.py +32 -3
- geovisio/utils/sentry.py +1 -1
- geovisio/utils/sequences.py +51 -34
- geovisio/utils/upload_set.py +285 -198
- geovisio/utils/website.py +1 -1
- geovisio/web/annotations.py +209 -68
- geovisio/web/auth.py +1 -1
- geovisio/web/collections.py +26 -22
- geovisio/web/configuration.py +24 -4
- geovisio/web/docs.py +93 -11
- geovisio/web/items.py +197 -121
- geovisio/web/params.py +44 -31
- geovisio/web/pictures.py +34 -0
- geovisio/web/tokens.py +49 -1
- geovisio/web/upload_set.py +150 -32
- geovisio/web/users.py +4 -4
- geovisio/web/utils.py +2 -2
- geovisio/workers/runner_pictures.py +128 -23
- {geovisio-2.9.0.dist-info → geovisio-2.10.0.dist-info}/METADATA +13 -13
- geovisio-2.10.0.dist-info/RECORD +105 -0
- geovisio-2.9.0.dist-info/RECORD +0 -98
- {geovisio-2.9.0.dist-info → geovisio-2.10.0.dist-info}/WHEEL +0 -0
- {geovisio-2.9.0.dist-info → geovisio-2.10.0.dist-info}/licenses/LICENSE +0 -0
geovisio/web/items.py
CHANGED
|
@@ -15,7 +15,9 @@ from geovisio.utils.cql2 import parse_search_filter
|
|
|
15
15
|
from geovisio.utils.params import validation_error
|
|
16
16
|
from geovisio.utils.pictures import cleanupExif
|
|
17
17
|
from geovisio.utils.semantics import Entity, EntityType, update_tags
|
|
18
|
+
from geovisio.utils.items import SortableItemField, SortBy, ItemSortByField
|
|
18
19
|
from geovisio.utils.tags import SemanticTagUpdate
|
|
20
|
+
from geovisio.utils.auth import Account
|
|
19
21
|
from geovisio.web.params import (
|
|
20
22
|
as_latitude,
|
|
21
23
|
as_longitude,
|
|
@@ -23,12 +25,13 @@ from geovisio.web.params import (
|
|
|
23
25
|
parse_datetime,
|
|
24
26
|
parse_datetime_interval,
|
|
25
27
|
parse_bbox,
|
|
28
|
+
parse_item_sortby,
|
|
26
29
|
parse_list,
|
|
27
30
|
parse_lonlat,
|
|
28
31
|
parse_distance_range,
|
|
29
32
|
parse_picture_heading,
|
|
30
33
|
)
|
|
31
|
-
from geovisio.utils.fields import Bounds
|
|
34
|
+
from geovisio.utils.fields import Bounds, SQLDirection
|
|
32
35
|
import hashlib
|
|
33
36
|
from psycopg.rows import dict_row
|
|
34
37
|
from psycopg.sql import SQL
|
|
@@ -52,7 +55,7 @@ import math
|
|
|
52
55
|
bp = Blueprint("stac_items", __name__, url_prefix="/api")
|
|
53
56
|
|
|
54
57
|
|
|
55
|
-
def dbPictureToStacItem(
|
|
58
|
+
def dbPictureToStacItem(dbPic):
|
|
56
59
|
"""Transforms a picture extracted from database into a STAC Item
|
|
57
60
|
|
|
58
61
|
Parameters
|
|
@@ -70,6 +73,7 @@ def dbPictureToStacItem(seqId, dbPic):
|
|
|
70
73
|
|
|
71
74
|
sensorDim = None
|
|
72
75
|
visibleArea = None
|
|
76
|
+
seqId = str(dbPic["seq_id"])
|
|
73
77
|
if dbPic["metadata"].get("crop") is not None:
|
|
74
78
|
sensorDim = [dbPic["metadata"]["crop"].get("fullWidth"), dbPic["metadata"]["crop"].get("fullHeight")]
|
|
75
79
|
visibleArea = [
|
|
@@ -115,7 +119,7 @@ def dbPictureToStacItem(seqId, dbPic):
|
|
|
115
119
|
"datetime": dbTsToStac(dbPic["ts"]),
|
|
116
120
|
"datetimetz": dbTsToStacTZ(dbPic["ts"], dbPic["metadata"].get("tz")),
|
|
117
121
|
"created": dbTsToStac(dbPic["inserted_at"]),
|
|
118
|
-
|
|
122
|
+
"updated": dbTsToStac(dbPic["updated_at"]),
|
|
119
123
|
"license": current_app.config["API_PICTURES_LICENSE_SPDX_ID"],
|
|
120
124
|
"view:azimuth": dbPic["heading"],
|
|
121
125
|
"pers:interior_orientation": (
|
|
@@ -151,6 +155,7 @@ def dbPictureToStacItem(seqId, dbPic):
|
|
|
151
155
|
"quality:horizontal_accuracy": float("{:.1f}".format(dbPic["gps_accuracy_m"])) if dbPic.get("gps_accuracy_m") else None,
|
|
152
156
|
"semantics": [s for s in dbPic.get("semantics") or [] if s],
|
|
153
157
|
"annotations": [a for a in dbPic.get("annotations") or [] if a],
|
|
158
|
+
"collection": {"semantics": dbPic["sequence_semantics"]} if "sequence_semantics" in dbPic else None,
|
|
154
159
|
}
|
|
155
160
|
),
|
|
156
161
|
"links": cleanNoneInList(
|
|
@@ -424,7 +429,7 @@ def getCollectionItems(collectionId):
|
|
|
424
429
|
params={"id": withPicture, "seq": collectionId},
|
|
425
430
|
).fetchone()
|
|
426
431
|
if not pic:
|
|
427
|
-
raise errors.InvalidAPIUsage(_("Picture with id %(p)s does not
|
|
432
|
+
raise errors.InvalidAPIUsage(_("Picture with id %(p)s does not exist", p=withPicture))
|
|
428
433
|
rank = get_first_rank_of_page(pic["rank"], limit)
|
|
429
434
|
|
|
430
435
|
filters.append(SQL("rank >= %(start_after_rank)s"))
|
|
@@ -433,11 +438,11 @@ def getCollectionItems(collectionId):
|
|
|
433
438
|
query = SQL(
|
|
434
439
|
"""
|
|
435
440
|
SELECT
|
|
436
|
-
p.id, p.ts, p.heading, p.metadata, p.inserted_at, p.status,
|
|
441
|
+
p.id, p.ts, p.heading, p.metadata, p.inserted_at, p.updated_at, p.status,
|
|
437
442
|
ST_AsGeoJSON(p.geom)::json AS geojson,
|
|
438
443
|
a.name AS account_name,
|
|
439
444
|
p.account_id AS account_id,
|
|
440
|
-
sp.rank, p.exif, p.gps_accuracy_m, p.h_pixel_density,
|
|
445
|
+
sp.seq_id, sp.rank, p.exif, p.gps_accuracy_m, p.h_pixel_density,
|
|
441
446
|
CASE WHEN LAG(p.status) OVER othpics = 'ready' THEN LAG(p.id) OVER othpics END AS prevpic,
|
|
442
447
|
CASE WHEN LAG(p.status) OVER othpics = 'ready' THEN ST_AsGeoJSON(LAG(p.geom) OVER othpics)::json END AS prevpicgeojson,
|
|
443
448
|
CASE WHEN LEAD(p.status) OVER othpics = 'ready' THEN LEAD(p.id) OVER othpics END AS nextpic,
|
|
@@ -464,7 +469,7 @@ def getCollectionItems(collectionId):
|
|
|
464
469
|
if first_rank is None:
|
|
465
470
|
first_rank = dbPic["rank"]
|
|
466
471
|
last_rank = dbPic["rank"]
|
|
467
|
-
items.append(dbPictureToStacItem(
|
|
472
|
+
items.append(dbPictureToStacItem(dbPic))
|
|
468
473
|
bounds = Bounds(first=first_rank, last=last_rank) if records else None
|
|
469
474
|
|
|
470
475
|
links = [
|
|
@@ -570,26 +575,8 @@ def getCollectionItems(collectionId):
|
|
|
570
575
|
)
|
|
571
576
|
|
|
572
577
|
|
|
573
|
-
def _getPictureItemById(
|
|
574
|
-
"""Get a picture metadata by its ID
|
|
575
|
-
|
|
576
|
-
---
|
|
577
|
-
tags:
|
|
578
|
-
- Pictures
|
|
579
|
-
parameters:
|
|
580
|
-
- name: collectionId
|
|
581
|
-
in: path
|
|
582
|
-
description: ID of collection to retrieve
|
|
583
|
-
required: true
|
|
584
|
-
schema:
|
|
585
|
-
type: string
|
|
586
|
-
- name: itemId
|
|
587
|
-
in: path
|
|
588
|
-
description: ID of item to retrieve
|
|
589
|
-
required: true
|
|
590
|
-
schema:
|
|
591
|
-
type: string
|
|
592
|
-
"""
|
|
578
|
+
def _getPictureItemById(itemId: UUID, account: Optional[Account]):
|
|
579
|
+
"""Get a picture metadata by its ID"""
|
|
593
580
|
with current_app.pool.connection() as conn:
|
|
594
581
|
with conn.cursor(row_factory=dict_row) as cursor:
|
|
595
582
|
# Check if there is a logged user
|
|
@@ -598,16 +585,18 @@ def _getPictureItemById(collectionId, itemId):
|
|
|
598
585
|
|
|
599
586
|
# Get rank + position of wanted picture
|
|
600
587
|
record = cursor.execute(
|
|
601
|
-
"""
|
|
588
|
+
"""WITH seq AS (
|
|
589
|
+
SELECT seq_id FROM sequences_pictures WHERE pic_id = %(pic)s LIMIT 1
|
|
590
|
+
)
|
|
602
591
|
SELECT
|
|
603
|
-
p.id, sp.rank, ST_AsGeoJSON(p.geom)::json AS geojson, p.heading, p.ts, p.metadata,
|
|
604
|
-
p.inserted_at, p.status, accounts.name AS account_name,
|
|
592
|
+
p.id, sp.seq_id, sp.rank, ST_AsGeoJSON(p.geom)::json AS geojson, p.heading, p.ts, p.metadata,
|
|
593
|
+
p.inserted_at, p.updated_at, p.status, accounts.name AS account_name,
|
|
605
594
|
p.account_id AS account_id,
|
|
606
595
|
spl.prevpic, spl.prevpicgeojson, spl.nextpic, spl.nextpicgeojson, p.exif,
|
|
607
596
|
relp.related_pics, p.gps_accuracy_m, p.h_pixel_density,
|
|
608
|
-
|
|
609
597
|
get_picture_semantics(p.id) as semantics,
|
|
610
|
-
get_picture_annotations(p.id) as annotations
|
|
598
|
+
get_picture_annotations(p.id) as annotations,
|
|
599
|
+
COALESCE(seq_sem.semantics, '[]'::json) AS sequence_semantics
|
|
611
600
|
FROM pictures p
|
|
612
601
|
JOIN sequences_pictures sp ON sp.pic_id = p.id
|
|
613
602
|
JOIN accounts ON p.account_id = accounts.id
|
|
@@ -622,7 +611,7 @@ def _getPictureItemById(collectionId, itemId):
|
|
|
622
611
|
FROM pictures p
|
|
623
612
|
JOIN sequences_pictures sp ON p.id = sp.pic_id
|
|
624
613
|
WHERE
|
|
625
|
-
sp.seq_id
|
|
614
|
+
sp.seq_id IN (SELECT seq_id FROM seq)
|
|
626
615
|
AND (p.account_id = %(acc)s OR p.status != 'hidden')
|
|
627
616
|
WINDOW othpics AS (PARTITION BY sp.seq_id ORDER BY sp.rank)
|
|
628
617
|
) spl ON p.id = spl.id
|
|
@@ -650,7 +639,7 @@ def _getPictureItemById(collectionId, itemId):
|
|
|
650
639
|
AND relp.status != 'waiting-for-delete'
|
|
651
640
|
AND relp.id != p.id
|
|
652
641
|
AND relsp.pic_id = relp.id
|
|
653
|
-
AND relsp.seq_id
|
|
642
|
+
AND relsp.seq_id NOT IN (SELECT seq_id FROM seq)
|
|
654
643
|
AND (
|
|
655
644
|
p.metadata->>'type' = 'equirectangular'
|
|
656
645
|
OR (relp.heading IS NULL OR p.heading IS NULL)
|
|
@@ -663,19 +652,27 @@ def _getPictureItemById(collectionId, itemId):
|
|
|
663
652
|
ORDER BY relsp.seq_id, p.geom <-> relp.geom
|
|
664
653
|
) a
|
|
665
654
|
) relp ON TRUE
|
|
666
|
-
|
|
655
|
+
LEFT JOIN (
|
|
656
|
+
SELECT sequence_id, json_agg(json_strip_nulls(json_build_object(
|
|
657
|
+
'key', key,
|
|
658
|
+
'value', value
|
|
659
|
+
)) ORDER BY key, value) AS semantics
|
|
660
|
+
FROM sequences_semantics
|
|
661
|
+
GROUP BY sequence_id
|
|
662
|
+
) seq_sem ON seq_sem.sequence_id = s.id
|
|
663
|
+
WHERE sp.seq_id IN (SELECT seq_id FROM seq)
|
|
667
664
|
AND p.id = %(pic)s
|
|
668
665
|
AND (p.account_id = %(acc)s OR p.status != 'hidden')
|
|
669
666
|
AND (s.status != 'hidden' OR s.account_id = %(acc)s)
|
|
670
667
|
AND s.status != 'deleted'
|
|
671
668
|
""",
|
|
672
|
-
{"
|
|
669
|
+
{"pic": itemId, "acc": accountId},
|
|
673
670
|
).fetchone()
|
|
674
671
|
|
|
675
672
|
if record is None:
|
|
676
673
|
return None
|
|
677
674
|
|
|
678
|
-
return dbPictureToStacItem(
|
|
675
|
+
return dbPictureToStacItem(record)
|
|
679
676
|
|
|
680
677
|
|
|
681
678
|
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>")
|
|
@@ -711,12 +708,12 @@ def getCollectionItem(collectionId, itemId):
|
|
|
711
708
|
schema:
|
|
712
709
|
$ref: '#/components/schemas/GeoVisioItem'
|
|
713
710
|
"""
|
|
711
|
+
account = auth.get_current_account()
|
|
714
712
|
|
|
715
|
-
stacItem = _getPictureItemById(
|
|
713
|
+
stacItem = _getPictureItemById(itemId, account)
|
|
716
714
|
if stacItem is None:
|
|
717
715
|
raise errors.InvalidAPIUsage(_("Item doesn't exist"), status_code=404)
|
|
718
716
|
|
|
719
|
-
account = auth.get_current_account()
|
|
720
717
|
picStatusToHttpCode = {
|
|
721
718
|
"waiting-for-process": 102,
|
|
722
719
|
"ready": 200,
|
|
@@ -726,18 +723,6 @@ def getCollectionItem(collectionId, itemId):
|
|
|
726
723
|
return stacItem, picStatusToHttpCode[stacItem["properties"]["geovisio:status"]], {"Content-Type": "application/geo+json"}
|
|
727
724
|
|
|
728
725
|
|
|
729
|
-
class SearchParams(BaseModel):
|
|
730
|
-
bbox: Optional[str] = None
|
|
731
|
-
limit: int = 10
|
|
732
|
-
datetime: Optional[str] = None
|
|
733
|
-
place_position: Optional[str] = None
|
|
734
|
-
place_distance: Optional[str] = None
|
|
735
|
-
place_fov_tolerance: Optional[int] = None
|
|
736
|
-
intersects: Optional[str] = None
|
|
737
|
-
ids: Optional[str] = None
|
|
738
|
-
collections: Optional[str] = None
|
|
739
|
-
|
|
740
|
-
|
|
741
726
|
@bp.route("/search", methods=["GET", "POST"])
|
|
742
727
|
def searchItems():
|
|
743
728
|
"""Search through all available items
|
|
@@ -759,6 +744,7 @@ def searchItems():
|
|
|
759
744
|
- $ref: '#/components/parameters/GeoVisio_place_distance'
|
|
760
745
|
- $ref: '#/components/parameters/GeoVisio_place_fov_tolerance'
|
|
761
746
|
- $ref: '#/components/parameters/searchCQL2_filter'
|
|
747
|
+
- $ref: '#/components/parameters/GeoVisioSearchSortedBy'
|
|
762
748
|
post:
|
|
763
749
|
requestBody:
|
|
764
750
|
required: true
|
|
@@ -768,7 +754,11 @@ def searchItems():
|
|
|
768
754
|
$ref: '#/components/schemas/GeoVisioItemSearchBody'
|
|
769
755
|
responses:
|
|
770
756
|
200:
|
|
771
|
-
|
|
757
|
+
description: The search results
|
|
758
|
+
content:
|
|
759
|
+
application/geo+json:
|
|
760
|
+
schema:
|
|
761
|
+
$ref: '#/components/schemas/GeoVisioCollectionItems'
|
|
772
762
|
"""
|
|
773
763
|
|
|
774
764
|
account = auth.get_current_account()
|
|
@@ -776,7 +766,6 @@ def searchItems():
|
|
|
776
766
|
sqlWhere = [SQL("(p.status = 'ready' OR p.account_id = %(account)s)"), SQL("(is_sequence_visible_by_user(s, %(account)s))")]
|
|
777
767
|
sqlParams: Dict[str, Any] = {"account": accountId}
|
|
778
768
|
sqlSubQueryWhere = [SQL("(p.status = 'ready' OR p.account_id = %(account)s)")]
|
|
779
|
-
order_by = SQL("")
|
|
780
769
|
|
|
781
770
|
#
|
|
782
771
|
# Parameters parsing and verification
|
|
@@ -801,6 +790,8 @@ def searchItems():
|
|
|
801
790
|
raise errors.InvalidAPIUsage(_("Parameter limit must be either empty or a number between 1 and 10000"), status_code=400)
|
|
802
791
|
sqlParams["limit"] = limit
|
|
803
792
|
|
|
793
|
+
sort_by = parse_item_sortby(args.get("sortby"))
|
|
794
|
+
|
|
804
795
|
# Bounding box
|
|
805
796
|
bboxarg = parse_bbox(args.getlist("bbox"))
|
|
806
797
|
if bboxarg is not None:
|
|
@@ -810,7 +801,16 @@ def searchItems():
|
|
|
810
801
|
sqlParams["maxx"] = bboxarg[2]
|
|
811
802
|
sqlParams["maxy"] = bboxarg[3]
|
|
812
803
|
# if we search by bbox, we'll give first the items near the center of the bounding box
|
|
813
|
-
|
|
804
|
+
if not sort_by:
|
|
805
|
+
sort_by = SortBy(
|
|
806
|
+
fields=[
|
|
807
|
+
ItemSortByField(
|
|
808
|
+
field=SortableItemField.distance_to,
|
|
809
|
+
direction=SQLDirection.ASC,
|
|
810
|
+
obj_to_compare=SQL("ST_Centroid(ST_MakeEnvelope(%(minx)s, %(miny)s, %(maxx)s, %(maxy)s, 4326))"),
|
|
811
|
+
),
|
|
812
|
+
]
|
|
813
|
+
)
|
|
814
814
|
|
|
815
815
|
# Datetime
|
|
816
816
|
min_dt, max_dt = parse_datetime_interval(args.get("datetime"))
|
|
@@ -866,7 +866,16 @@ def searchItems():
|
|
|
866
866
|
)
|
|
867
867
|
|
|
868
868
|
# Sort pictures by nearest to POI
|
|
869
|
-
|
|
869
|
+
if not sort_by:
|
|
870
|
+
sort_by = SortBy(
|
|
871
|
+
fields=[
|
|
872
|
+
ItemSortByField(
|
|
873
|
+
field=SortableItemField.distance_to,
|
|
874
|
+
direction=SQLDirection.ASC,
|
|
875
|
+
obj_to_compare=SQL("ST_Point(%(placex)s, %(placey)s, 4326)"),
|
|
876
|
+
),
|
|
877
|
+
]
|
|
878
|
+
)
|
|
870
879
|
|
|
871
880
|
# Intersects
|
|
872
881
|
if args.get("intersects") is not None:
|
|
@@ -881,7 +890,16 @@ def searchItems():
|
|
|
881
890
|
sqlWhere.append(SQL("ST_Intersects(p.geom, ST_GeomFromGeoJSON(%(geom)s))"))
|
|
882
891
|
sqlParams["geom"] = Jsonb(intersects)
|
|
883
892
|
# if we search by bbox, we'll give first the items near the center of the bounding box
|
|
884
|
-
|
|
893
|
+
if not sort_by:
|
|
894
|
+
sort_by = SortBy(
|
|
895
|
+
fields=[
|
|
896
|
+
ItemSortByField(
|
|
897
|
+
field=SortableItemField.distance_to,
|
|
898
|
+
direction=SQLDirection.ASC,
|
|
899
|
+
obj_to_compare=SQL("ST_Centroid(ST_GeomFromGeoJSON(%(geom)s))"),
|
|
900
|
+
),
|
|
901
|
+
]
|
|
902
|
+
)
|
|
885
903
|
|
|
886
904
|
# Ids
|
|
887
905
|
if args.get("ids") is not None:
|
|
@@ -910,11 +928,7 @@ def searchItems():
|
|
|
910
928
|
picture_id = ids[0]
|
|
911
929
|
|
|
912
930
|
with current_app.pool.connection() as conn, conn.cursor() as cursor:
|
|
913
|
-
|
|
914
|
-
if not seq:
|
|
915
|
-
raise errors.InvalidAPIUsage(_("Picture doesn't exist"), status_code=404)
|
|
916
|
-
|
|
917
|
-
item = _getPictureItemById(seq[0], UUID(picture_id))
|
|
931
|
+
item = _getPictureItemById(UUID(picture_id), account)
|
|
918
932
|
features = [item] if item else []
|
|
919
933
|
return (
|
|
920
934
|
{"type": "FeatureCollection", "features": features, "links": [get_root_link()]},
|
|
@@ -926,6 +940,18 @@ def searchItems():
|
|
|
926
940
|
cql_filter = parse_search_filter(filter_param)
|
|
927
941
|
if cql_filter is not None:
|
|
928
942
|
sqlWhere.append(cql_filter)
|
|
943
|
+
|
|
944
|
+
if not sort_by:
|
|
945
|
+
# by default we sort by last updated (and id in case of equalities)
|
|
946
|
+
sort_by = SortBy(
|
|
947
|
+
fields=[
|
|
948
|
+
ItemSortByField(field=SortableItemField.updated, direction=SQLDirection.DESC),
|
|
949
|
+
ItemSortByField(field=SortableItemField.id, direction=SQLDirection.ASC),
|
|
950
|
+
]
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
order_by = sort_by.to_sql()
|
|
954
|
+
|
|
929
955
|
#
|
|
930
956
|
# Database query
|
|
931
957
|
#
|
|
@@ -934,18 +960,27 @@ def searchItems():
|
|
|
934
960
|
"""
|
|
935
961
|
SELECT * FROM (
|
|
936
962
|
SELECT
|
|
937
|
-
p.id, p.ts, p.heading, p.metadata, p.inserted_at,
|
|
963
|
+
p.id, p.ts, p.heading, p.metadata, p.inserted_at, p.updated_at,
|
|
938
964
|
ST_AsGeoJSON(p.geom)::json AS geojson,
|
|
939
965
|
sp.seq_id, sp.rank AS rank,
|
|
940
966
|
accounts.name AS account_name,
|
|
941
967
|
p.account_id AS account_id,
|
|
942
968
|
p.exif, p.gps_accuracy_m, p.h_pixel_density,
|
|
943
969
|
get_picture_semantics(p.id) as semantics,
|
|
944
|
-
get_picture_annotations(p.id) as annotations
|
|
970
|
+
get_picture_annotations(p.id) as annotations,
|
|
971
|
+
COALESCE(seq_sem.semantics, '[]'::json) AS sequence_semantics
|
|
945
972
|
FROM pictures p
|
|
946
973
|
LEFT JOIN sequences_pictures sp ON p.id = sp.pic_id
|
|
947
974
|
LEFT JOIN sequences s ON s.id = sp.seq_id
|
|
948
975
|
LEFT JOIN accounts ON p.account_id = accounts.id
|
|
976
|
+
LEFT JOIN (
|
|
977
|
+
SELECT sequence_id, json_agg(json_strip_nulls(json_build_object(
|
|
978
|
+
'key', key,
|
|
979
|
+
'value', value
|
|
980
|
+
)) ORDER BY key, value) AS semantics
|
|
981
|
+
FROM sequences_semantics
|
|
982
|
+
GROUP BY sequence_id
|
|
983
|
+
) seq_sem ON seq_sem.sequence_id = s.id
|
|
949
984
|
WHERE {sqlWhere}
|
|
950
985
|
{orderBy}
|
|
951
986
|
LIMIT %(limit)s
|
|
@@ -968,13 +1003,14 @@ LEFT JOIN LATERAL (
|
|
|
968
1003
|
ORDER BY sp.rank ASC
|
|
969
1004
|
LIMIT 1
|
|
970
1005
|
) next on true
|
|
1006
|
+
|
|
971
1007
|
;
|
|
972
1008
|
"""
|
|
973
1009
|
).format(sqlWhere=SQL(" AND ").join(sqlWhere), sqlSubQueryWhere=SQL(" AND ").join(sqlSubQueryWhere), orderBy=order_by)
|
|
974
1010
|
|
|
975
1011
|
records = cursor.execute(query, sqlParams)
|
|
976
1012
|
|
|
977
|
-
items = [dbPictureToStacItem(
|
|
1013
|
+
items = [dbPictureToStacItem(dbPic) for dbPic in records]
|
|
978
1014
|
|
|
979
1015
|
return (
|
|
980
1016
|
{
|
|
@@ -992,7 +1028,9 @@ LEFT JOIN LATERAL (
|
|
|
992
1028
|
@bp.route("/collections/<uuid:collectionId>/items", methods=["POST"])
|
|
993
1029
|
@auth.login_required_by_setting("API_FORCE_AUTH_ON_UPLOAD")
|
|
994
1030
|
def postCollectionItem(collectionId, account=None):
|
|
995
|
-
"""Add a new picture in a given sequence
|
|
1031
|
+
"""Add a new picture in a given sequence.
|
|
1032
|
+
|
|
1033
|
+
Note that this is the legacy API, upload should be done using the [UploadSet](#UploadSet) endpoints if possible.
|
|
996
1034
|
---
|
|
997
1035
|
tags:
|
|
998
1036
|
- Upload
|
|
@@ -1018,6 +1056,42 @@ def postCollectionItem(collectionId, account=None):
|
|
|
1018
1056
|
application/geo+json:
|
|
1019
1057
|
schema:
|
|
1020
1058
|
$ref: '#/components/schemas/GeoVisioItem'
|
|
1059
|
+
400:
|
|
1060
|
+
description: Error if the request is malformed
|
|
1061
|
+
content:
|
|
1062
|
+
application/json:
|
|
1063
|
+
schema:
|
|
1064
|
+
$ref: '#/components/schemas/GeoVisioError'
|
|
1065
|
+
401:
|
|
1066
|
+
description: Error if you're not logged in
|
|
1067
|
+
content:
|
|
1068
|
+
application/json:
|
|
1069
|
+
schema:
|
|
1070
|
+
$ref: '#/components/schemas/GeoVisioError'
|
|
1071
|
+
403:
|
|
1072
|
+
description: Error if you're not authorized to add picture to this collection
|
|
1073
|
+
content:
|
|
1074
|
+
application/json:
|
|
1075
|
+
schema:
|
|
1076
|
+
$ref: '#/components/schemas/GeoVisioError'
|
|
1077
|
+
404:
|
|
1078
|
+
description: Error if the collection doesn't exist
|
|
1079
|
+
content:
|
|
1080
|
+
application/json:
|
|
1081
|
+
schema:
|
|
1082
|
+
$ref: '#/components/schemas/GeoVisioError'
|
|
1083
|
+
409:
|
|
1084
|
+
description: Error if a picture (named `item` in the API) has already been added in the same index (named `position` in the API) in this collection
|
|
1085
|
+
content:
|
|
1086
|
+
application/json:
|
|
1087
|
+
schema:
|
|
1088
|
+
$ref: '#/components/schemas/GeoVisioError'
|
|
1089
|
+
415:
|
|
1090
|
+
description: Error if the content type is not multipart/form-data
|
|
1091
|
+
content:
|
|
1092
|
+
application/json:
|
|
1093
|
+
schema:
|
|
1094
|
+
$ref: '#/components/schemas/GeoVisioError'
|
|
1021
1095
|
"""
|
|
1022
1096
|
|
|
1023
1097
|
if not request.headers.get("Content-Type", "").startswith("multipart/form-data"):
|
|
@@ -1125,7 +1199,7 @@ def postCollectionItem(collectionId, account=None):
|
|
|
1125
1199
|
conn, collectionId, position, updated_picture, accountId, additionalMetadata, lang=get_locale().language
|
|
1126
1200
|
)
|
|
1127
1201
|
except utils.pictures.PicturePositionConflict:
|
|
1128
|
-
raise errors.InvalidAPIUsage(_("
|
|
1202
|
+
raise errors.InvalidAPIUsage(_("There is already a picture with the same index in the sequence"), status_code=409)
|
|
1129
1203
|
except utils.pictures.MetadataReadingError as e:
|
|
1130
1204
|
raise errors.InvalidAPIUsage(_("Impossible to parse picture metadata"), payload={"details": {"error": e.details}})
|
|
1131
1205
|
except utils.pictures.InvalidMetadataValue as e:
|
|
@@ -1233,60 +1307,8 @@ class PatchItemParameter(BaseModel):
|
|
|
1233
1307
|
return self.model_fields_set == {"semantics"}
|
|
1234
1308
|
|
|
1235
1309
|
|
|
1236
|
-
|
|
1237
|
-
@auth.login_required()
|
|
1238
|
-
def patchCollectionItem(collectionId, itemId, account):
|
|
1239
|
-
"""Edits properties of an existing picture
|
|
1240
|
-
|
|
1241
|
-
Note that tags cannot be added as form-data for the moment, only as JSON.
|
|
1242
|
-
|
|
1243
|
-
Note that there are rules on the editing of a picture's metadata:
|
|
1244
|
-
|
|
1245
|
-
- Only the owner of a picture can change its visibility
|
|
1246
|
-
- For core metadata (heading, capture_time, position, longitude, latitude), the owner can restrict their change by other accounts (see `collaborative_metadata` field in `/api/users/me`) and if not explicitly defined by the user, the instance's default value is used.
|
|
1247
|
-
- Everyone can add/edit/delete semantics tags.
|
|
1248
|
-
---
|
|
1249
|
-
tags:
|
|
1250
|
-
- Editing
|
|
1251
|
-
- Semantics
|
|
1252
|
-
parameters:
|
|
1253
|
-
- name: collectionId
|
|
1254
|
-
in: path
|
|
1255
|
-
description: ID of sequence the picture belongs to
|
|
1256
|
-
required: true
|
|
1257
|
-
schema:
|
|
1258
|
-
type: string
|
|
1259
|
-
- name: itemId
|
|
1260
|
-
in: path
|
|
1261
|
-
description: ID of picture to edit
|
|
1262
|
-
required: true
|
|
1263
|
-
schema:
|
|
1264
|
-
type: string
|
|
1265
|
-
requestBody:
|
|
1266
|
-
content:
|
|
1267
|
-
application/json:
|
|
1268
|
-
schema:
|
|
1269
|
-
$ref: '#/components/schemas/GeoVisioPatchItem'
|
|
1270
|
-
application/x-www-form-urlencoded:
|
|
1271
|
-
schema:
|
|
1272
|
-
$ref: '#/components/schemas/GeoVisioPatchItem'
|
|
1273
|
-
multipart/form-data:
|
|
1274
|
-
schema:
|
|
1275
|
-
$ref: '#/components/schemas/GeoVisioPatchItem'
|
|
1276
|
-
security:
|
|
1277
|
-
- bearerToken: []
|
|
1278
|
-
- cookieAuth: []
|
|
1279
|
-
responses:
|
|
1280
|
-
200:
|
|
1281
|
-
description: the wanted item
|
|
1282
|
-
content:
|
|
1283
|
-
application/geo+json:
|
|
1284
|
-
schema:
|
|
1285
|
-
$ref: '#/components/schemas/GeoVisioItem'
|
|
1286
|
-
"""
|
|
1287
|
-
|
|
1310
|
+
def update_picture(itemId: UUID, account: Optional[Account]):
|
|
1288
1311
|
# Parse received parameters
|
|
1289
|
-
|
|
1290
1312
|
metadata = None
|
|
1291
1313
|
content_type = (request.headers.get("Content-Type") or "").split(";")[0]
|
|
1292
1314
|
|
|
@@ -1300,7 +1322,7 @@ def patchCollectionItem(collectionId, itemId, account):
|
|
|
1300
1322
|
|
|
1301
1323
|
# If no parameter is set
|
|
1302
1324
|
if metadata is None or not metadata.has_override():
|
|
1303
|
-
return
|
|
1325
|
+
return (_getPictureItemById(itemId, account), 304)
|
|
1304
1326
|
|
|
1305
1327
|
# Check if picture exists and if given account is authorized to edit
|
|
1306
1328
|
with db.conn(current_app) as conn:
|
|
@@ -1381,7 +1403,61 @@ WHERE id = %(id)s"""
|
|
|
1381
1403
|
)
|
|
1382
1404
|
|
|
1383
1405
|
# Redirect response to a classic GET
|
|
1384
|
-
return
|
|
1406
|
+
return (_getPictureItemById(itemId, account), 200)
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>", methods=["PATCH"])
|
|
1410
|
+
@auth.login_required()
|
|
1411
|
+
def patchCollectionItem(collectionId, itemId, account):
|
|
1412
|
+
"""Edits properties of an existing picture
|
|
1413
|
+
|
|
1414
|
+
Note that tags cannot be added as form-data for the moment, only as JSON.
|
|
1415
|
+
|
|
1416
|
+
Note that there are rules on the editing of a picture's metadata:
|
|
1417
|
+
|
|
1418
|
+
- Only the owner of a picture can change its visibility
|
|
1419
|
+
- For core metadata (heading, capture_time, position, longitude, latitude), the owner can restrict their change by other accounts (see `collaborative_metadata` field in `/api/users/me`) and if not explicitly defined by the user, the instance's default value is used.
|
|
1420
|
+
- Everyone can add/edit/delete semantics tags.
|
|
1421
|
+
---
|
|
1422
|
+
tags:
|
|
1423
|
+
- Editing
|
|
1424
|
+
- Semantics
|
|
1425
|
+
parameters:
|
|
1426
|
+
- name: collectionId
|
|
1427
|
+
in: path
|
|
1428
|
+
description: ID of sequence the picture belongs to
|
|
1429
|
+
required: true
|
|
1430
|
+
schema:
|
|
1431
|
+
type: string
|
|
1432
|
+
- name: itemId
|
|
1433
|
+
in: path
|
|
1434
|
+
description: ID of picture to edit
|
|
1435
|
+
required: true
|
|
1436
|
+
schema:
|
|
1437
|
+
type: string
|
|
1438
|
+
requestBody:
|
|
1439
|
+
content:
|
|
1440
|
+
application/json:
|
|
1441
|
+
schema:
|
|
1442
|
+
$ref: '#/components/schemas/GeoVisioPatchItem'
|
|
1443
|
+
application/x-www-form-urlencoded:
|
|
1444
|
+
schema:
|
|
1445
|
+
$ref: '#/components/schemas/GeoVisioPatchItem'
|
|
1446
|
+
multipart/form-data:
|
|
1447
|
+
schema:
|
|
1448
|
+
$ref: '#/components/schemas/GeoVisioPatchItem'
|
|
1449
|
+
security:
|
|
1450
|
+
- bearerToken: []
|
|
1451
|
+
- cookieAuth: []
|
|
1452
|
+
responses:
|
|
1453
|
+
200:
|
|
1454
|
+
description: the wanted item
|
|
1455
|
+
content:
|
|
1456
|
+
application/geo+json:
|
|
1457
|
+
schema:
|
|
1458
|
+
$ref: '#/components/schemas/GeoVisioItem'
|
|
1459
|
+
"""
|
|
1460
|
+
return update_picture(itemId, account)
|
|
1385
1461
|
|
|
1386
1462
|
|
|
1387
1463
|
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>", methods=["DELETE"])
|