geovisio 2.10.0__py3-none-any.whl → 2.11.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 +3 -1
- geovisio/admin_cli/user.py +7 -2
- geovisio/config_app.py +21 -7
- geovisio/translations/be/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/be/LC_MESSAGES/messages.po +886 -0
- geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/da/LC_MESSAGES/messages.po +96 -5
- geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/de/LC_MESSAGES/messages.po +171 -132
- geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/en/LC_MESSAGES/messages.po +169 -146
- geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/eo/LC_MESSAGES/messages.po +3 -2
- geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.po +3 -2
- geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/it/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/messages.pot +159 -138
- geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/nl/LC_MESSAGES/messages.po +44 -2
- geovisio/translations/oc/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/oc/LC_MESSAGES/messages.po +9 -6
- geovisio/translations/pt/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/pt/LC_MESSAGES/messages.po +944 -0
- geovisio/translations/pt_BR/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/pt_BR/LC_MESSAGES/messages.po +942 -0
- geovisio/translations/sv/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/sv/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/tr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/tr/LC_MESSAGES/messages.po +927 -0
- geovisio/translations/uk/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/uk/LC_MESSAGES/messages.po +920 -0
- geovisio/utils/annotations.py +7 -4
- geovisio/utils/auth.py +33 -0
- geovisio/utils/cql2.py +20 -3
- geovisio/utils/pictures.py +16 -18
- geovisio/utils/sequences.py +104 -75
- geovisio/utils/upload_set.py +20 -10
- geovisio/utils/users.py +18 -0
- geovisio/web/annotations.py +96 -3
- geovisio/web/collections.py +169 -76
- geovisio/web/configuration.py +12 -0
- geovisio/web/docs.py +17 -3
- geovisio/web/items.py +129 -72
- geovisio/web/map.py +92 -54
- geovisio/web/pages.py +48 -4
- geovisio/web/params.py +56 -11
- geovisio/web/pictures.py +3 -3
- geovisio/web/prepare.py +4 -2
- geovisio/web/queryables.py +57 -0
- geovisio/web/stac.py +8 -2
- geovisio/web/upload_set.py +83 -26
- geovisio/web/users.py +85 -4
- geovisio/web/utils.py +24 -6
- {geovisio-2.10.0.dist-info → geovisio-2.11.0.dist-info}/METADATA +3 -2
- {geovisio-2.10.0.dist-info → geovisio-2.11.0.dist-info}/RECORD +58 -46
- {geovisio-2.10.0.dist-info → geovisio-2.11.0.dist-info}/WHEEL +0 -0
- {geovisio-2.10.0.dist-info → geovisio-2.11.0.dist-info}/licenses/LICENSE +0 -0
geovisio/web/docs.py
CHANGED
|
@@ -394,6 +394,7 @@ The CSV headers will be:
|
|
|
394
394
|
"geovisio:sorted-by": {"$ref": "#/components/schemas/GeoVisioCollectionSortedBy"},
|
|
395
395
|
"geovisio:upload-software": {"$ref": "#/components/schemas/GeoVisioCollectionUploadSoftware"},
|
|
396
396
|
"geovisio:length_km": {"$ref": "#/components/schemas/GeoVisioLengthKm"},
|
|
397
|
+
"geovisio:visibility": {"$ref": "#/components/schemas/GeoVisioVisibility"},
|
|
397
398
|
"quality:horizontal_accuracy": {"type": "number", "title": "Estimated GPS position precision (in meters)"},
|
|
398
399
|
"quality:horizontal_accuracy_type": {
|
|
399
400
|
"type": "string",
|
|
@@ -496,6 +497,7 @@ The CSV headers will be:
|
|
|
496
497
|
"minimum": 1,
|
|
497
498
|
"title": "Rank of the picture in its collection.",
|
|
498
499
|
},
|
|
500
|
+
"geovisio:visibility": {"$ref": "#/components/schemas/GeoVisioVisibility"},
|
|
499
501
|
"original_file:size": {"type": "integer", "minimum": 0, "title": "Size of the original file, in bytes"},
|
|
500
502
|
"original_file:name": {"type": "string", "title": "Original file name"},
|
|
501
503
|
"panoramax:horizontal_pixel_density": {
|
|
@@ -655,7 +657,15 @@ Available properties are:
|
|
|
655
657
|
},
|
|
656
658
|
"GeoVisioItemStatus": {
|
|
657
659
|
"type": "string",
|
|
658
|
-
"enum": ["ready", "broken", "waiting-for-process"],
|
|
660
|
+
"enum": ["ready", "broken", "waiting-for-process", "pouet"],
|
|
661
|
+
},
|
|
662
|
+
"GeoVisioVisibility": {
|
|
663
|
+
"type": "string",
|
|
664
|
+
"description": """Visibility of the object. Can be set to:
|
|
665
|
+
* `anyone`: visible to anyone
|
|
666
|
+
* `owner-only`: visible to the owner and administrator only
|
|
667
|
+
* `logged-only`: visible to logged users only. Note that this is not available on all Panoramax instances, only those with restricted account creation. See the possible visibility values for a given instance on /api/configuration (field `visibility`).""",
|
|
668
|
+
"enum": ["anyone", "owner-only", "logged-only"],
|
|
659
669
|
},
|
|
660
670
|
"GeoVisioPostReport": reports.ReportCreationParameter.model_json_schema(
|
|
661
671
|
ref_template="#/components/schemas/GeoVisioPostReport/$defs/{model}", mode="serialization"
|
|
@@ -938,7 +948,8 @@ A CQL2 filter expression for filtering sequences.
|
|
|
938
948
|
Allowed properties are:
|
|
939
949
|
* "created": upload date
|
|
940
950
|
* "updated": last edit date
|
|
941
|
-
|
|
951
|
+
|
|
952
|
+
Note: the `status` filter is not supported anymore, use the `show_deleted` parameter instead if you need to query deleted collections
|
|
942
953
|
|
|
943
954
|
Usage doc can be found here: https://docs.geoserver.org/2.23.x/en/user/tutorials/cql/cql_tutorial.html
|
|
944
955
|
|
|
@@ -1009,7 +1020,7 @@ Note that this parameter is not taken in account for 360° pictures, as by defin
|
|
|
1009
1020
|
"description": """Define the sort order of the results of a search.
|
|
1010
1021
|
Sort order is defined based on preceding '+' (asc) or '-' (desc).
|
|
1011
1022
|
|
|
1012
|
-
By default we sort to get the last updated pictures
|
|
1023
|
+
By default we sort to get the last updated pictures first (-updated).
|
|
1013
1024
|
|
|
1014
1025
|
Available properties are:
|
|
1015
1026
|
* `ts`: capture datetime of the picture
|
|
@@ -1037,11 +1048,14 @@ For the moment only equality (`=`) and list (`IN`) filters are supported. We do
|
|
|
1037
1048
|
|
|
1038
1049
|
To search for any values of a semantic tag, use `semantics.some_key IS NOT NULL` (case matter here).
|
|
1039
1050
|
|
|
1051
|
+
To search for items with any semantic tags, use `"semantics" IS NOT NULL`.
|
|
1052
|
+
|
|
1040
1053
|
Examples:
|
|
1041
1054
|
|
|
1042
1055
|
* "semantics.osm|traffic_sign"='yes'
|
|
1043
1056
|
* "semantics.osm|traffic_sign" IS NOT NULL'
|
|
1044
1057
|
* "semantics.osm|amenity" IN ('bench', 'whatever') OR "semantics.osm|traffic_sign"='yes'
|
|
1058
|
+
* "semantics" IS NOT NULL
|
|
1045
1059
|
""",
|
|
1046
1060
|
"required": False,
|
|
1047
1061
|
"schema": {
|
geovisio/web/items.py
CHANGED
|
@@ -5,8 +5,7 @@ import os
|
|
|
5
5
|
from typing import Dict, List, Optional, Any
|
|
6
6
|
from urllib.parse import unquote
|
|
7
7
|
from psycopg.types.json import Jsonb
|
|
8
|
-
from pydantic import BaseModel,
|
|
9
|
-
from shapely import intersects
|
|
8
|
+
from pydantic import BaseModel, ValidationError, field_validator, model_validator
|
|
10
9
|
from werkzeug.datastructures import MultiDict
|
|
11
10
|
from uuid import UUID
|
|
12
11
|
from geovisio import errors, utils
|
|
@@ -30,13 +29,15 @@ from geovisio.web.params import (
|
|
|
30
29
|
parse_lonlat,
|
|
31
30
|
parse_distance_range,
|
|
32
31
|
parse_picture_heading,
|
|
32
|
+
Visibility,
|
|
33
|
+
check_visibility,
|
|
33
34
|
)
|
|
34
35
|
from geovisio.utils.fields import Bounds, SQLDirection
|
|
35
36
|
import hashlib
|
|
36
37
|
from psycopg.rows import dict_row
|
|
37
38
|
from psycopg.sql import SQL
|
|
38
39
|
from geovisio.web.utils import (
|
|
39
|
-
|
|
40
|
+
accountOrDefault,
|
|
40
41
|
cleanNoneInList,
|
|
41
42
|
dbTsToStac,
|
|
42
43
|
dbTsToStacTZ,
|
|
@@ -49,12 +50,18 @@ from flask import current_app, request, url_for, Blueprint
|
|
|
49
50
|
from flask_babel import gettext as _, get_locale
|
|
50
51
|
from geopic_tag_reader.writer import writePictureMetadata, PictureMetadata
|
|
51
52
|
import sentry_sdk
|
|
52
|
-
import math
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
bp = Blueprint("stac_items", __name__, url_prefix="/api")
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
def retrocompatible_picture_status(db_pic):
|
|
59
|
+
"""We used to display status='hidden' for hidden picture, now that the status and the visiblity has been split, we still return a 'ready' status for retrocompatibility"""
|
|
60
|
+
if db_pic.get("status") == "hidden":
|
|
61
|
+
return "ready"
|
|
62
|
+
return db_pic.get("status")
|
|
63
|
+
|
|
64
|
+
|
|
58
65
|
def dbPictureToStacItem(dbPic):
|
|
59
66
|
"""Transforms a picture extracted from database into a STAC Item
|
|
60
67
|
|
|
@@ -143,14 +150,15 @@ def dbPictureToStacItem(dbPic):
|
|
|
143
150
|
),
|
|
144
151
|
"pers:pitch": dbPic["metadata"].get("pitch"),
|
|
145
152
|
"pers:roll": dbPic["metadata"].get("roll"),
|
|
146
|
-
"geovisio:status": dbPic
|
|
153
|
+
"geovisio:status": retrocompatible_picture_status(dbPic),
|
|
154
|
+
"geovisio:visibility": dbPic.get("visibility"),
|
|
147
155
|
"geovisio:producer": dbPic["account_name"],
|
|
148
156
|
"geovisio:rank_in_collection": dbPic["rank"],
|
|
149
157
|
"original_file:size": dbPic["metadata"].get("originalFileSize"),
|
|
150
158
|
"original_file:name": dbPic["metadata"].get("originalFileName"),
|
|
151
159
|
"panoramax:horizontal_pixel_density": dbPic.get("h_pixel_density"),
|
|
152
|
-
"geovisio:image": _getHDJpgPictureURL(dbPic["id"], dbPic.get("
|
|
153
|
-
"geovisio:thumbnail": _getThumbJpgPictureURL(dbPic["id"], dbPic.get("
|
|
160
|
+
"geovisio:image": _getHDJpgPictureURL(dbPic["id"], dbPic.get("visibility")),
|
|
161
|
+
"geovisio:thumbnail": _getThumbJpgPictureURL(dbPic["id"], dbPic.get("visibility")),
|
|
154
162
|
"exif": removeNoneInDict(cleanupExif(dbPic["exif"])),
|
|
155
163
|
"quality:horizontal_accuracy": float("{:.1f}".format(dbPic["gps_accuracy_m"])) if dbPic.get("gps_accuracy_m") else None,
|
|
156
164
|
"semantics": [s for s in dbPic.get("semantics") or [] if s],
|
|
@@ -185,21 +193,21 @@ def dbPictureToStacItem(dbPic):
|
|
|
185
193
|
"description": "Highest resolution available of this picture",
|
|
186
194
|
"roles": ["data"],
|
|
187
195
|
"type": "image/jpeg",
|
|
188
|
-
"href": _getHDJpgPictureURL(dbPic["id"],
|
|
196
|
+
"href": _getHDJpgPictureURL(dbPic["id"], visibility=dbPic.get("visibility")),
|
|
189
197
|
},
|
|
190
198
|
"sd": {
|
|
191
199
|
"title": "SD picture",
|
|
192
200
|
"description": "Picture in standard definition (fixed width of 2048px)",
|
|
193
201
|
"roles": ["visual"],
|
|
194
202
|
"type": "image/jpeg",
|
|
195
|
-
"href": _getSDJpgPictureURL(dbPic["id"],
|
|
203
|
+
"href": _getSDJpgPictureURL(dbPic["id"], visibility=dbPic.get("visibility")),
|
|
196
204
|
},
|
|
197
205
|
"thumb": {
|
|
198
206
|
"title": "Thumbnail",
|
|
199
207
|
"description": "Picture in low definition (fixed width of 500px)",
|
|
200
208
|
"roles": ["thumbnail"],
|
|
201
209
|
"type": "image/jpeg",
|
|
202
|
-
"href": _getThumbJpgPictureURL(dbPic["id"],
|
|
210
|
+
"href": _getThumbJpgPictureURL(dbPic["id"], visibility=dbPic.get("visibility")),
|
|
203
211
|
},
|
|
204
212
|
},
|
|
205
213
|
"collection": str(seqId),
|
|
@@ -277,7 +285,7 @@ def dbPictureToStacItem(dbPic):
|
|
|
277
285
|
"description": "Highest resolution available of this picture, as tiles",
|
|
278
286
|
"roles": ["data"],
|
|
279
287
|
"type": "image/jpeg",
|
|
280
|
-
"href": _getTilesJpgPictureURL(dbPic["id"],
|
|
288
|
+
"href": _getTilesJpgPictureURL(dbPic["id"], visibility=dbPic.get("visibility")),
|
|
281
289
|
}
|
|
282
290
|
}
|
|
283
291
|
|
|
@@ -366,9 +374,10 @@ def getCollectionItems(collectionId):
|
|
|
366
374
|
|
|
367
375
|
filters = [
|
|
368
376
|
SQL("sp.seq_id = %(seq)s"),
|
|
369
|
-
SQL("(p.
|
|
370
|
-
SQL("(is_sequence_visible_by_user(s, %(account)s))"),
|
|
377
|
+
SQL("(p.preparing_status = 'prepared' OR p.account_id = %(account)s)"),
|
|
371
378
|
]
|
|
379
|
+
if account is None or not account.can_see_all():
|
|
380
|
+
filters.append(SQL("(is_picture_visible_by_user(p, %(account)s))"))
|
|
372
381
|
|
|
373
382
|
# Check if limit is valid
|
|
374
383
|
sql_limit = SQL("")
|
|
@@ -407,7 +416,7 @@ def getCollectionItems(collectionId):
|
|
|
407
416
|
+ (", MAX(sp.rank) AS max_rank, MIN(sp.rank) AS min_rank " if paginated else "")
|
|
408
417
|
+ "FROM sequences s "
|
|
409
418
|
+ ("LEFT JOIN sequences_pictures sp ON sp.seq_id = s.id " if paginated else "")
|
|
410
|
-
+ "WHERE s.id = %(seq)s AND (is_sequence_visible_by_user(s, %(account)s)
|
|
419
|
+
+ "WHERE s.id = %(seq)s AND (s.status = 'ready' OR s.account_id = %(account)s) AND is_sequence_visible_by_user(s, %(account)s) AND s.status != 'deleted'"
|
|
411
420
|
+ ("GROUP BY s.id" if paginated else ""),
|
|
412
421
|
params,
|
|
413
422
|
).fetchone()
|
|
@@ -436,29 +445,36 @@ def getCollectionItems(collectionId):
|
|
|
436
445
|
params["start_after_rank"] = rank
|
|
437
446
|
|
|
438
447
|
query = SQL(
|
|
439
|
-
"""
|
|
440
|
-
|
|
441
|
-
p.id, p.ts, p.heading, p.metadata, p.inserted_at, p.updated_at, p.status,
|
|
448
|
+
"""SELECT
|
|
449
|
+
p.id, p.ts, p.heading, p.metadata, p.inserted_at, p.updated_at, p.status, p.visibility,
|
|
442
450
|
ST_AsGeoJSON(p.geom)::json AS geojson,
|
|
443
451
|
a.name AS account_name,
|
|
444
452
|
p.account_id AS account_id,
|
|
445
453
|
sp.seq_id, sp.rank, p.exif, p.gps_accuracy_m, p.h_pixel_density,
|
|
446
|
-
CASE WHEN LAG(p
|
|
447
|
-
CASE WHEN LAG(p
|
|
448
|
-
CASE WHEN LEAD(p
|
|
449
|
-
CASE WHEN LEAD(p
|
|
454
|
+
CASE WHEN LAG(is_picture_visible_by_user(p, %(account)s)) OVER othpics THEN LAG(p.id) OVER othpics END AS prevpic,
|
|
455
|
+
CASE WHEN LAG(is_picture_visible_by_user(p, %(account)s)) OVER othpics THEN ST_AsGeoJSON(LAG(p.geom) OVER othpics)::json END AS prevpicgeojson,
|
|
456
|
+
CASE WHEN LEAD(is_picture_visible_by_user(p, %(account)s)) OVER othpics THEN LEAD(p.id) OVER othpics END AS nextpic,
|
|
457
|
+
CASE WHEN LEAD(is_picture_visible_by_user(p, %(account)s)) OVER othpics THEN ST_AsGeoJSON(LEAD(p.geom) OVER othpics)::json END AS nextpicgeojson,
|
|
450
458
|
get_picture_semantics(p.id) as semantics,
|
|
451
|
-
get_picture_annotations(p.id) as annotations
|
|
459
|
+
get_picture_annotations(p.id) as annotations,
|
|
460
|
+
COALESCE(seq_sem.semantics, '[]'::json) AS sequence_semantics
|
|
452
461
|
FROM sequences_pictures sp
|
|
453
462
|
JOIN pictures p ON sp.pic_id = p.id
|
|
454
463
|
JOIN accounts a ON a.id = p.account_id
|
|
455
464
|
JOIN sequences s ON s.id = sp.seq_id
|
|
465
|
+
LEFT JOIN (
|
|
466
|
+
SELECT sequence_id, json_agg(json_strip_nulls(json_build_object(
|
|
467
|
+
'key', key,
|
|
468
|
+
'value', value
|
|
469
|
+
)) ORDER BY key, value) AS semantics
|
|
470
|
+
FROM sequences_semantics
|
|
471
|
+
GROUP BY sequence_id
|
|
472
|
+
) seq_sem ON seq_sem.sequence_id = s.id
|
|
456
473
|
WHERE
|
|
457
474
|
{filter}
|
|
458
475
|
WINDOW othpics AS (PARTITION BY sp.seq_id ORDER BY sp.rank)
|
|
459
476
|
ORDER BY rank
|
|
460
|
-
{limit}
|
|
461
|
-
"""
|
|
477
|
+
{limit}"""
|
|
462
478
|
).format(filter=SQL(" AND ").join(filters), limit=sql_limit)
|
|
463
479
|
|
|
464
480
|
records = cursor.execute(query, params)
|
|
@@ -578,20 +594,31 @@ def getCollectionItems(collectionId):
|
|
|
578
594
|
def _getPictureItemById(itemId: UUID, account: Optional[Account]):
|
|
579
595
|
"""Get a picture metadata by its ID"""
|
|
580
596
|
with current_app.pool.connection() as conn:
|
|
597
|
+
perm_filter = SQL("")
|
|
598
|
+
if account is not None and account.can_see_all():
|
|
599
|
+
# admins can see all pictures, regardless of their visibility
|
|
600
|
+
perm_filter = SQL("TRUE")
|
|
601
|
+
else:
|
|
602
|
+
perm_filter = SQL(
|
|
603
|
+
"""(p.account_id = %(acc)s OR p.status != 'hidden') -- for retrocompabitilty, we can drop this filter once database have migrated all hidden pictures
|
|
604
|
+
AND (is_picture_visible_by_user(p, %(acc)s))
|
|
605
|
+
AND (s.status != 'hidden' OR s.account_id = %(acc)s) -- same, we can drop this later (and replace it with `s.status = 'ready'`)
|
|
606
|
+
AND is_sequence_visible_by_user(s, %(acc)s)"""
|
|
607
|
+
)
|
|
608
|
+
|
|
581
609
|
with conn.cursor(row_factory=dict_row) as cursor:
|
|
582
|
-
# Check if there is a logged user
|
|
583
|
-
account = auth.get_current_account()
|
|
584
|
-
accountId = account.id if account else None
|
|
585
610
|
|
|
586
611
|
# Get rank + position of wanted picture
|
|
587
612
|
record = cursor.execute(
|
|
588
|
-
|
|
613
|
+
SQL(
|
|
614
|
+
"""WITH seq AS (
|
|
589
615
|
SELECT seq_id FROM sequences_pictures WHERE pic_id = %(pic)s LIMIT 1
|
|
590
616
|
)
|
|
591
617
|
SELECT
|
|
592
618
|
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,
|
|
594
|
-
p.account_id AS account_id,
|
|
619
|
+
p.inserted_at, p.updated_at, p.status,
|
|
620
|
+
accounts.name AS account_name, p.account_id AS account_id,
|
|
621
|
+
p.visibility,
|
|
595
622
|
spl.prevpic, spl.prevpicgeojson, spl.nextpic, spl.nextpicgeojson, p.exif,
|
|
596
623
|
relp.related_pics, p.gps_accuracy_m, p.h_pixel_density,
|
|
597
624
|
get_picture_semantics(p.id) as semantics,
|
|
@@ -612,7 +639,7 @@ def _getPictureItemById(itemId: UUID, account: Optional[Account]):
|
|
|
612
639
|
JOIN sequences_pictures sp ON p.id = sp.pic_id
|
|
613
640
|
WHERE
|
|
614
641
|
sp.seq_id IN (SELECT seq_id FROM seq)
|
|
615
|
-
AND (p
|
|
642
|
+
AND (is_picture_visible_by_user(p, %(acc)s) AND p.preparing_status = 'prepared')
|
|
616
643
|
WINDOW othpics AS (PARTITION BY sp.seq_id ORDER BY sp.rank)
|
|
617
644
|
) spl ON p.id = spl.id
|
|
618
645
|
LEFT JOIN (
|
|
@@ -662,11 +689,12 @@ def _getPictureItemById(itemId: UUID, account: Optional[Account]):
|
|
|
662
689
|
) seq_sem ON seq_sem.sequence_id = s.id
|
|
663
690
|
WHERE sp.seq_id IN (SELECT seq_id FROM seq)
|
|
664
691
|
AND p.id = %(pic)s
|
|
665
|
-
AND (p.account_id = %(acc)s OR p.
|
|
666
|
-
AND
|
|
692
|
+
-- TODO Should we show non prepared items to all ? AND (p.account_id = %(acc)s OR p.preparing_status = 'prepared')
|
|
693
|
+
AND {perm_filter}
|
|
667
694
|
AND s.status != 'deleted'
|
|
668
|
-
"""
|
|
669
|
-
|
|
695
|
+
"""
|
|
696
|
+
).format(perm_filter=perm_filter),
|
|
697
|
+
{"pic": itemId, "acc": account.id if account is not None else None},
|
|
670
698
|
).fetchone()
|
|
671
699
|
|
|
672
700
|
if record is None:
|
|
@@ -763,9 +791,12 @@ def searchItems():
|
|
|
763
791
|
|
|
764
792
|
account = auth.get_current_account()
|
|
765
793
|
accountId = account.id if account is not None else None
|
|
766
|
-
sqlWhere = [
|
|
794
|
+
sqlWhere = [
|
|
795
|
+
SQL("(p.status = 'ready' AND is_picture_visible_by_user(p, %(account)s))"),
|
|
796
|
+
SQL("(s.status = 'ready' AND is_sequence_visible_by_user(s, %(account)s))"),
|
|
797
|
+
]
|
|
767
798
|
sqlParams: Dict[str, Any] = {"account": accountId}
|
|
768
|
-
sqlSubQueryWhere = [SQL("(p.status = 'ready'
|
|
799
|
+
sqlSubQueryWhere = [SQL("(p.status = 'ready' AND is_picture_visible_by_user(p, %(account)s))")]
|
|
769
800
|
|
|
770
801
|
#
|
|
771
802
|
# Parameters parsing and verification
|
|
@@ -1172,7 +1203,7 @@ def postCollectionItem(collectionId, account=None):
|
|
|
1172
1203
|
raise errors.InvalidAPIUsage(_("The collection has been deleted, impossible to add pictures to it"), status_code=404)
|
|
1173
1204
|
|
|
1174
1205
|
# Compute various metadata
|
|
1175
|
-
accountId =
|
|
1206
|
+
accountId = accountOrDefault(account).id
|
|
1176
1207
|
raw_pic = picture.read()
|
|
1177
1208
|
filesize = len(raw_pic)
|
|
1178
1209
|
|
|
@@ -1217,7 +1248,7 @@ def postCollectionItem(collectionId, account=None):
|
|
|
1217
1248
|
|
|
1218
1249
|
# Return picture metadata
|
|
1219
1250
|
return (
|
|
1220
|
-
|
|
1251
|
+
_getPictureItemById(picId, account=account),
|
|
1221
1252
|
202,
|
|
1222
1253
|
{
|
|
1223
1254
|
"Content-Type": "application/json",
|
|
@@ -1233,7 +1264,21 @@ class PatchItemParameter(BaseModel):
|
|
|
1233
1264
|
heading: Optional[int] = None
|
|
1234
1265
|
"""Heading of the picture. The new heading will not be persisted in the picture's exif tags for the moment."""
|
|
1235
1266
|
visible: Optional[bool] = None
|
|
1236
|
-
"""Should the picture be publicly visible ?
|
|
1267
|
+
"""Should the picture be publicly visible ?
|
|
1268
|
+
|
|
1269
|
+
This parameter is deprecated in favor of the finer grained `visibility` parameter.
|
|
1270
|
+
`visible=true` is equivalent to `visibility=anyone`.
|
|
1271
|
+
`visible=false` is equivalent to `visibility=logged-only`.
|
|
1272
|
+
"""
|
|
1273
|
+
visibility: Optional[Visibility] = None
|
|
1274
|
+
"""Visibility of the sequence. Can be set to:
|
|
1275
|
+
* `anyone`: the sequence is visible to anyone
|
|
1276
|
+
* `owner-only`: the sequence is visible to the owner and administrator only
|
|
1277
|
+
* `logged-only`: the sequence is visible to logged users only
|
|
1278
|
+
|
|
1279
|
+
This visibility can also be set for each picture individually, using the `visibility` field of the pictures.
|
|
1280
|
+
If not set at the sequence level, it will default to the visibility of the `upload_set` and if not set the default visibility of the `account` and if not set the default visibility of the instance.
|
|
1281
|
+
"""
|
|
1237
1282
|
|
|
1238
1283
|
capture_time: Optional[datetime] = None
|
|
1239
1284
|
"""Capture time of the picture. The new capture time will not be persisted in the picture's exif tags for the moment."""
|
|
@@ -1301,13 +1346,28 @@ class PatchItemParameter(BaseModel):
|
|
|
1301
1346
|
raise errors.InvalidAPIUsage(_("Longitude cannot be overridden alone, latitude also needs to be set"))
|
|
1302
1347
|
if self.longitude is None and self.latitude is not None:
|
|
1303
1348
|
raise errors.InvalidAPIUsage(_("Latitude cannot be overridden alone, longitude also needs to be set"))
|
|
1349
|
+
if self.visibility is not None and self.visible is not None:
|
|
1350
|
+
raise errors.InvalidAPIUsage(_("Visibility and visible parameters are mutually exclusive parameters"))
|
|
1351
|
+
# handle retrocompatibility on the visible parameter
|
|
1352
|
+
if self.visible is not None:
|
|
1353
|
+
self.visibility = Visibility.anyone if self.visible is True else Visibility.owner_only
|
|
1304
1354
|
return self
|
|
1305
1355
|
|
|
1306
1356
|
def has_only_semantics_updates(self):
|
|
1307
1357
|
return self.model_fields_set == {"semantics"}
|
|
1308
1358
|
|
|
1359
|
+
@field_validator("visibility", mode="after")
|
|
1360
|
+
@classmethod
|
|
1361
|
+
def validate_visibility(cls, visibility):
|
|
1362
|
+
if not check_visibility(visibility):
|
|
1363
|
+
raise errors.InvalidAPIUsage(
|
|
1364
|
+
_("The logged-only visibility is not allowed on this instance since anybody can create an account"),
|
|
1365
|
+
status_code=400,
|
|
1366
|
+
)
|
|
1367
|
+
return visibility
|
|
1368
|
+
|
|
1309
1369
|
|
|
1310
|
-
def update_picture(itemId: UUID, account:
|
|
1370
|
+
def update_picture(itemId: UUID, account: Account):
|
|
1311
1371
|
# Parse received parameters
|
|
1312
1372
|
metadata = None
|
|
1313
1373
|
content_type = (request.headers.get("Content-Type") or "").split(";")[0]
|
|
@@ -1327,16 +1387,23 @@ def update_picture(itemId: UUID, account: Optional[Account]):
|
|
|
1327
1387
|
# Check if picture exists and if given account is authorized to edit
|
|
1328
1388
|
with db.conn(current_app) as conn:
|
|
1329
1389
|
with conn.transaction(), conn.cursor(row_factory=dict_row) as cursor:
|
|
1330
|
-
pic = cursor.execute(
|
|
1390
|
+
pic = cursor.execute(
|
|
1391
|
+
"""SELECT p.visibility, p.account_id
|
|
1392
|
+
FROM pictures p
|
|
1393
|
+
JOIN sequences_pictures sp ON sp.pic_id = p.id
|
|
1394
|
+
JOIN sequences s ON s.id = sp.seq_id
|
|
1395
|
+
WHERE p.id = %(id)s AND is_picture_visible_by_user(p, %(account)s) AND is_sequence_visible_by_user(s, %(account)s)""",
|
|
1396
|
+
{"id": itemId, "account": account.id},
|
|
1397
|
+
).fetchone()
|
|
1331
1398
|
|
|
1332
1399
|
# Picture not found
|
|
1333
1400
|
if not pic:
|
|
1334
1401
|
raise errors.InvalidAPIUsage(_("Picture %(p)s wasn't found in database", p=itemId), status_code=404)
|
|
1335
1402
|
|
|
1336
|
-
if
|
|
1403
|
+
if not account.can_edit_item(str(pic["account_id"])):
|
|
1337
1404
|
# Account associated to picture doesn't match current user
|
|
1338
1405
|
# and we limit the status change to only the owner.
|
|
1339
|
-
if metadata.
|
|
1406
|
+
if metadata.visibility is not None:
|
|
1340
1407
|
raise errors.InvalidAPIUsage(
|
|
1341
1408
|
_("You're not authorized to edit the visibility of this picture. Only the owner can change this."), status_code=403
|
|
1342
1409
|
)
|
|
@@ -1352,24 +1419,14 @@ def update_picture(itemId: UUID, account: Optional[Account]):
|
|
|
1352
1419
|
sqlParams = {"id": itemId, "account": account.id}
|
|
1353
1420
|
|
|
1354
1421
|
# Let's edit this picture
|
|
1355
|
-
|
|
1356
|
-
if oldStatus not in ["ready", "hidden"]:
|
|
1357
|
-
# Picture is in a preparing/broken/... state so no edit possible
|
|
1358
|
-
raise errors.InvalidAPIUsage(
|
|
1359
|
-
_(
|
|
1360
|
-
"Picture %(p)s is in %(s)s state, its visibility can't be changed for now",
|
|
1361
|
-
p=itemId,
|
|
1362
|
-
s=oldStatus,
|
|
1363
|
-
),
|
|
1364
|
-
status_code=400,
|
|
1365
|
-
)
|
|
1422
|
+
oldVisibility = pic["visibility"]
|
|
1366
1423
|
|
|
1367
|
-
|
|
1368
|
-
if metadata.
|
|
1369
|
-
|
|
1370
|
-
if
|
|
1371
|
-
sqlUpdates.append(SQL("
|
|
1372
|
-
sqlParams["
|
|
1424
|
+
newVisibility = None
|
|
1425
|
+
if metadata.visibility is not None:
|
|
1426
|
+
newVisibility = metadata.visibility.value
|
|
1427
|
+
if newVisibility != oldVisibility:
|
|
1428
|
+
sqlUpdates.append(SQL("visibility = %(visibility)s"))
|
|
1429
|
+
sqlParams["visibility"] = newVisibility
|
|
1373
1430
|
|
|
1374
1431
|
if metadata.heading is not None:
|
|
1375
1432
|
sqlUpdates.extend([SQL("heading = %(heading)s"), SQL("heading_computed = false")])
|
|
@@ -1457,12 +1514,12 @@ def patchCollectionItem(collectionId, itemId, account):
|
|
|
1457
1514
|
schema:
|
|
1458
1515
|
$ref: '#/components/schemas/GeoVisioItem'
|
|
1459
1516
|
"""
|
|
1460
|
-
return update_picture(itemId, account)
|
|
1517
|
+
return update_picture(itemId, account=account)
|
|
1461
1518
|
|
|
1462
1519
|
|
|
1463
1520
|
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>", methods=["DELETE"])
|
|
1464
1521
|
@auth.login_required()
|
|
1465
|
-
def deleteCollectionItem(collectionId, itemId, account):
|
|
1522
|
+
def deleteCollectionItem(collectionId: UUID, itemId: UUID, account: Account):
|
|
1466
1523
|
"""Delete an existing picture
|
|
1467
1524
|
---
|
|
1468
1525
|
tags:
|
|
@@ -1498,7 +1555,7 @@ def deleteCollectionItem(collectionId, itemId, account):
|
|
|
1498
1555
|
raise errors.InvalidAPIUsage(_("Picture %(p)s wasn't found in database", p=itemId), status_code=404)
|
|
1499
1556
|
|
|
1500
1557
|
# Account associated to picture doesn't match current user
|
|
1501
|
-
if
|
|
1558
|
+
if not account.can_edit_item(str(pic[1])):
|
|
1502
1559
|
raise errors.InvalidAPIUsage(_("You're not authorized to edit this picture"), status_code=403)
|
|
1503
1560
|
|
|
1504
1561
|
cursor.execute("DELETE FROM pictures WHERE id = %s", [itemId])
|
|
@@ -1509,29 +1566,29 @@ def deleteCollectionItem(collectionId, itemId, account):
|
|
|
1509
1566
|
return "", 204
|
|
1510
1567
|
|
|
1511
1568
|
|
|
1512
|
-
def _getHDJpgPictureURL(picId: str,
|
|
1569
|
+
def _getHDJpgPictureURL(picId: str, visibility: Optional[str]):
|
|
1513
1570
|
external_url = utils.pictures.getPublicHDPictureExternalUrl(picId, format="jpg")
|
|
1514
|
-
if external_url and
|
|
1571
|
+
if external_url and visibility == "anyone": # we always serve non public pictures through the API to be able to check permission:
|
|
1515
1572
|
return external_url
|
|
1516
1573
|
return url_for("pictures.getPictureHD", _external=True, pictureId=picId, format="jpg")
|
|
1517
1574
|
|
|
1518
1575
|
|
|
1519
|
-
def _getSDJpgPictureURL(picId: str,
|
|
1576
|
+
def _getSDJpgPictureURL(picId: str, visibility: Optional[str]):
|
|
1520
1577
|
external_url = utils.pictures.getPublicDerivatePictureExternalUrl(picId, format="jpg", derivateFileName="sd.jpg")
|
|
1521
|
-
if external_url and
|
|
1578
|
+
if external_url and visibility == "anyone": # we always serve non public pictures through the API to be able to check permission:
|
|
1522
1579
|
return external_url
|
|
1523
1580
|
return url_for("pictures.getPictureSD", _external=True, pictureId=picId, format="jpg")
|
|
1524
1581
|
|
|
1525
1582
|
|
|
1526
|
-
def _getThumbJpgPictureURL(picId: str,
|
|
1583
|
+
def _getThumbJpgPictureURL(picId: str, visibility: Optional[str]):
|
|
1527
1584
|
external_url = utils.pictures.getPublicDerivatePictureExternalUrl(picId, format="jpg", derivateFileName="thumb.jpg")
|
|
1528
|
-
if external_url and
|
|
1585
|
+
if external_url and visibility == "anyone": # we always serve non public pictures through the API to be able to check permission
|
|
1529
1586
|
return external_url
|
|
1530
1587
|
return url_for("pictures.getPictureThumb", _external=True, pictureId=picId, format="jpg")
|
|
1531
1588
|
|
|
1532
1589
|
|
|
1533
|
-
def _getTilesJpgPictureURL(picId: str,
|
|
1590
|
+
def _getTilesJpgPictureURL(picId: str, visibility: Optional[str]):
|
|
1534
1591
|
external_url = utils.pictures.getPublicDerivatePictureExternalUrl(picId, format="jpg", derivateFileName="tiles/{TileCol}_{TileRow}.jpg")
|
|
1535
|
-
if external_url and
|
|
1592
|
+
if external_url and visibility == "anyone": # we always serve non public pictures through the API to be able to check permission:
|
|
1536
1593
|
return external_url
|
|
1537
1594
|
return unquote(url_for("pictures.getPictureTile", _external=True, pictureId=picId, format="jpg", col="{TileCol}", row="{TileRow}"))
|