geovisio 2.7.1__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 (60) hide show
  1. geovisio/__init__.py +10 -2
  2. geovisio/admin_cli/__init__.py +3 -1
  3. geovisio/admin_cli/user.py +75 -0
  4. geovisio/config_app.py +87 -4
  5. geovisio/templates/main.html +2 -2
  6. geovisio/templates/viewer.html +3 -3
  7. geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
  8. geovisio/translations/da/LC_MESSAGES/messages.po +850 -0
  9. geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
  10. geovisio/translations/de/LC_MESSAGES/messages.po +97 -1
  11. geovisio/translations/el/LC_MESSAGES/messages.mo +0 -0
  12. geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
  13. geovisio/translations/en/LC_MESSAGES/messages.po +210 -127
  14. geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
  15. geovisio/translations/eo/LC_MESSAGES/messages.po +790 -0
  16. geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
  17. geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
  18. geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
  19. geovisio/translations/fr/LC_MESSAGES/messages.po +39 -2
  20. geovisio/translations/hu/LC_MESSAGES/messages.mo +0 -0
  21. geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
  22. geovisio/translations/it/LC_MESSAGES/messages.po +875 -0
  23. geovisio/translations/ja/LC_MESSAGES/messages.mo +0 -0
  24. geovisio/translations/ja/LC_MESSAGES/messages.po +719 -0
  25. geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
  26. geovisio/translations/messages.pot +191 -122
  27. geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
  28. geovisio/translations/pl/LC_MESSAGES/messages.mo +0 -0
  29. geovisio/translations/pl/LC_MESSAGES/messages.po +727 -0
  30. geovisio/translations/zh_Hant/LC_MESSAGES/messages.mo +0 -0
  31. geovisio/translations/zh_Hant/LC_MESSAGES/messages.po +719 -0
  32. geovisio/utils/auth.py +80 -8
  33. geovisio/utils/link.py +3 -2
  34. geovisio/utils/model_query.py +55 -0
  35. geovisio/utils/pictures.py +12 -43
  36. geovisio/utils/semantics.py +120 -0
  37. geovisio/utils/sequences.py +10 -1
  38. geovisio/utils/tokens.py +5 -3
  39. geovisio/utils/upload_set.py +50 -15
  40. geovisio/utils/website.py +50 -0
  41. geovisio/web/annotations.py +17 -0
  42. geovisio/web/auth.py +9 -5
  43. geovisio/web/collections.py +217 -61
  44. geovisio/web/configuration.py +17 -1
  45. geovisio/web/docs.py +64 -53
  46. geovisio/web/items.py +220 -96
  47. geovisio/web/map.py +48 -18
  48. geovisio/web/pages.py +240 -0
  49. geovisio/web/params.py +17 -0
  50. geovisio/web/prepare.py +165 -0
  51. geovisio/web/stac.py +17 -4
  52. geovisio/web/tokens.py +14 -4
  53. geovisio/web/upload_set.py +10 -4
  54. geovisio/web/users.py +176 -44
  55. geovisio/workers/runner_pictures.py +61 -22
  56. {geovisio-2.7.1.dist-info → geovisio-2.8.0.dist-info}/METADATA +5 -4
  57. geovisio-2.8.0.dist-info/RECORD +89 -0
  58. geovisio-2.7.1.dist-info/RECORD +0 -70
  59. {geovisio-2.7.1.dist-info → geovisio-2.8.0.dist-info}/LICENSE +0 -0
  60. {geovisio-2.7.1.dist-info → geovisio-2.8.0.dist-info}/WHEEL +0 -0
geovisio/web/map.py CHANGED
@@ -215,10 +215,21 @@ def getStyle():
215
215
  def getTile(z: int, x: int, y: int, format: str):
216
216
  """Get pictures and sequences as vector tiles
217
217
 
218
- Vector tiles contains different layers based on zoom level : sequences, pictures or grid.
218
+ Vector tiles contains different layers based on zoom level : grid, sequences or pictures.
219
+
220
+ Layer "grid":
221
+ - Available on zoom levels 0 to 7 (excluded)
222
+ - Available properties:
223
+ - id
224
+ - nb_pictures
225
+ - nb_360_pictures (number of 360° pictures)
226
+ - nb_flat_pictures (number of flat pictures)
227
+ - coef (value from 0 to 1, relative quantity of available pictures)
228
+ - coef_360_pictures (value from 0 to 1, relative quantity of available 360° pictures)
229
+ - coef_flat_pictures (value from 0 to 1, relative quantity of available flat pictures)
219
230
 
220
231
  Layer "sequences":
221
- - Available on zoom levels >= 6
232
+ - Available on zoom levels >= 7 (and simplified version on zoom >= 6 and < 7)
222
233
  - Available properties:
223
234
  - id (sequence ID)
224
235
  - account_id
@@ -229,28 +240,19 @@ def getTile(z: int, x: int, y: int, format: str):
229
240
  - h_pixel_density (number of pixels on horizon per field of view degree)
230
241
 
231
242
  Layer "pictures":
232
- - Available on zoom levels >= 13
243
+ - Available on zoom levels >= 15
233
244
  - Available properties:
234
245
  - id (picture ID)
235
246
  - account_id
236
247
  - ts (picture date/time)
237
248
  - heading (picture heading in degrees)
238
- - sequences (list of sequences ID this pictures belongs to)
239
249
  - type (flat or equirectangular)
250
+ - hidden (picture visibility, true or false)
240
251
  - model (camera make and model)
241
252
  - gps_accuracy (95% confidence interval of GPS position precision, in meters)
242
253
  - h_pixel_density (number of pixels on horizon per field of view degree)
243
-
244
- Layer "grid":
245
- - Available on zoom levels 0 to 5 (included)
246
- - Available properties:
247
- - id
248
- - nb_pictures
249
- - nb_360_pictures (number of 360° pictures)
250
- - nb_flat_pictures (number of flat pictures)
251
- - coef (value from 0 to 1, relative quantity of available pictures)
252
- - coef_360_pictures (value from 0 to 1, relative quantity of available 360° pictures)
253
- - coef_flat_pictures (value from 0 to 1, relative quantity of available flat pictures)
254
+ - sequences (list of sequences ID this pictures belongs to)
255
+ - first_sequence (sequence ID, first from the list)
254
256
 
255
257
  ---
256
258
  tags:
@@ -260,7 +262,7 @@ def getTile(z: int, x: int, y: int, format: str):
260
262
  parameters:
261
263
  - name: z
262
264
  in: path
263
- description: Zoom level (6 to 14)
265
+ description: Zoom level (6 to 15)
264
266
  required: true
265
267
  schema:
266
268
  type: number
@@ -421,7 +423,8 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
421
423
  TRIM(CONCAT(p.metadata->>'make', ' ', p.metadata->>'model')) AS model,
422
424
  gps_accuracy_m AS gps_accuracy,
423
425
  h_pixel_density,
424
- array_to_json(ARRAY_AGG(sp.seq_id)) AS sequences
426
+ array_to_json(ARRAY_AGG(sp.seq_id)) AS sequences,
427
+ MIN(sp.seq_id::varchar) AS first_sequence
425
428
  FROM pictures p
426
429
  LEFT JOIN sequences_pictures sp ON p.id = sp.pic_id
427
430
  LEFT JOIN sequences s ON s.id = sp.seq_id
@@ -536,7 +539,34 @@ def getUserStyle(userId: UUID):
536
539
  @user_dependant_response(True)
537
540
  def getUserTile(userId: UUID, z: int, x: int, y: int, format: str):
538
541
  """Get pictures and sequences as vector tiles for a specific user.
539
- This tile will contain the same layers as the generic tiles (from `/map/z/x/y.format` route), but with sequences properties on all levels
542
+
543
+ Vector tiles contains different layers based on zoom level : sequences, lowzoom_360pictures or pictures.
544
+
545
+ Layer "sequences":
546
+ - Available on all zoom levels
547
+ - Available properties:
548
+ - id (sequence ID)
549
+ - account_id
550
+ - model (camera make and model)
551
+ - type (flat or equirectangular)
552
+ - date (capture date, as YYYY-MM-DD)
553
+ - gps_accuracy (95% confidence interval of GPS position precision, in meters)
554
+ - h_pixel_density (number of pixels on horizon per field of view degree)
555
+
556
+ Layer "pictures":
557
+ - Available on zoom levels >= 15
558
+ - Available properties:
559
+ - id (picture ID)
560
+ - account_id
561
+ - ts (picture date/time)
562
+ - heading (picture heading in degrees)
563
+ - type (flat or equirectangular)
564
+ - hidden (picture visibility, true or false)
565
+ - model (camera make and model)
566
+ - gps_accuracy (95% confidence interval of GPS position precision, in meters)
567
+ - h_pixel_density (number of pixels on horizon per field of view degree)
568
+ - sequences (list of sequences ID this pictures belongs to)
569
+ - first_sequence (sequence ID, first from the list)
540
570
 
541
571
  ---
542
572
  tags:
geovisio/web/pages.py ADDED
@@ -0,0 +1,240 @@
1
+ from flask import current_app, request, url_for, Blueprint
2
+ from pydantic import BaseModel, ConfigDict
3
+ from enum import Enum
4
+ from typing import List
5
+ from geovisio.utils import db, auth
6
+ from geovisio.utils.link import Link, make_link
7
+ from geovisio.errors import InvalidAPIUsage
8
+ from flask_babel import gettext as _
9
+ from psycopg.sql import SQL
10
+
11
+ bp = Blueprint("pages", __name__, url_prefix="/api")
12
+
13
+
14
+ class PageName(Enum):
15
+ end_user_license_agreement = "end-user-license-agreement"
16
+ terms_of_service = "terms-of-service"
17
+ end_user_license_agreement_summary = "end-user-license-agreement-summary"
18
+
19
+
20
+ class PageLanguage(BaseModel):
21
+ """A specific language for the page"""
22
+
23
+ language: str
24
+ """The language (as ISO 639-2 code)"""
25
+
26
+ links: List[Link]
27
+ """Link to page content"""
28
+
29
+
30
+ class PageSummary(BaseModel):
31
+ """Page summary"""
32
+
33
+ name: PageName
34
+ """Page name"""
35
+ languages: List[PageLanguage]
36
+ """Available translations"""
37
+
38
+ model_config = ConfigDict(use_attribute_docstrings=True)
39
+
40
+
41
+ def check_page_name(v: str) -> PageName:
42
+ try:
43
+ return PageName(v)
44
+ except ValueError:
45
+ raise InvalidAPIUsage(_("Page name is not recognized"), status_code=400)
46
+
47
+
48
+ @bp.route("/pages/<page>", methods=["GET"])
49
+ def getPageLanguages(page):
50
+ """List available languages for a single page
51
+ ---
52
+ tags:
53
+ - Configuration
54
+ parameters:
55
+ - name: page
56
+ in: path
57
+ description: Page name
58
+ required: true
59
+ schema:
60
+ $ref: '#/components/schemas/GeoVisioPageName'
61
+ responses:
62
+ 200:
63
+ description: the languages list
64
+ content:
65
+ application/json:
66
+ schema:
67
+ $ref: '#/components/schemas/GeoVisioPageSummary'
68
+ """
69
+
70
+ name = check_page_name(page)
71
+ langs = [d[0] for d in db.fetchall(current_app, SQL("SELECT lang FROM pages WHERE name = %(name)s"), {"name": name.value})]
72
+
73
+ # If page doesn't exist yet, send empty list of languages
74
+ if langs is None or len(langs) == 0:
75
+ langs = []
76
+
77
+ summary = PageSummary(
78
+ name=name,
79
+ languages=[PageLanguage(language=l, links=[make_link(rel="self", route="pages.getPage", page=name.value, lang=l)]) for l in langs],
80
+ )
81
+
82
+ return (
83
+ summary.model_dump_json(exclude_none=True),
84
+ 200,
85
+ {
86
+ "Content-Type": "application/json",
87
+ },
88
+ )
89
+
90
+
91
+ @bp.route("/pages/<page>/<lang>", methods=["GET"])
92
+ def getPage(page, lang):
93
+ """Get page HTML content for a certain language
94
+ ---
95
+ tags:
96
+ - Configuration
97
+ parameters:
98
+ - name: page
99
+ in: path
100
+ description: Page name
101
+ required: true
102
+ schema:
103
+ $ref: '#/components/schemas/GeoVisioPageName'
104
+ - name: lang
105
+ in: path
106
+ description: Language ISO 639-2 code
107
+ required: true
108
+ schema:
109
+ type: string
110
+ responses:
111
+ 200:
112
+ description: the HTML content for this page
113
+ content:
114
+ text/html:
115
+ schema:
116
+ type: string
117
+ """
118
+
119
+ page = check_page_name(page)
120
+ page_content = db.fetchone(
121
+ current_app,
122
+ SQL("SELECT content FROM pages WHERE name = %(name)s AND lang = %(lang)s"),
123
+ {"name": page.value, "lang": lang},
124
+ )
125
+
126
+ if page_content is None:
127
+ raise InvalidAPIUsage(_("Page not available in language %(l)s", l=lang), status_code=404)
128
+
129
+ return (
130
+ page_content[0],
131
+ 200,
132
+ {
133
+ "Content-Type": "text/html",
134
+ },
135
+ )
136
+
137
+
138
+ @bp.route("/pages/<page>/<lang>", methods=["POST", "PUT"])
139
+ @auth.login_required()
140
+ def postPage(page, lang, account):
141
+ """Save HTML content for a certain language of a page.
142
+
143
+ This call is only available for account with admin role.
144
+ ---
145
+ tags:
146
+ - Configuration
147
+ parameters:
148
+ - name: page
149
+ in: path
150
+ description: Page name
151
+ required: true
152
+ schema:
153
+ $ref: '#/components/schemas/GeoVisioPageName'
154
+ - name: lang
155
+ in: path
156
+ description: Language ISO 639-2 code
157
+ required: true
158
+ schema:
159
+ type: string
160
+ security:
161
+ - bearerToken: []
162
+ - cookieAuth: []
163
+ requestBody:
164
+ content:
165
+ text/html:
166
+ schema:
167
+ type: string
168
+ responses:
169
+ 200:
170
+ description: Successfully saved
171
+ """
172
+
173
+ name = check_page_name(page)
174
+
175
+ if not account.can_edit_pages():
176
+ raise InvalidAPIUsage(_("You must be logged-in as admin to edit pages"), 403)
177
+ if request.content_type != "text/html":
178
+ raise InvalidAPIUsage(_("Page content must be HTML (with " "Content-Type: text/html" " header set)"), 400)
179
+
180
+ with db.execute(
181
+ current_app,
182
+ SQL(
183
+ """
184
+ INSERT INTO pages (name, lang, content)
185
+ VALUES (%(name)s, %(lang)s, %(content)s)
186
+ ON CONFLICT (name, lang) DO UPDATE SET content=EXCLUDED.content
187
+ """
188
+ ),
189
+ {"name": name.value, "lang": lang, "content": request.get_data(as_text=True)},
190
+ ) as res:
191
+ if not res.rowcount:
192
+ raise InvalidAPIUsage(_("Could not update page content"), 500)
193
+
194
+ return "", 200
195
+
196
+
197
+ @bp.route("/pages/<page>/<lang>", methods=["DELETE"])
198
+ @auth.login_required()
199
+ def deletePage(page, lang, account):
200
+ """Delete HTML content for a certain language of a page.
201
+
202
+ This call is only available for account with admin role.
203
+ ---
204
+ tags:
205
+ - Configuration
206
+ parameters:
207
+ - name: page
208
+ in: path
209
+ description: Page name
210
+ required: true
211
+ schema:
212
+ $ref: '#/components/schemas/GeoVisioPageName'
213
+ - name: lang
214
+ in: path
215
+ description: Language ISO 639-2 code
216
+ required: true
217
+ schema:
218
+ type: string
219
+ security:
220
+ - bearerToken: []
221
+ - cookieAuth: []
222
+ responses:
223
+ 200:
224
+ description: Successfully deleted
225
+ """
226
+
227
+ name = check_page_name(page)
228
+
229
+ if not account.can_edit_pages():
230
+ raise InvalidAPIUsage(_("You must be logged-in as admin to edit pages"), 403)
231
+
232
+ with db.execute(
233
+ current_app, SQL("DELETE FROM pages WHERE name = %(name)s AND lang = %(lang)s"), {"name": name.value, "lang": lang}
234
+ ) as res:
235
+ if res.rowcount == 0:
236
+ raise InvalidAPIUsage(_("Page not available in language %(l)s", l=lang), status_code=404)
237
+ elif not res.rowcount:
238
+ raise InvalidAPIUsage(_("Could not delete page content"), 500)
239
+
240
+ return "", 200
geovisio/web/params.py CHANGED
@@ -358,6 +358,23 @@ def parse_filter(value: Optional[str]) -> Optional[sql.SQL]:
358
358
  return None
359
359
 
360
360
 
361
+ def parse_picture_heading(heading: Optional[str]) -> Optional[int]:
362
+ if heading is None:
363
+ return None
364
+ try:
365
+ heading = int(heading)
366
+ if heading < 0 or heading > 360:
367
+ raise ValueError()
368
+ return heading
369
+ except ValueError:
370
+ raise errors.InvalidAPIUsage(
371
+ _(
372
+ "Heading is not valid, should be an integer in degrees from 0° to 360°. North is 0°, East = 90°, South = 180° and West = 270°."
373
+ ),
374
+ status_code=400,
375
+ )
376
+
377
+
361
378
  class _FilterAstUpdated(Evaluator):
362
379
  """
363
380
  We alter the parsed AST in order to always query for 'hidden' pictures when we query for 'deleted' ones
@@ -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
 
@@ -185,8 +185,7 @@ def getUploadSet(upload_set_id):
185
185
 
186
186
 
187
187
  @bp.route("/upload_sets/<uuid:upload_set_id>/files", methods=["GET"])
188
- @auth.login_required_by_setting("API_FORCE_AUTH_ON_UPLOAD")
189
- def getUploadSetFiles(upload_set_id, account=None):
188
+ def getUploadSetFiles(upload_set_id):
190
189
  """List the files of an UploadSet
191
190
  ---
192
191
  tags:
@@ -210,13 +209,20 @@ def getUploadSetFiles(upload_set_id, account=None):
210
209
  schema:
211
210
  $ref: '#/components/schemas/GeoVisioUploadSetFiles'
212
211
  """
212
+ account = utils.auth.get_current_account()
213
+
213
214
  u = get_simple_upload_set(upload_set_id)
214
215
  if u is None:
215
216
  raise errors.InvalidAPIUsage(_("UploadSet doesn't exist"), status_code=404)
216
- if account is not None and account.id != str(u.account_id):
217
- raise errors.InvalidAPIUsage(_("You're not authorized to list pictures in this upload set"), status_code=403)
218
217
 
219
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
+
220
226
  return upload_set_files.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/json"}
221
227
 
222
228