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/prepare.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from flask import current_app, request, url_for, Blueprint
|
|
2
|
+
from geovisio import errors
|
|
3
|
+
from geovisio.utils import auth
|
|
4
|
+
from psycopg.rows import dict_row
|
|
5
|
+
from psycopg.types.json import Jsonb
|
|
6
|
+
from psycopg.sql import SQL
|
|
7
|
+
from flask_babel import gettext as _
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, ValidationError
|
|
9
|
+
|
|
10
|
+
from geovisio.utils.params import validation_error
|
|
11
|
+
|
|
12
|
+
bp = Blueprint("prepare", __name__, url_prefix="/api")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PreparationParameter(BaseModel):
|
|
16
|
+
"""Parameters used control the behaviour of the preparation process"""
|
|
17
|
+
|
|
18
|
+
skip_blurring: bool = False
|
|
19
|
+
"""If true, the picture will not be blurred again"""
|
|
20
|
+
|
|
21
|
+
def as_sql(self):
|
|
22
|
+
return Jsonb({"skip_blurring": self.skip_blurring}) if self.skip_blurring else None
|
|
23
|
+
|
|
24
|
+
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/prepare", methods=["POST"])
|
|
28
|
+
def prepareItem(collectionId, itemId, account=None):
|
|
29
|
+
"""Ask for preparation of a picture. The picture will be blurred if needed, and derivates will be generated.
|
|
30
|
+
---
|
|
31
|
+
tags:
|
|
32
|
+
- Pictures
|
|
33
|
+
parameters:
|
|
34
|
+
- name: collectionId
|
|
35
|
+
in: path
|
|
36
|
+
description: ID of collection
|
|
37
|
+
required: true
|
|
38
|
+
schema:
|
|
39
|
+
type: string
|
|
40
|
+
- name: itemId
|
|
41
|
+
in: path
|
|
42
|
+
description: ID of item
|
|
43
|
+
required: true
|
|
44
|
+
schema:
|
|
45
|
+
type: string
|
|
46
|
+
requestBody:
|
|
47
|
+
content:
|
|
48
|
+
application/json:
|
|
49
|
+
schema:
|
|
50
|
+
$ref: '#/components/schemas/PreparationParameter'
|
|
51
|
+
responses:
|
|
52
|
+
202:
|
|
53
|
+
description: Empty response for the moment, but later we might return a way to track the progress of the preparation
|
|
54
|
+
content:
|
|
55
|
+
application/json:
|
|
56
|
+
schema:
|
|
57
|
+
type: object
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
params = PreparationParameter(**(request.json if request.is_json else {}))
|
|
61
|
+
except ValidationError as ve:
|
|
62
|
+
raise errors.InvalidAPIUsage(_("Impossible to parse parameters"), payload=validation_error(ve))
|
|
63
|
+
|
|
64
|
+
with current_app.pool.connection() as conn:
|
|
65
|
+
with conn.cursor(row_factory=dict_row) as cursor:
|
|
66
|
+
account = auth.get_current_account()
|
|
67
|
+
accountId = account.id if account else None
|
|
68
|
+
|
|
69
|
+
record = cursor.execute(
|
|
70
|
+
SQL(
|
|
71
|
+
"""SELECT 1
|
|
72
|
+
FROM pictures p
|
|
73
|
+
JOIN sequences_pictures sp ON p.id = sp.pic_id
|
|
74
|
+
WHERE
|
|
75
|
+
p.id = %(pic)s
|
|
76
|
+
AND sp.seq_id = %(seq)s
|
|
77
|
+
AND (p.account_id = %(acc)s OR p.status != 'hidden')"""
|
|
78
|
+
),
|
|
79
|
+
{"pic": itemId, "seq": collectionId, "acc": accountId},
|
|
80
|
+
).fetchone()
|
|
81
|
+
|
|
82
|
+
if not record:
|
|
83
|
+
raise errors.InvalidAPIUsage(
|
|
84
|
+
_("Picture %(p)s wasn't found in database", p=itemId),
|
|
85
|
+
status_code=404,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
cursor.execute(
|
|
89
|
+
SQL("INSERT INTO job_queue(picture_id, task, args) VALUES (%(pic)s, 'prepare', %(args)s)"),
|
|
90
|
+
{"pic": itemId, "args": params.as_sql()},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# run background task to prepare the picture
|
|
94
|
+
current_app.background_processor.process_pictures() # type: ignore
|
|
95
|
+
|
|
96
|
+
return {}, 202, {"Content-Type": "application/json"}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@bp.route("/collections/<uuid:collectionId>/prepare", methods=["POST"])
|
|
100
|
+
def prepareCollection(collectionId, account=None):
|
|
101
|
+
"""Ask for preparation of all the pictures of a collection. The pictures will be blurred if needed, and derivates will be generated.
|
|
102
|
+
---
|
|
103
|
+
tags:
|
|
104
|
+
- Sequences
|
|
105
|
+
parameters:
|
|
106
|
+
- name: collectionId
|
|
107
|
+
in: path
|
|
108
|
+
description: ID of collection
|
|
109
|
+
required: true
|
|
110
|
+
schema:
|
|
111
|
+
type: string
|
|
112
|
+
requestBody:
|
|
113
|
+
content:
|
|
114
|
+
application/json:
|
|
115
|
+
schema:
|
|
116
|
+
$ref: '#/components/schemas/PreparationParameter'
|
|
117
|
+
responses:
|
|
118
|
+
202:
|
|
119
|
+
description: Empty response for the moment, but later we might return a way to track the progress of the preparation
|
|
120
|
+
content:
|
|
121
|
+
application/json:
|
|
122
|
+
schema:
|
|
123
|
+
type: object
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
params = PreparationParameter(**(request.json if request.is_json else {}))
|
|
127
|
+
except ValidationError as ve:
|
|
128
|
+
raise errors.InvalidAPIUsage(_("Impossible to parse parameters"), payload=validation_error(ve))
|
|
129
|
+
|
|
130
|
+
with current_app.pool.connection() as conn:
|
|
131
|
+
with conn.cursor(row_factory=dict_row) as cursor:
|
|
132
|
+
account = auth.get_current_account()
|
|
133
|
+
accountId = account.id if account else None
|
|
134
|
+
|
|
135
|
+
record = cursor.execute(
|
|
136
|
+
SQL(
|
|
137
|
+
"""SELECT 1
|
|
138
|
+
FROM sequences
|
|
139
|
+
WHERE
|
|
140
|
+
id = %(seq)s
|
|
141
|
+
AND (account_id = %(acc)s OR status != 'hidden')"""
|
|
142
|
+
),
|
|
143
|
+
{"seq": collectionId, "acc": accountId},
|
|
144
|
+
).fetchone()
|
|
145
|
+
|
|
146
|
+
if not record:
|
|
147
|
+
raise errors.InvalidAPIUsage(
|
|
148
|
+
_("Collection %(c)s wasn't found in database", c=collectionId),
|
|
149
|
+
status_code=404,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
cursor.execute(
|
|
153
|
+
SQL(
|
|
154
|
+
"""INSERT INTO job_queue(picture_id, task, args)
|
|
155
|
+
SELECT pic_id, 'prepare', %(args)s
|
|
156
|
+
FROM sequences_pictures
|
|
157
|
+
WHERE seq_id = %(seq)s"""
|
|
158
|
+
),
|
|
159
|
+
{"seq": collectionId, "args": params.as_sql()},
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# run background task to prepare the picture
|
|
163
|
+
current_app.background_processor.process_pictures() # type: ignore
|
|
164
|
+
|
|
165
|
+
return {}, 202, {"Content-Type": "application/json"}
|
geovisio/web/stac.py
CHANGED
|
@@ -82,11 +82,11 @@ def getLanding():
|
|
|
82
82
|
if spatial_xmin is not None or temporal_min is not None
|
|
83
83
|
else None
|
|
84
84
|
)
|
|
85
|
-
|
|
85
|
+
apiSum = current_app.config["API_SUMMARY"]
|
|
86
86
|
catalog = dbSequencesToStacCatalog(
|
|
87
87
|
id="geovisio",
|
|
88
|
-
title=
|
|
89
|
-
description=
|
|
88
|
+
title=apiSum.name.get("en"),
|
|
89
|
+
description=apiSum.description.get("en"),
|
|
90
90
|
sequences=[],
|
|
91
91
|
request=request,
|
|
92
92
|
extent=extent,
|
|
@@ -112,7 +112,17 @@ def getLanding():
|
|
|
112
112
|
if "stac_extensions" not in catalog:
|
|
113
113
|
catalog["stac_extensions"] = []
|
|
114
114
|
|
|
115
|
-
catalog["stac_extensions"] += [
|
|
115
|
+
catalog["stac_extensions"] += [
|
|
116
|
+
"https://stac-extensions.github.io/web-map-links/v1.0.0/schema.json",
|
|
117
|
+
"https://stac-extensions.github.io/contacts/v0.1.1/schema.json",
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
catalog["contacts"] = [
|
|
121
|
+
{
|
|
122
|
+
"name": apiSum.name.get("en"),
|
|
123
|
+
"emails": [{"value": apiSum.email}],
|
|
124
|
+
},
|
|
125
|
+
]
|
|
116
126
|
|
|
117
127
|
catalog["links"] += cleanNoneInList(
|
|
118
128
|
[
|
|
@@ -299,10 +309,13 @@ def dbSequencesToStacCollection(id, title, description, sequences, request, exte
|
|
|
299
309
|
@auth.isUserIdMatchingCurrentAccount()
|
|
300
310
|
def getUserCatalog(userId, userIdMatchesAccount=False):
|
|
301
311
|
"""Retrieves an user list of sequences (catalog)
|
|
312
|
+
|
|
313
|
+
Note that this route is deprecated in favor of `/api/users/<uuid:userId>/collection`. This new route provides more information and offers more filtering and sorting options.
|
|
302
314
|
---
|
|
303
315
|
tags:
|
|
304
316
|
- Sequences
|
|
305
317
|
- Users
|
|
318
|
+
deprecated: true
|
|
306
319
|
parameters:
|
|
307
320
|
- name: userId
|
|
308
321
|
in: path
|
geovisio/web/tokens.py
CHANGED
|
@@ -7,7 +7,7 @@ from authlib.jose import jwt
|
|
|
7
7
|
from authlib.jose.errors import DecodeError
|
|
8
8
|
import logging
|
|
9
9
|
import uuid
|
|
10
|
-
from geovisio.utils import auth, db
|
|
10
|
+
from geovisio.utils import auth, db, website
|
|
11
11
|
from geovisio import errors, utils
|
|
12
12
|
|
|
13
13
|
|
|
@@ -222,9 +222,7 @@ def claim_non_associated_token(token_id, account):
|
|
|
222
222
|
"""
|
|
223
223
|
with db.cursor(current_app, row_factory=dict_row) as cursor:
|
|
224
224
|
token = cursor.execute(
|
|
225
|
-
""
|
|
226
|
-
SELECT account_id FROM tokens WHERE id = %(token)s
|
|
227
|
-
""",
|
|
225
|
+
"SELECT account_id FROM tokens WHERE id = %(token)s",
|
|
228
226
|
{"token": token_id},
|
|
229
227
|
).fetchone()
|
|
230
228
|
if not token:
|
|
@@ -241,6 +239,18 @@ def claim_non_associated_token(token_id, account):
|
|
|
241
239
|
"UPDATE tokens SET account_id = %(account)s WHERE id = %(token)s",
|
|
242
240
|
{"account": account.id, "token": token_id},
|
|
243
241
|
)
|
|
242
|
+
|
|
243
|
+
next_url = None
|
|
244
|
+
if account.tos_accepted is False and current_app.config["API_ENFORCE_TOS_ACCEPTANCE"]:
|
|
245
|
+
# if the tos have not been accepted, we redirect to the website page to accept it (with a redirect afterward to the token associated page)
|
|
246
|
+
next_url = current_app.config["API_WEBSITE_URL"].tos_validation_page({"next_url": "/token-accepted"})
|
|
247
|
+
else:
|
|
248
|
+
next_url = current_app.config["API_WEBSITE_URL"].cli_token_accepted_page()
|
|
249
|
+
|
|
250
|
+
if next_url:
|
|
251
|
+
# if there is an associated website, we redirect with a nice page explaining the token association
|
|
252
|
+
return flask.redirect(next_url)
|
|
253
|
+
# else we return a simple text to explain it
|
|
244
254
|
return "You are now logged in the CLI, you can upload your pictures", 200
|
|
245
255
|
|
|
246
256
|
|
geovisio/web/upload_set.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
1
2
|
from dataclasses import dataclass
|
|
2
3
|
|
|
3
4
|
import PIL
|
|
@@ -184,8 +185,7 @@ def getUploadSet(upload_set_id):
|
|
|
184
185
|
|
|
185
186
|
|
|
186
187
|
@bp.route("/upload_sets/<uuid:upload_set_id>/files", methods=["GET"])
|
|
187
|
-
|
|
188
|
-
def getUploadSetFiles(upload_set_id, account=None):
|
|
188
|
+
def getUploadSetFiles(upload_set_id):
|
|
189
189
|
"""List the files of an UploadSet
|
|
190
190
|
---
|
|
191
191
|
tags:
|
|
@@ -209,13 +209,20 @@ def getUploadSetFiles(upload_set_id, account=None):
|
|
|
209
209
|
schema:
|
|
210
210
|
$ref: '#/components/schemas/GeoVisioUploadSetFiles'
|
|
211
211
|
"""
|
|
212
|
+
account = utils.auth.get_current_account()
|
|
213
|
+
|
|
212
214
|
u = get_simple_upload_set(upload_set_id)
|
|
213
215
|
if u is None:
|
|
214
216
|
raise errors.InvalidAPIUsage(_("UploadSet doesn't exist"), status_code=404)
|
|
215
|
-
if account is not None and account.id != str(u.account_id):
|
|
216
|
-
raise errors.InvalidAPIUsage(_("You're not authorized to list pictures in this upload set"), status_code=403)
|
|
217
217
|
|
|
218
218
|
upload_set_files = get_upload_set_files(upload_set_id)
|
|
219
|
+
|
|
220
|
+
if account is None or account.id != str(u.account_id):
|
|
221
|
+
# if the user is not the owner of the upload set, we remove the picture_id since we might leak too many information
|
|
222
|
+
# not sure about this one, this could evolve in the future
|
|
223
|
+
for f in upload_set_files.files:
|
|
224
|
+
f.picture_id = None
|
|
225
|
+
|
|
219
226
|
return upload_set_files.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/json"}
|
|
220
227
|
|
|
221
228
|
|
|
@@ -529,9 +536,7 @@ def addFilesToUploadSet(upload_set_id: UUID, account=None):
|
|
|
529
536
|
with db.conn(current_app) as conn:
|
|
530
537
|
try:
|
|
531
538
|
with conn.transaction(), conn.cursor(row_factory=dict_row) as cursor:
|
|
532
|
-
upload_set = cursor.execute(
|
|
533
|
-
"SELECT id, account_id, completed FROM upload_sets WHERE id = %s AND not deleted", [upload_set_id]
|
|
534
|
-
).fetchone()
|
|
539
|
+
upload_set = cursor.execute("SELECT id, account_id, completed FROM upload_sets WHERE id = %s", [upload_set_id]).fetchone()
|
|
535
540
|
if not upload_set:
|
|
536
541
|
raise errors.InvalidAPIUsage(_("UploadSet %(u)s does not exist", u=upload_set_id), status_code=404)
|
|
537
542
|
|
|
@@ -586,7 +591,7 @@ def addFilesToUploadSet(upload_set_id: UUID, account=None):
|
|
|
586
591
|
raise TrackedFileException(
|
|
587
592
|
_("The same picture has already been sent in a past upload"),
|
|
588
593
|
payload={"upload_sets": same_pics},
|
|
589
|
-
rejection_status=FileRejectionStatus.
|
|
594
|
+
rejection_status=FileRejectionStatus.file_duplicate,
|
|
590
595
|
status_code=409,
|
|
591
596
|
file=file,
|
|
592
597
|
)
|
|
@@ -613,7 +618,7 @@ def addFilesToUploadSet(upload_set_id: UUID, account=None):
|
|
|
613
618
|
except utils.pictures.MetadataReadingError as e:
|
|
614
619
|
raise TrackedFileException(
|
|
615
620
|
_("Impossible to parse picture metadata"),
|
|
616
|
-
payload={"details": {"error": e.details}},
|
|
621
|
+
payload={"details": {"error": e.details, "missing_fields": e.missing_mandatory_tags}},
|
|
617
622
|
rejection_status=FileRejectionStatus.invalid_metadata,
|
|
618
623
|
file=file,
|
|
619
624
|
)
|
|
@@ -652,14 +657,18 @@ def addFilesToUploadSet(upload_set_id: UUID, account=None):
|
|
|
652
657
|
# something went wrong, we reject the file, but keep track of it
|
|
653
658
|
with conn.transaction(), conn.cursor(row_factory=dict_row) as cursor:
|
|
654
659
|
msg = e.message
|
|
660
|
+
d = None
|
|
655
661
|
if e.payload and e.payload.get("details", {}).get("error") is not None:
|
|
656
|
-
|
|
662
|
+
d = deepcopy(e.payload["details"])
|
|
663
|
+
msg = d.pop("error")
|
|
664
|
+
|
|
657
665
|
utils.upload_set.insertFileInDatabase(
|
|
658
666
|
cursor=cursor,
|
|
659
667
|
upload_set_id=upload_set_id,
|
|
660
668
|
**e.file,
|
|
661
669
|
rejection_status=e.rejection_status,
|
|
662
670
|
rejection_message=msg,
|
|
671
|
+
rejection_details=d,
|
|
663
672
|
)
|
|
664
673
|
handle_completion(cursor, upload_set)
|
|
665
674
|
raise e
|
geovisio/web/users.py
CHANGED
|
@@ -1,43 +1,64 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from uuid import UUID
|
|
1
3
|
import flask
|
|
2
|
-
from flask import request, current_app, url_for
|
|
4
|
+
from flask import redirect, request, current_app, session, url_for
|
|
3
5
|
from flask_babel import gettext as _
|
|
6
|
+
from pydantic import BaseModel, Field, ValidationError, computed_field
|
|
4
7
|
from geovisio.utils import auth, db
|
|
5
8
|
from geovisio import errors
|
|
6
|
-
from psycopg.rows import dict_row
|
|
9
|
+
from psycopg.rows import dict_row, class_row
|
|
7
10
|
from psycopg.sql import SQL
|
|
8
11
|
|
|
12
|
+
from geovisio.utils.link import Link, make_link
|
|
13
|
+
from geovisio.utils.model_query import get_db_params_and_values
|
|
14
|
+
from geovisio.utils.params import validation_error
|
|
9
15
|
from geovisio.web import stac
|
|
16
|
+
from geovisio.web.auth import NEXT_URL_KEY
|
|
10
17
|
from geovisio.web.utils import get_root_link
|
|
11
18
|
|
|
12
19
|
bp = flask.Blueprint("user", __name__, url_prefix="/api/users")
|
|
13
20
|
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
22
|
+
class UserInfo(BaseModel):
|
|
23
|
+
name: str
|
|
24
|
+
"""Name of the user"""
|
|
25
|
+
id: UUID
|
|
26
|
+
"""Unique identifier of the user"""
|
|
27
|
+
collaborative_metadata: Optional[bool] = None
|
|
28
|
+
"""If true, the user can edit the metadata of all sequences. If unset, default to the instance's default configuration."""
|
|
29
|
+
|
|
30
|
+
tos_accepted: Optional[bool] = None
|
|
31
|
+
"""True means the user has accepted the terms of service (tos). Can only be seen by the user itself"""
|
|
32
|
+
|
|
33
|
+
@computed_field
|
|
34
|
+
@property
|
|
35
|
+
def links(self) -> List[Link]:
|
|
36
|
+
userMapUrl = (
|
|
37
|
+
flask.url_for("map.getUserTile", userId=self.id, x="11111111", y="22222222", z="33333333", format="mvt", _external=True)
|
|
38
|
+
.replace("11111111", "{x}")
|
|
39
|
+
.replace("22222222", "{y}")
|
|
40
|
+
.replace("33333333", "{z}")
|
|
41
|
+
)
|
|
42
|
+
return [
|
|
43
|
+
make_link(rel="catalog", route="stac.getUserCatalog", userId=self.id),
|
|
44
|
+
make_link(rel="collection", route="stac_collections.getUserCollection", userId=self.id),
|
|
45
|
+
Link(
|
|
46
|
+
rel="user-xyz",
|
|
47
|
+
type="application/vnd.mapbox-vector-tile",
|
|
48
|
+
title="Pictures and sequences vector tiles for a given user",
|
|
49
|
+
href=userMapUrl,
|
|
50
|
+
),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_user_info(account: auth.Account):
|
|
55
|
+
user_info = UserInfo(id=account.id, name=account.name, collaborative_metadata=account.collaborative_metadata)
|
|
56
|
+
logged_account = auth.get_current_account()
|
|
57
|
+
if logged_account is not None and account.id == logged_account.id:
|
|
58
|
+
# we show the term of service acceptance only if the user is the logged user
|
|
59
|
+
user_info.tos_accepted = account.tos_accepted
|
|
60
|
+
|
|
61
|
+
return user_info.model_dump(exclude_unset=True), 200, {"Content-Type": "application/json"}
|
|
41
62
|
|
|
42
63
|
|
|
43
64
|
@bp.route("/me")
|
|
@@ -55,7 +76,7 @@ def getMyUserInfo(account):
|
|
|
55
76
|
schema:
|
|
56
77
|
$ref: '#/components/schemas/GeoVisioUser'
|
|
57
78
|
"""
|
|
58
|
-
return _get_user_info(account
|
|
79
|
+
return _get_user_info(account)
|
|
59
80
|
|
|
60
81
|
|
|
61
82
|
@bp.route("/<uuid:userId>")
|
|
@@ -79,21 +100,29 @@ def getUserInfo(userId):
|
|
|
79
100
|
schema:
|
|
80
101
|
$ref: '#/components/schemas/GeoVisioUser'
|
|
81
102
|
"""
|
|
82
|
-
account = db.fetchone(
|
|
103
|
+
account = db.fetchone(
|
|
104
|
+
current_app,
|
|
105
|
+
SQL("SELECT name, id::text, collaborative_metadata, role, tos_accepted FROM accounts WHERE id = %s"),
|
|
106
|
+
[userId],
|
|
107
|
+
row_factory=class_row(auth.Account),
|
|
108
|
+
)
|
|
83
109
|
if not account:
|
|
84
110
|
raise errors.InvalidAPIUsage(_("Impossible to find user"), status_code=404)
|
|
85
111
|
|
|
86
|
-
return _get_user_info(account
|
|
112
|
+
return _get_user_info(account)
|
|
87
113
|
|
|
88
114
|
|
|
89
115
|
@bp.route("/me/catalog")
|
|
90
116
|
@auth.login_required_with_redirect()
|
|
91
117
|
def getMyCatalog(account):
|
|
92
|
-
"""Get current logged user catalog
|
|
118
|
+
"""Get current logged user catalog.
|
|
119
|
+
|
|
120
|
+
Note that this route is deprecated in favor of `/api/users/me/collection`. This new route provides more information and offers more filtering and sorting options.
|
|
93
121
|
---
|
|
94
122
|
tags:
|
|
95
123
|
- Users
|
|
96
124
|
- Sequences
|
|
125
|
+
deprecated: true
|
|
97
126
|
responses:
|
|
98
127
|
200:
|
|
99
128
|
description: the Catalog listing all sequences associated to given user. Note that it's similar to the user's colletion, but with less metadata since a STAC collection is an enhanced STAC catalog.
|
|
@@ -102,18 +131,37 @@ def getMyCatalog(account):
|
|
|
102
131
|
schema:
|
|
103
132
|
$ref: '#/components/schemas/GeoVisioCatalog'
|
|
104
133
|
"""
|
|
105
|
-
return flask.redirect(
|
|
134
|
+
return flask.redirect(
|
|
135
|
+
flask.url_for(
|
|
136
|
+
"stac.getUserCatalog",
|
|
137
|
+
userId=account.id,
|
|
138
|
+
limit=request.args.get("limit"),
|
|
139
|
+
page=request.args.get("page"),
|
|
140
|
+
_external=True,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
106
143
|
|
|
107
144
|
|
|
108
145
|
@bp.route("/me/collection")
|
|
109
146
|
@auth.login_required_with_redirect()
|
|
110
147
|
def getMyCollection(account):
|
|
111
148
|
"""Get current logged user collection
|
|
149
|
+
|
|
150
|
+
Note that the result can also be a CSV file, if the "Accept" header is set to "text/csv", or if the "format" query parameter is set to "csv".
|
|
151
|
+
|
|
112
152
|
---
|
|
113
153
|
tags:
|
|
114
154
|
- Users
|
|
115
155
|
- Sequences
|
|
116
156
|
parameters:
|
|
157
|
+
- name: format
|
|
158
|
+
in: query
|
|
159
|
+
description: Expected output format (STAC JSON or a csv file)
|
|
160
|
+
required: false
|
|
161
|
+
schema:
|
|
162
|
+
type: string
|
|
163
|
+
enum: [csv, json]
|
|
164
|
+
default: json
|
|
117
165
|
- $ref: '#/components/parameters/STAC_collections_limit'
|
|
118
166
|
- $ref: '#/components/parameters/STAC_collections_filter'
|
|
119
167
|
- $ref: '#/components/parameters/STAC_bbox'
|
|
@@ -125,19 +173,14 @@ def getMyCollection(account):
|
|
|
125
173
|
application/json:
|
|
126
174
|
schema:
|
|
127
175
|
$ref: '#/components/schemas/GeoVisioCollectionOfCollection'
|
|
176
|
+
|
|
177
|
+
text/csv:
|
|
178
|
+
schema:
|
|
179
|
+
$ref: '#/components/schemas/GeoVisioCSVCollections'
|
|
128
180
|
"""
|
|
181
|
+
from geovisio.web.collections import getUserCollection
|
|
129
182
|
|
|
130
|
-
return
|
|
131
|
-
flask.url_for(
|
|
132
|
-
"stac_collections.getUserCollection",
|
|
133
|
-
userId=account.id,
|
|
134
|
-
filter=request.args.get("filter"),
|
|
135
|
-
limit=request.args.get("limit"),
|
|
136
|
-
sortby=request.args.get("sortby"),
|
|
137
|
-
bbox=request.args.get("bbox"),
|
|
138
|
-
_external=True,
|
|
139
|
-
)
|
|
140
|
-
)
|
|
183
|
+
return getUserCollection(userId=account.id, userIdMatchesAccount=True)
|
|
141
184
|
|
|
142
185
|
|
|
143
186
|
@bp.route("/search")
|
|
@@ -272,3 +315,92 @@ LIMIT {limit};"""
|
|
|
272
315
|
if r["has_seq"]
|
|
273
316
|
],
|
|
274
317
|
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class UserConfiguration(BaseModel):
|
|
321
|
+
collaborative_metadata: Optional[bool] = None
|
|
322
|
+
"""If true, all sequences's metadata will be, by default, editable by all users.
|
|
323
|
+
|
|
324
|
+
If not set, it will default to the instance default collaborative editing policy."""
|
|
325
|
+
|
|
326
|
+
def has_override(self) -> bool:
|
|
327
|
+
return bool(self.model_fields_set)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@bp.route("/me", methods=["PATCH"])
|
|
331
|
+
@auth.login_required()
|
|
332
|
+
def patchUserConfiguration(account):
|
|
333
|
+
"""Edit the current user configuration
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
tags:
|
|
337
|
+
- Users
|
|
338
|
+
requestBody:
|
|
339
|
+
content:
|
|
340
|
+
application/json:
|
|
341
|
+
schema:
|
|
342
|
+
$ref: '#/components/schemas/GeoVisioUserConfiguration'
|
|
343
|
+
security:
|
|
344
|
+
- bearerToken: []
|
|
345
|
+
- cookieAuth: []
|
|
346
|
+
responses:
|
|
347
|
+
200:
|
|
348
|
+
description: the user configuration
|
|
349
|
+
content:
|
|
350
|
+
application/json:
|
|
351
|
+
schema:
|
|
352
|
+
$ref: '#/components/schemas/GeoVisioUser'
|
|
353
|
+
"""
|
|
354
|
+
metadata = None
|
|
355
|
+
try:
|
|
356
|
+
if request.is_json and request.json:
|
|
357
|
+
metadata = UserConfiguration(**request.json)
|
|
358
|
+
except ValidationError as ve:
|
|
359
|
+
raise errors.InvalidAPIUsage(_("Impossible to parse parameters"), payload=validation_error(ve))
|
|
360
|
+
|
|
361
|
+
if not metadata:
|
|
362
|
+
return _get_user_info(account)
|
|
363
|
+
params = get_db_params_and_values(metadata)
|
|
364
|
+
if metadata.has_override():
|
|
365
|
+
|
|
366
|
+
fields = params.fields_for_set_list()
|
|
367
|
+
|
|
368
|
+
account = db.fetchone(
|
|
369
|
+
current_app,
|
|
370
|
+
SQL("UPDATE accounts SET {fields} WHERE id = %(account_id)s RETURNING *").format(fields=SQL(", ").join(fields)),
|
|
371
|
+
params.params_as_dict | {"account_id": account.id},
|
|
372
|
+
row_factory=class_row(auth.Account),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
return _get_user_info(account)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@bp.route("/me/accept_tos", methods=["POST"])
|
|
379
|
+
@auth.login_required()
|
|
380
|
+
def accept_tos(account: auth.Account):
|
|
381
|
+
"""
|
|
382
|
+
Accept the terms of service for the current user
|
|
383
|
+
---
|
|
384
|
+
tags:
|
|
385
|
+
- Auth
|
|
386
|
+
responses:
|
|
387
|
+
200:
|
|
388
|
+
description: the user configuration
|
|
389
|
+
content:
|
|
390
|
+
application/json:
|
|
391
|
+
schema:
|
|
392
|
+
$ref: '#/components/schemas/GeoVisioUser'
|
|
393
|
+
"""
|
|
394
|
+
# Note: accepting twice does not change the accepted_at date
|
|
395
|
+
account = db.fetchone(
|
|
396
|
+
current_app,
|
|
397
|
+
SQL("UPDATE accounts SET tos_accepted_at = COALESCE(tos_accepted_at, NOW()) WHERE id = %(account_id)s RETURNING *"),
|
|
398
|
+
{"account_id": account.id},
|
|
399
|
+
row_factory=class_row(auth.Account),
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# we persist in the cookie the fact that the tos have been accepted
|
|
403
|
+
session[auth.ACCOUNT_KEY] = account.model_dump(exclude_none=True)
|
|
404
|
+
session.permanent = True
|
|
405
|
+
|
|
406
|
+
return _get_user_info(account)
|