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.
Files changed (65) hide show
  1. geovisio/__init__.py +6 -1
  2. geovisio/config_app.py +5 -5
  3. geovisio/translations/ar/LC_MESSAGES/messages.mo +0 -0
  4. geovisio/translations/ar/LC_MESSAGES/messages.po +818 -0
  5. geovisio/translations/br/LC_MESSAGES/messages.po +1 -1
  6. geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
  7. geovisio/translations/da/LC_MESSAGES/messages.po +4 -3
  8. geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
  9. geovisio/translations/de/LC_MESSAGES/messages.po +55 -2
  10. geovisio/translations/el/LC_MESSAGES/messages.po +1 -1
  11. geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
  12. geovisio/translations/en/LC_MESSAGES/messages.po +193 -139
  13. geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
  14. geovisio/translations/eo/LC_MESSAGES/messages.po +53 -4
  15. geovisio/translations/es/LC_MESSAGES/messages.po +1 -1
  16. geovisio/translations/fi/LC_MESSAGES/messages.po +1 -1
  17. geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
  18. geovisio/translations/fr/LC_MESSAGES/messages.po +91 -3
  19. geovisio/translations/hu/LC_MESSAGES/messages.po +1 -1
  20. geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
  21. geovisio/translations/it/LC_MESSAGES/messages.po +63 -3
  22. geovisio/translations/ja/LC_MESSAGES/messages.po +1 -1
  23. geovisio/translations/ko/LC_MESSAGES/messages.po +1 -1
  24. geovisio/translations/messages.pot +185 -129
  25. geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
  26. geovisio/translations/nl/LC_MESSAGES/messages.po +292 -63
  27. geovisio/translations/oc/LC_MESSAGES/messages.mo +0 -0
  28. geovisio/translations/oc/LC_MESSAGES/messages.po +818 -0
  29. geovisio/translations/pl/LC_MESSAGES/messages.po +1 -1
  30. geovisio/translations/sv/LC_MESSAGES/messages.mo +0 -0
  31. geovisio/translations/sv/LC_MESSAGES/messages.po +4 -3
  32. geovisio/translations/ti/LC_MESSAGES/messages.mo +0 -0
  33. geovisio/translations/ti/LC_MESSAGES/messages.po +762 -0
  34. geovisio/translations/zh_Hant/LC_MESSAGES/messages.po +1 -1
  35. geovisio/utils/annotations.py +14 -17
  36. geovisio/utils/auth.py +14 -13
  37. geovisio/utils/cql2.py +2 -2
  38. geovisio/utils/fields.py +14 -2
  39. geovisio/utils/items.py +44 -0
  40. geovisio/utils/model_query.py +2 -2
  41. geovisio/utils/pic_shape.py +1 -1
  42. geovisio/utils/pictures.py +111 -18
  43. geovisio/utils/semantics.py +32 -3
  44. geovisio/utils/sentry.py +1 -1
  45. geovisio/utils/sequences.py +51 -34
  46. geovisio/utils/upload_set.py +285 -198
  47. geovisio/utils/website.py +1 -1
  48. geovisio/web/annotations.py +209 -68
  49. geovisio/web/auth.py +1 -1
  50. geovisio/web/collections.py +26 -22
  51. geovisio/web/configuration.py +24 -4
  52. geovisio/web/docs.py +93 -11
  53. geovisio/web/items.py +197 -121
  54. geovisio/web/params.py +44 -31
  55. geovisio/web/pictures.py +34 -0
  56. geovisio/web/tokens.py +49 -1
  57. geovisio/web/upload_set.py +150 -32
  58. geovisio/web/users.py +4 -4
  59. geovisio/web/utils.py +2 -2
  60. geovisio/workers/runner_pictures.py +128 -23
  61. {geovisio-2.9.0.dist-info → geovisio-2.10.0.dist-info}/METADATA +13 -13
  62. geovisio-2.10.0.dist-info/RECORD +105 -0
  63. geovisio-2.9.0.dist-info/RECORD +0 -98
  64. {geovisio-2.9.0.dist-info → geovisio-2.10.0.dist-info}/WHEEL +0 -0
  65. {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(seqId, dbPic):
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
- # TODO : add "updated" TS for last edit time of metadata
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 exists", p=withPicture))
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(collectionId, dbPic))
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(collectionId, itemId):
574
- """Get a picture metadata by its ID and collection 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 = %(seq)s
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 != %(seq)s
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
- WHERE sp.seq_id = %(seq)s
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
- {"seq": collectionId, "pic": itemId, "acc": accountId},
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(collectionId, record)
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(collectionId, itemId)
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
- $ref: '#/components/responses/STAC_search'
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
- order_by = SQL("ORDER BY p.geom <-> ST_Centroid(ST_MakeEnvelope(%(minx)s, %(miny)s, %(maxx)s, %(maxy)s, 4326))")
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
- order_by = SQL("ORDER BY p.geom <-> ST_Point(%(placex)s, %(placey)s, 4326)")
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
- order_by = SQL("ORDER BY p.geom <-> ST_Centroid(ST_GeomFromGeoJSON(%(geom)s))")
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
- seq = cursor.execute("SELECT seq_id FROM sequences_pictures WHERE pic_id = %s", [picture_id]).fetchone()
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(str(dbPic["seq_id"]), dbPic) for dbPic in records]
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(_("Picture at given position already exist"), status_code=409)
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
- @bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>", methods=["PATCH"])
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 getCollectionItem(collectionId, itemId)
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 getCollectionItem(collectionId, itemId)
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"])