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.
- geovisio/__init__.py +11 -3
- geovisio/admin_cli/__init__.py +3 -1
- geovisio/admin_cli/cleanup.py +2 -2
- geovisio/admin_cli/user.py +75 -0
- geovisio/config_app.py +87 -4
- geovisio/templates/main.html +2 -2
- geovisio/templates/viewer.html +3 -3
- geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/da/LC_MESSAGES/messages.po +850 -0
- geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/de/LC_MESSAGES/messages.po +235 -2
- geovisio/translations/el/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/el/LC_MESSAGES/messages.po +685 -0
- geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/en/LC_MESSAGES/messages.po +244 -153
- geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/eo/LC_MESSAGES/messages.po +790 -0
- geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.po +40 -3
- geovisio/translations/hu/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/hu/LC_MESSAGES/messages.po +773 -0
- geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/it/LC_MESSAGES/messages.po +875 -0
- geovisio/translations/ja/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ja/LC_MESSAGES/messages.po +719 -0
- geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/messages.pot +225 -148
- geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/nl/LC_MESSAGES/messages.po +24 -16
- geovisio/translations/pl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/pl/LC_MESSAGES/messages.po +727 -0
- geovisio/translations/zh_Hant/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/zh_Hant/LC_MESSAGES/messages.po +719 -0
- geovisio/utils/auth.py +80 -8
- geovisio/utils/link.py +3 -2
- geovisio/utils/model_query.py +55 -0
- geovisio/utils/pictures.py +29 -62
- geovisio/utils/semantics.py +120 -0
- geovisio/utils/sequences.py +30 -23
- geovisio/utils/tokens.py +5 -3
- geovisio/utils/upload_set.py +87 -64
- geovisio/utils/website.py +50 -0
- geovisio/web/annotations.py +17 -0
- geovisio/web/auth.py +9 -5
- geovisio/web/collections.py +235 -63
- geovisio/web/configuration.py +17 -1
- geovisio/web/docs.py +99 -54
- geovisio/web/items.py +233 -100
- geovisio/web/map.py +129 -31
- geovisio/web/pages.py +240 -0
- geovisio/web/params.py +17 -0
- geovisio/web/prepare.py +165 -0
- geovisio/web/stac.py +17 -4
- geovisio/web/tokens.py +14 -4
- geovisio/web/upload_set.py +19 -10
- geovisio/web/users.py +176 -44
- geovisio/workers/runner_pictures.py +75 -50
- {geovisio-2.7.0.dist-info → geovisio-2.8.0.dist-info}/METADATA +6 -5
- geovisio-2.8.0.dist-info/RECORD +89 -0
- {geovisio-2.7.0.dist-info → geovisio-2.8.0.dist-info}/WHEEL +1 -1
- geovisio-2.7.0.dist-info/RECORD +0 -66
- {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(
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
582
|
-
|
|
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
|
-
|
|
586
|
-
relp.related_pics
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
1288
|
+
|
|
1289
|
+
metadata = None
|
|
1158
1290
|
content_type = (request.headers.get("Content-Type") or "").split(";")[0]
|
|
1159
|
-
|
|
1291
|
+
|
|
1292
|
+
try:
|
|
1160
1293
|
if request.is_json and request.json:
|
|
1161
|
-
metadata
|
|
1294
|
+
metadata = PatchItemParameter(**request.json)
|
|
1162
1295
|
elif content_type in ["multipart/form-data", "application/x-www-form-urlencoded"]:
|
|
1163
|
-
metadata
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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
|
-
|
|
1297
|
-
|
|
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
|
|