geovisio 2.7.0__py3-none-any.whl → 2.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. geovisio/__init__.py +11 -3
  2. geovisio/admin_cli/__init__.py +3 -1
  3. geovisio/admin_cli/cleanup.py +2 -2
  4. geovisio/admin_cli/user.py +75 -0
  5. geovisio/config_app.py +87 -4
  6. geovisio/templates/main.html +2 -2
  7. geovisio/templates/viewer.html +3 -3
  8. geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
  9. geovisio/translations/da/LC_MESSAGES/messages.po +850 -0
  10. geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
  11. geovisio/translations/de/LC_MESSAGES/messages.po +235 -2
  12. geovisio/translations/el/LC_MESSAGES/messages.mo +0 -0
  13. geovisio/translations/el/LC_MESSAGES/messages.po +685 -0
  14. geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
  15. geovisio/translations/en/LC_MESSAGES/messages.po +244 -153
  16. geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
  17. geovisio/translations/eo/LC_MESSAGES/messages.po +790 -0
  18. geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
  19. geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
  20. geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
  21. geovisio/translations/fr/LC_MESSAGES/messages.po +40 -3
  22. geovisio/translations/hu/LC_MESSAGES/messages.mo +0 -0
  23. geovisio/translations/hu/LC_MESSAGES/messages.po +773 -0
  24. geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
  25. geovisio/translations/it/LC_MESSAGES/messages.po +875 -0
  26. geovisio/translations/ja/LC_MESSAGES/messages.mo +0 -0
  27. geovisio/translations/ja/LC_MESSAGES/messages.po +719 -0
  28. geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
  29. geovisio/translations/messages.pot +225 -148
  30. geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
  31. geovisio/translations/nl/LC_MESSAGES/messages.po +24 -16
  32. geovisio/translations/pl/LC_MESSAGES/messages.mo +0 -0
  33. geovisio/translations/pl/LC_MESSAGES/messages.po +727 -0
  34. geovisio/translations/zh_Hant/LC_MESSAGES/messages.mo +0 -0
  35. geovisio/translations/zh_Hant/LC_MESSAGES/messages.po +719 -0
  36. geovisio/utils/auth.py +80 -8
  37. geovisio/utils/link.py +3 -2
  38. geovisio/utils/model_query.py +55 -0
  39. geovisio/utils/pictures.py +29 -62
  40. geovisio/utils/semantics.py +120 -0
  41. geovisio/utils/sequences.py +30 -23
  42. geovisio/utils/tokens.py +5 -3
  43. geovisio/utils/upload_set.py +87 -64
  44. geovisio/utils/website.py +50 -0
  45. geovisio/web/annotations.py +17 -0
  46. geovisio/web/auth.py +9 -5
  47. geovisio/web/collections.py +235 -63
  48. geovisio/web/configuration.py +17 -1
  49. geovisio/web/docs.py +99 -54
  50. geovisio/web/items.py +233 -100
  51. geovisio/web/map.py +129 -31
  52. geovisio/web/pages.py +240 -0
  53. geovisio/web/params.py +17 -0
  54. geovisio/web/prepare.py +165 -0
  55. geovisio/web/stac.py +17 -4
  56. geovisio/web/tokens.py +14 -4
  57. geovisio/web/upload_set.py +19 -10
  58. geovisio/web/users.py +176 -44
  59. geovisio/workers/runner_pictures.py +75 -50
  60. {geovisio-2.7.0.dist-info → geovisio-2.8.0.dist-info}/METADATA +6 -5
  61. geovisio-2.8.0.dist-info/RECORD +89 -0
  62. {geovisio-2.7.0.dist-info → geovisio-2.8.0.dist-info}/WHEEL +1 -1
  63. geovisio-2.7.0.dist-info/RECORD +0 -66
  64. {geovisio-2.7.0.dist-info → geovisio-2.8.0.dist-info}/LICENSE +0 -0
geovisio/web/items.py CHANGED
@@ -1,14 +1,18 @@
1
+ from datetime import datetime
1
2
  import json
2
3
  import logging
3
4
  import os
4
- from typing import Dict, Optional, Any
5
+ from typing import Dict, List, Optional, Any
5
6
  from urllib.parse import unquote
6
7
  from psycopg.types.json import Jsonb
8
+ from pydantic import BaseModel, ConfigDict, ValidationError, field_validator, model_validator
7
9
  from werkzeug.datastructures import MultiDict
8
10
  from uuid import UUID
9
11
  from geovisio import errors, utils
10
12
  from geovisio.utils import auth, db
13
+ from geovisio.utils.params import validation_error
11
14
  from geovisio.utils.pictures import cleanupExif
15
+ from geovisio.utils.semantics import SemanticTagUpdate, Entity, EntityType, update_tags
12
16
  from geovisio.web.params import (
13
17
  as_latitude,
14
18
  as_longitude,
@@ -19,6 +23,7 @@ from geovisio.web.params import (
19
23
  parse_list,
20
24
  parse_lonlat,
21
25
  parse_distance_range,
26
+ parse_picture_heading,
22
27
  )
23
28
  from geovisio.utils.fields import Bounds
24
29
  import hashlib
@@ -38,6 +43,7 @@ from flask import current_app, request, url_for, Blueprint
38
43
  from flask_babel import gettext as _, get_locale
39
44
  from geopic_tag_reader.writer import writePictureMetadata, PictureMetadata
40
45
  import sentry_sdk
46
+ import math
41
47
 
42
48
 
43
49
  bp = Blueprint("stac_items", __name__, url_prefix="/api")
@@ -77,6 +83,8 @@ def dbPictureToStacItem(seqId, dbPic):
77
83
  sensorDim = None
78
84
  if None in visibleArea or visibleArea == [0, 0, 0, 0]:
79
85
  visibleArea = None
86
+ elif "height" in dbPic["metadata"] and "width" in dbPic["metadata"]:
87
+ sensorDim = [dbPic["metadata"]["width"], dbPic["metadata"]["height"]]
80
88
 
81
89
  item = removeNoneInDict(
82
90
  {
@@ -119,7 +127,11 @@ def dbPictureToStacItem(seqId, dbPic):
119
127
  }
120
128
  )
121
129
  if "metadata" in dbPic
122
- and any(True for f in dbPic["metadata"] if f in ["make", "model", "focal_length", "field_of_view", "crop"])
130
+ and any(
131
+ True
132
+ for f in dbPic["metadata"]
133
+ if f in ["make", "model", "focal_length", "field_of_view", "crop", "width", "height"]
134
+ )
123
135
  else {}
124
136
  ),
125
137
  "pers:pitch": dbPic["metadata"].get("pitch"),
@@ -128,9 +140,12 @@ def dbPictureToStacItem(seqId, dbPic):
128
140
  "geovisio:producer": dbPic["account_name"],
129
141
  "original_file:size": dbPic["metadata"].get("originalFileSize"),
130
142
  "original_file:name": dbPic["metadata"].get("originalFileName"),
143
+ "panoramax:horizontal_pixel_density": dbPic.get("h_pixel_density"),
131
144
  "geovisio:image": _getHDJpgPictureURL(dbPic["id"], dbPic.get("status")),
132
145
  "geovisio:thumbnail": _getThumbJpgPictureURL(dbPic["id"], dbPic.get("status")),
133
146
  "exif": removeNoneInDict(cleanupExif(dbPic["exif"])),
147
+ "quality:horizontal_accuracy": float("{:.1f}".format(dbPic["gps_accuracy_m"])) if dbPic.get("gps_accuracy_m") else None,
148
+ "semantics": dbPic["semantics"] if "semantics" in dbPic else None,
134
149
  }
135
150
  ),
136
151
  "links": cleanNoneInList(
@@ -412,24 +427,33 @@ def getCollectionItems(collectionId):
412
427
 
413
428
  query = SQL(
414
429
  """
415
- SELECT
416
- p.id, p.ts, p.heading, p.metadata, p.inserted_at, p.status,
417
- ST_AsGeoJSON(p.geom)::json AS geojson,
418
- a.name AS account_name,
430
+ SELECT
431
+ p.id, p.ts, p.heading, p.metadata, p.inserted_at, p.status,
432
+ ST_AsGeoJSON(p.geom)::json AS geojson,
433
+ a.name AS account_name,
419
434
  p.account_id AS account_id,
420
- sp.rank, p.exif,
421
- CASE WHEN LAG(p.status) OVER othpics = 'ready' THEN LAG(p.id) OVER othpics END AS prevpic,
422
- CASE WHEN LAG(p.status) OVER othpics = 'ready' THEN ST_AsGeoJSON(LAG(p.geom) OVER othpics)::json END AS prevpicgeojson,
423
- CASE WHEN LEAD(p.status) OVER othpics = 'ready' THEN LEAD(p.id) OVER othpics END AS nextpic,
424
- CASE WHEN LEAD(p.status) OVER othpics = 'ready' THEN ST_AsGeoJSON(LEAD(p.geom) OVER othpics)::json END AS nextpicgeojson
425
- FROM sequences_pictures sp
426
- JOIN pictures p ON sp.pic_id = p.id
427
- JOIN accounts a ON a.id = p.account_id
428
- JOIN sequences s ON s.id = sp.seq_id
429
- WHERE
430
- {filter}
431
- WINDOW othpics AS (PARTITION BY sp.seq_id ORDER BY sp.rank)
432
- ORDER BY rank
435
+ sp.rank, p.exif, p.gps_accuracy_m, p.h_pixel_density,
436
+ CASE WHEN LAG(p.status) OVER othpics = 'ready' THEN LAG(p.id) OVER othpics END AS prevpic,
437
+ CASE WHEN LAG(p.status) OVER othpics = 'ready' THEN ST_AsGeoJSON(LAG(p.geom) OVER othpics)::json END AS prevpicgeojson,
438
+ CASE WHEN LEAD(p.status) OVER othpics = 'ready' THEN LEAD(p.id) OVER othpics END AS nextpic,
439
+ CASE WHEN LEAD(p.status) OVER othpics = 'ready' THEN ST_AsGeoJSON(LEAD(p.geom) OVER othpics)::json END AS nextpicgeojson,
440
+ t.semantics
441
+ FROM sequences_pictures sp
442
+ JOIN pictures p ON sp.pic_id = p.id
443
+ JOIN accounts a ON a.id = p.account_id
444
+ JOIN sequences s ON s.id = sp.seq_id
445
+ LEFT JOIN (
446
+ SELECT picture_id, json_agg(json_strip_nulls(json_build_object(
447
+ 'key', key,
448
+ 'value', value
449
+ ))) AS semantics
450
+ FROM pictures_semantics
451
+ GROUP BY picture_id
452
+ ) t ON t.picture_id = p.id
453
+ WHERE
454
+ {filter}
455
+ WINDOW othpics AS (PARTITION BY sp.seq_id ORDER BY sp.rank)
456
+ ORDER BY rank
433
457
  {limit}
434
458
  """
435
459
  ).format(filter=SQL(" AND ").join(filters), limit=sql_limit)
@@ -578,30 +602,39 @@ def _getPictureItemById(collectionId, itemId):
578
602
  # Get rank + position of wanted picture
579
603
  record = cursor.execute(
580
604
  """
581
- SELECT
582
- p.id, sp.rank, ST_AsGeoJSON(p.geom)::json AS geojson, p.heading, p.ts, p.metadata,
605
+ SELECT
606
+ p.id, sp.rank, ST_AsGeoJSON(p.geom)::json AS geojson, p.heading, p.ts, p.metadata,
583
607
  p.inserted_at, p.status, accounts.name AS account_name,
584
608
  p.account_id AS account_id,
585
- spl.prevpic, spl.prevpicgeojson, spl.nextpic, spl.nextpicgeojson, p.exif,
586
- relp.related_pics
587
- FROM pictures p
588
- JOIN sequences_pictures sp ON sp.pic_id = p.id
589
- JOIN accounts ON p.account_id = accounts.id
590
- JOIN sequences s ON sp.seq_id = s.id
591
- LEFT JOIN (
592
- SELECT
593
- p.id,
594
- LAG(p.id) OVER othpics AS prevpic,
595
- ST_AsGeoJSON(LAG(p.geom) OVER othpics)::json AS prevpicgeojson,
596
- LEAD(p.id) OVER othpics AS nextpic,
597
- ST_AsGeoJSON(LEAD(p.geom) OVER othpics)::json AS nextpicgeojson
598
- FROM pictures p
599
- JOIN sequences_pictures sp ON p.id = sp.pic_id
600
- WHERE
601
- sp.seq_id = %(seq)s
602
- AND (p.account_id = %(acc)s OR p.status != 'hidden')
603
- WINDOW othpics AS (PARTITION BY sp.seq_id ORDER BY sp.rank)
604
- ) spl ON p.id = spl.id
609
+ spl.prevpic, spl.prevpicgeojson, spl.nextpic, spl.nextpicgeojson, p.exif,
610
+ relp.related_pics, p.gps_accuracy_m, p.h_pixel_density,
611
+ t.semantics
612
+ FROM pictures p
613
+ JOIN sequences_pictures sp ON sp.pic_id = p.id
614
+ JOIN accounts ON p.account_id = accounts.id
615
+ JOIN sequences s ON sp.seq_id = s.id
616
+ LEFT JOIN (
617
+ SELECT picture_id, json_agg(json_strip_nulls(json_build_object(
618
+ 'key', key,
619
+ 'value', value
620
+ ))) AS semantics
621
+ FROM pictures_semantics
622
+ GROUP BY picture_id
623
+ ) t ON t.picture_id = p.id
624
+ LEFT JOIN (
625
+ SELECT
626
+ p.id,
627
+ LAG(p.id) OVER othpics AS prevpic,
628
+ ST_AsGeoJSON(LAG(p.geom) OVER othpics)::json AS prevpicgeojson,
629
+ LEAD(p.id) OVER othpics AS nextpic,
630
+ ST_AsGeoJSON(LEAD(p.geom) OVER othpics)::json AS nextpicgeojson
631
+ FROM pictures p
632
+ JOIN sequences_pictures sp ON p.id = sp.pic_id
633
+ WHERE
634
+ sp.seq_id = %(seq)s
635
+ AND (p.account_id = %(acc)s OR p.status != 'hidden')
636
+ WINDOW othpics AS (PARTITION BY sp.seq_id ORDER BY sp.rank)
637
+ ) spl ON p.id = spl.id
605
638
  LEFT JOIN (
606
639
  SELECT array_agg(ARRAY[seq_id::text, id::text, geom, tstxt]) AS related_pics
607
640
  FROM (
@@ -639,12 +672,12 @@ def _getPictureItemById(collectionId, itemId):
639
672
  ORDER BY relsp.seq_id, p.geom <-> relp.geom
640
673
  ) a
641
674
  ) relp ON TRUE
642
- WHERE sp.seq_id = %(seq)s
643
- AND p.id = %(pic)s
644
- AND (p.account_id = %(acc)s OR p.status != 'hidden')
645
- AND (s.status != 'hidden' OR s.account_id = %(acc)s)
675
+ WHERE sp.seq_id = %(seq)s
676
+ AND p.id = %(pic)s
677
+ AND (p.account_id = %(acc)s OR p.status != 'hidden')
678
+ AND (s.status != 'hidden' OR s.account_id = %(acc)s)
646
679
  AND s.status != 'deleted'
647
- """,
680
+ """,
648
681
  {"seq": collectionId, "pic": itemId, "acc": accountId},
649
682
  ).fetchone()
650
683
 
@@ -898,11 +931,20 @@ SELECT * FROM (
898
931
  sp.seq_id, sp.rank AS rank,
899
932
  accounts.name AS account_name,
900
933
  p.account_id AS account_id,
901
- p.exif
934
+ p.exif, p.gps_accuracy_m, p.h_pixel_density,
935
+ t.semantics
902
936
  FROM pictures p
903
937
  LEFT JOIN sequences_pictures sp ON p.id = sp.pic_id
904
938
  LEFT JOIN sequences s ON s.id = sp.seq_id
905
939
  LEFT JOIN accounts ON p.account_id = accounts.id
940
+ LEFT JOIN (
941
+ SELECT picture_id, json_agg(json_strip_nulls(json_build_object(
942
+ 'key', key,
943
+ 'value', value
944
+ ))) AS semantics
945
+ FROM pictures_semantics
946
+ GROUP BY picture_id
947
+ ) t ON t.picture_id = p.id
906
948
  WHERE {sqlWhere}
907
949
  {orderBy}
908
950
  LIMIT %(limit)s
@@ -1110,13 +1152,102 @@ def postCollectionItem(collectionId, account=None):
1110
1152
  )
1111
1153
 
1112
1154
 
1155
+ class PatchItemParameter(BaseModel):
1156
+ """Parameters used to add an item to an UploadSet"""
1157
+
1158
+ heading: Optional[int] = None
1159
+ """Heading of the picture. The new heading will not be persisted in the picture's exif tags for the moment."""
1160
+ visible: Optional[bool] = None
1161
+ """Should the picture be publicly visible ?"""
1162
+
1163
+ capture_time: Optional[datetime] = None
1164
+ """Capture time of the picture. The new capture time will not be persisted in the picture's exif tags for the moment."""
1165
+ longitude: Optional[float] = None
1166
+ """Longitude of the picture. The new longitude will not be persisted in the picture's exif tags for the moment."""
1167
+ latitude: Optional[float] = None
1168
+ """Latitude of the picture. The new latitude will not be persisted in the picture's exif tags for the moment."""
1169
+
1170
+ semantics: Optional[List[SemanticTagUpdate]] = None
1171
+ """Tags to update on the picture. By default each tag will be added to the picture's tags, but you can change this behavior by setting the `action` parameter to `delete`.
1172
+
1173
+ If you want to replace a tag, you need to first delete it, then add it again.
1174
+
1175
+ Like:
1176
+ [
1177
+ {"key": "some_key", "value": "some_value", "action": "delete"},
1178
+ {"key": "some_key", "value": "some_new_value"}
1179
+ ]
1180
+
1181
+
1182
+ Note that updating tags is only possible with JSON data, not with form-data."""
1183
+
1184
+ def has_override(self) -> bool:
1185
+ return self.model_fields_set
1186
+
1187
+ @field_validator("heading", mode="before")
1188
+ @classmethod
1189
+ def parse_heading(cls, value):
1190
+ if value is None:
1191
+ return None
1192
+ return parse_picture_heading(value)
1193
+
1194
+ @field_validator("visible", mode="before")
1195
+ @classmethod
1196
+ def parse_visible(cls, value):
1197
+ if value not in ["true", "false"]:
1198
+ raise errors.InvalidAPIUsage(_("Picture visibility parameter (visible) should be either unset, true or false"), status_code=400)
1199
+ return value == "true"
1200
+
1201
+ @field_validator("capture_time", mode="before")
1202
+ @classmethod
1203
+ def parse_capture_time(cls, value):
1204
+ if value is None:
1205
+ return None
1206
+ return parse_datetime(
1207
+ value,
1208
+ error=_(
1209
+ "Parameter `capture_time` is not a valid datetime, it should be an iso formated datetime (like '2017-07-21T17:32:28Z')."
1210
+ ),
1211
+ )
1212
+
1213
+ @field_validator("longitude")
1214
+ @classmethod
1215
+ def parse_longitude(cls, value):
1216
+ return as_longitude(value, error=_("For parameter `longitude`, `%(v)s` is not a valid longitude", v=value))
1217
+
1218
+ @field_validator("latitude")
1219
+ @classmethod
1220
+ def parse_latitude(cls, value):
1221
+ return as_latitude(value, error=_("For parameter `latitude`, `%(v)s` is not a valid latitude", v=value))
1222
+
1223
+ @model_validator(mode="after")
1224
+ def validate(self):
1225
+ if self.latitude is None and self.longitude is not None:
1226
+ raise errors.InvalidAPIUsage(_("Longitude cannot be overridden alone, latitude also needs to be set"))
1227
+ if self.longitude is None and self.latitude is not None:
1228
+ raise errors.InvalidAPIUsage(_("Latitude cannot be overridden alone, longitude also needs to be set"))
1229
+ return self
1230
+
1231
+ def has_only_semantics_updates(self):
1232
+ return self.model_fields_set == {"semantics"}
1233
+
1234
+
1113
1235
  @bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>", methods=["PATCH"])
1114
1236
  @auth.login_required()
1115
1237
  def patchCollectionItem(collectionId, itemId, account):
1116
1238
  """Edits properties of an existing picture
1239
+
1240
+ Note that tags cannot be added as form-data for the moment, only as JSON.
1241
+
1242
+ Note that there are rules on the editing of a picture's metadata:
1243
+
1244
+ - Only the owner of a picture can change its visibility
1245
+ - 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.
1246
+ - Everyone can add/edit/delete semantics tags.
1117
1247
  ---
1118
1248
  tags:
1119
1249
  - Editing
1250
+ - Tags
1120
1251
  parameters:
1121
1252
  - name: collectionId
1122
1253
  in: path
@@ -1154,37 +1285,20 @@ def patchCollectionItem(collectionId, itemId, account):
1154
1285
  """
1155
1286
 
1156
1287
  # Parse received parameters
1157
- metadata = {}
1288
+
1289
+ metadata = None
1158
1290
  content_type = (request.headers.get("Content-Type") or "").split(";")[0]
1159
- for param in ["visible", "heading"]:
1291
+
1292
+ try:
1160
1293
  if request.is_json and request.json:
1161
- metadata[param] = request.json.get(param)
1294
+ metadata = PatchItemParameter(**request.json)
1162
1295
  elif content_type in ["multipart/form-data", "application/x-www-form-urlencoded"]:
1163
- metadata[param] = request.form.get(param)
1164
-
1165
- visible = metadata.get("visible")
1166
- if visible is not None:
1167
- if visible not in ["true", "false"]:
1168
- raise errors.InvalidAPIUsage(_("Picture visibility parameter (visible) should be either unset, true or false"), status_code=400)
1169
- visible = visible == "true"
1170
-
1171
- # Check if heading is valid
1172
- heading = metadata.get("heading")
1173
- if heading is not None:
1174
- try:
1175
- heading = int(heading)
1176
- if heading < 0 or heading > 360:
1177
- raise ValueError()
1178
- except ValueError:
1179
- raise errors.InvalidAPIUsage(
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
- ),
1183
- status_code=400,
1184
- )
1296
+ metadata = PatchItemParameter(**request.form)
1297
+ except ValidationError as ve:
1298
+ raise errors.InvalidAPIUsage(_("Impossible to parse parameters"), payload=validation_error(ve))
1185
1299
 
1186
1300
  # If no parameter is set
1187
- if {visible, heading} == {None}:
1301
+ if metadata is None or not metadata.has_override():
1188
1302
  return getCollectionItem(collectionId, itemId)
1189
1303
 
1190
1304
  # Check if picture exists and if given account is authorized to edit
@@ -1196,10 +1310,21 @@ def patchCollectionItem(collectionId, itemId, account):
1196
1310
  if not pic:
1197
1311
  raise errors.InvalidAPIUsage(_("Picture %(p)s wasn't found in database", p=itemId), status_code=404)
1198
1312
 
1199
- # Account associated to picture doesn't match current user
1200
1313
  if account is not None and account.id != str(pic["account_id"]):
1201
- raise errors.InvalidAPIUsage(_("You're not authorized to edit this picture"), status_code=403)
1314
+ # Account associated to picture doesn't match current user
1315
+ # and we limit the status change to only the owner.
1316
+ if metadata.visible is not None:
1317
+ raise errors.InvalidAPIUsage(
1318
+ _("You're not authorized to edit the visibility of this picture. Only the owner can change this."), status_code=403
1319
+ )
1202
1320
 
1321
+ # for core metadata editing (all appart the semantic tags), we check if the user has allowed it
1322
+ if not metadata.has_only_semantics_updates():
1323
+ if not auth.account_allow_collaborative_editing(pic["account_id"]):
1324
+ raise errors.InvalidAPIUsage(
1325
+ _("You're not authorized to edit this picture, collaborative editing is not allowed"),
1326
+ status_code=403,
1327
+ )
1203
1328
  sqlUpdates = []
1204
1329
  sqlParams = {"id": itemId, "account": account.id}
1205
1330
 
@@ -1217,34 +1342,42 @@ def patchCollectionItem(collectionId, itemId, account):
1217
1342
  )
1218
1343
 
1219
1344
  newStatus = None
1220
- if visible is not None:
1221
- newStatus = "ready" if visible is True else "hidden"
1345
+ if metadata.visible is not None:
1346
+ newStatus = "ready" if metadata.visible is True else "hidden"
1222
1347
  if newStatus != oldStatus:
1223
1348
  sqlUpdates.append(SQL("status = %(status)s"))
1224
1349
  sqlParams["status"] = newStatus
1225
1350
 
1226
- if heading is not None:
1351
+ if metadata.heading is not None:
1227
1352
  sqlUpdates.extend([SQL("heading = %(heading)s"), SQL("heading_computed = false")])
1228
- sqlParams["heading"] = heading
1229
-
1230
- if not sqlUpdates:
1231
- # Nothing to change, we can return the item
1232
- return getCollectionItem(collectionId, itemId)
1233
-
1234
- # Note: we set the field `last_account_to_edit` to track who changed the collection last (later we'll make it possible for everybody to edit some collection fields)
1235
- # setting this field will trigger the history tracking of the collection (using postgres trigger)
1236
- sqlUpdates.append(SQL("last_account_to_edit = %(account)s"))
1237
-
1238
- cursor.execute(
1239
- SQL(
1240
- """
1241
- UPDATE pictures
1242
- SET {updates}
1243
- WHERE id = %(id)s
1244
- """
1245
- ).format(updates=SQL(", ").join(sqlUpdates)),
1246
- sqlParams,
1247
- )
1353
+ sqlParams["heading"] = metadata.heading
1354
+
1355
+ if metadata.capture_time is not None:
1356
+ sqlUpdates.extend([SQL("ts = %(capture_time)s")])
1357
+ sqlParams["capture_time"] = metadata.capture_time
1358
+
1359
+ if metadata.longitude is not None and metadata.latitude is not None:
1360
+ sqlUpdates.extend([SQL("geom = ST_SetSRID(ST_MakePoint(%(longitude)s, %(latitude)s), 4326)")])
1361
+ sqlParams["longitude"] = metadata.longitude
1362
+ sqlParams["latitude"] = metadata.latitude
1363
+
1364
+ if metadata.semantics is not None:
1365
+ # semantic tags are managed separately
1366
+ update_tags(cursor, Entity(type=EntityType.pic, id=itemId), metadata.semantics, account=account.id)
1367
+
1368
+ if sqlUpdates:
1369
+ # Note: we set the field `last_account_to_edit` to track who changed the collection last
1370
+ # setting this field will trigger the history tracking of the collection (using postgres trigger)
1371
+ sqlUpdates.append(SQL("last_account_to_edit = %(account)s"))
1372
+
1373
+ cursor.execute(
1374
+ SQL(
1375
+ """UPDATE pictures
1376
+ SET {updates}
1377
+ WHERE id = %(id)s"""
1378
+ ).format(updates=SQL(", ").join(sqlUpdates)),
1379
+ sqlParams,
1380
+ )
1248
1381
 
1249
1382
  # Redirect response to a classic GET
1250
1383
  return getCollectionItem(collectionId, itemId)
@@ -1293,8 +1426,8 @@ def deleteCollectionItem(collectionId, itemId, account):
1293
1426
 
1294
1427
  cursor.execute("DELETE FROM pictures WHERE id = %s", [itemId])
1295
1428
 
1296
- # delete images
1297
- utils.pictures.removeAllFiles(itemId)
1429
+ # let the picture be removed from the filesystem by the asynchronous workers
1430
+ current_app.background_processor.process_pictures()
1298
1431
 
1299
1432
  return "", 204
1300
1433