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
@@ -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=current_app.config["API_SUMMARY"].name.get("en"),
89
- description=current_app.config["API_SUMMARY"].description.get("en"),
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"] += ["https://stac-extensions.github.io/web-map-links/v1.0.0/schema.json"]
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
 
@@ -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
- @auth.login_required_by_setting("API_FORCE_AUTH_ON_UPLOAD")
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.invalid_metadata,
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
- msg = e.payload["details"]["error"]
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
- def _get_user_info(id, name):
16
- userMapUrl = (
17
- flask.url_for("map.getUserTile", userId=id, x="11111111", y="22222222", z="33333333", format="mvt", _external=True)
18
- .replace("11111111", "{x}")
19
- .replace("22222222", "{y}")
20
- .replace("33333333", "{z}")
21
- )
22
- response = {
23
- "id": id,
24
- "name": name,
25
- "links": [
26
- {"rel": "catalog", "type": "application/json", "href": flask.url_for("stac.getUserCatalog", userId=id, _external=True)},
27
- {
28
- "rel": "collection",
29
- "type": "application/json",
30
- "href": flask.url_for("stac_collections.getUserCollection", userId=id, _external=True),
31
- },
32
- {
33
- "rel": "user-xyz",
34
- "type": "application/vnd.mapbox-vector-tile",
35
- "href": userMapUrl,
36
- "title": "Pictures and sequences vector tiles for a given user",
37
- },
38
- ],
39
- }
40
- return flask.jsonify(response)
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.id, account.name)
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(current_app, SQL("SELECT name, id FROM accounts WHERE id = %s"), [userId], row_factory=dict_row)
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["id"], account["name"])
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(flask.url_for("stac.getUserCatalog", userId=account.id, _external=True))
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 flask.redirect(
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)