geovisio 2.9.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.
Files changed (82) hide show
  1. geovisio/__init__.py +8 -1
  2. geovisio/admin_cli/user.py +7 -2
  3. geovisio/config_app.py +26 -12
  4. geovisio/translations/ar/LC_MESSAGES/messages.mo +0 -0
  5. geovisio/translations/ar/LC_MESSAGES/messages.po +818 -0
  6. geovisio/translations/be/LC_MESSAGES/messages.mo +0 -0
  7. geovisio/translations/be/LC_MESSAGES/messages.po +886 -0
  8. geovisio/translations/br/LC_MESSAGES/messages.po +1 -1
  9. geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
  10. geovisio/translations/da/LC_MESSAGES/messages.po +96 -4
  11. geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
  12. geovisio/translations/de/LC_MESSAGES/messages.po +214 -122
  13. geovisio/translations/el/LC_MESSAGES/messages.po +1 -1
  14. geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
  15. geovisio/translations/en/LC_MESSAGES/messages.po +234 -157
  16. geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
  17. geovisio/translations/eo/LC_MESSAGES/messages.po +55 -5
  18. geovisio/translations/es/LC_MESSAGES/messages.po +1 -1
  19. geovisio/translations/fi/LC_MESSAGES/messages.po +1 -1
  20. geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
  21. geovisio/translations/fr/LC_MESSAGES/messages.po +92 -3
  22. geovisio/translations/hu/LC_MESSAGES/messages.po +1 -1
  23. geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
  24. geovisio/translations/it/LC_MESSAGES/messages.po +63 -3
  25. geovisio/translations/ja/LC_MESSAGES/messages.po +1 -1
  26. geovisio/translations/ko/LC_MESSAGES/messages.po +1 -1
  27. geovisio/translations/messages.pot +216 -139
  28. geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
  29. geovisio/translations/nl/LC_MESSAGES/messages.po +333 -62
  30. geovisio/translations/oc/LC_MESSAGES/messages.mo +0 -0
  31. geovisio/translations/oc/LC_MESSAGES/messages.po +821 -0
  32. geovisio/translations/pl/LC_MESSAGES/messages.po +1 -1
  33. geovisio/translations/pt/LC_MESSAGES/messages.mo +0 -0
  34. geovisio/translations/pt/LC_MESSAGES/messages.po +944 -0
  35. geovisio/translations/pt_BR/LC_MESSAGES/messages.mo +0 -0
  36. geovisio/translations/pt_BR/LC_MESSAGES/messages.po +942 -0
  37. geovisio/translations/sv/LC_MESSAGES/messages.mo +0 -0
  38. geovisio/translations/sv/LC_MESSAGES/messages.po +4 -3
  39. geovisio/translations/ti/LC_MESSAGES/messages.mo +0 -0
  40. geovisio/translations/ti/LC_MESSAGES/messages.po +762 -0
  41. geovisio/translations/tr/LC_MESSAGES/messages.mo +0 -0
  42. geovisio/translations/tr/LC_MESSAGES/messages.po +927 -0
  43. geovisio/translations/uk/LC_MESSAGES/messages.mo +0 -0
  44. geovisio/translations/uk/LC_MESSAGES/messages.po +920 -0
  45. geovisio/translations/zh_Hant/LC_MESSAGES/messages.po +1 -1
  46. geovisio/utils/annotations.py +21 -21
  47. geovisio/utils/auth.py +47 -13
  48. geovisio/utils/cql2.py +22 -5
  49. geovisio/utils/fields.py +14 -2
  50. geovisio/utils/items.py +44 -0
  51. geovisio/utils/model_query.py +2 -2
  52. geovisio/utils/pic_shape.py +1 -1
  53. geovisio/utils/pictures.py +127 -36
  54. geovisio/utils/semantics.py +32 -3
  55. geovisio/utils/sentry.py +1 -1
  56. geovisio/utils/sequences.py +155 -109
  57. geovisio/utils/upload_set.py +303 -206
  58. geovisio/utils/users.py +18 -0
  59. geovisio/utils/website.py +1 -1
  60. geovisio/web/annotations.py +303 -69
  61. geovisio/web/auth.py +1 -1
  62. geovisio/web/collections.py +194 -97
  63. geovisio/web/configuration.py +36 -4
  64. geovisio/web/docs.py +109 -13
  65. geovisio/web/items.py +319 -186
  66. geovisio/web/map.py +92 -54
  67. geovisio/web/pages.py +48 -4
  68. geovisio/web/params.py +100 -42
  69. geovisio/web/pictures.py +37 -3
  70. geovisio/web/prepare.py +4 -2
  71. geovisio/web/queryables.py +57 -0
  72. geovisio/web/stac.py +8 -2
  73. geovisio/web/tokens.py +49 -1
  74. geovisio/web/upload_set.py +226 -51
  75. geovisio/web/users.py +89 -8
  76. geovisio/web/utils.py +26 -8
  77. geovisio/workers/runner_pictures.py +128 -23
  78. {geovisio-2.9.0.dist-info → geovisio-2.11.0.dist-info}/METADATA +15 -14
  79. geovisio-2.11.0.dist-info/RECORD +117 -0
  80. geovisio-2.9.0.dist-info/RECORD +0 -98
  81. {geovisio-2.9.0.dist-info → geovisio-2.11.0.dist-info}/WHEEL +0 -0
  82. {geovisio-2.9.0.dist-info → geovisio-2.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,18 @@
1
+ import logging
2
+ from geovisio.utils.auth import Account
3
+
4
+
5
+ def delete_user_data(conn, account: Account):
6
+ """Delete all the pictures of a user
7
+
8
+ Note that the database changes will be done synchronously but the picture deletion will be an asynchronous task,
9
+ so some background workers need to be run in order for the pictures deletion to be effective.
10
+ """
11
+ with conn.transaction(), conn.cursor() as cursor:
12
+ logging.info(f"Deleting pictures of account {account.name} ({account.id})")
13
+ # Note: deleting a picture's row add a new `delete` async task to the queue, to delete the associated files
14
+ nb_deleted_pics = cursor.execute("DELETE FROM pictures WHERE account_id = %s", [account.id]).rowcount
15
+ cursor.execute("DELETE FROM upload_sets WHERE account_id = %s", [account.id])
16
+ cursor.execute("UPDATE sequences SET status = 'deleted' WHERE account_id = %s", [account.id])
17
+ logging.info(f"Deleted {nb_deleted_pics} pictures from account {account.name} ({account.id})")
18
+ return nb_deleted_pics
geovisio/utils/website.py CHANGED
@@ -14,7 +14,7 @@ class Website:
14
14
  """Website associated to the API.
15
15
  This wrapper will define the routes we expect from the website.
16
16
 
17
- We should limit the interraction from the api to the website, but for some flow (especially auth flows), it's can be useful to redirect to website's page
17
+ We should limit the interaction from the api to the website, but for some flow (especially auth flows), it's can be useful to redirect to website's page
18
18
 
19
19
  If the url is:
20
20
  * set to `false`, there is no associated website
@@ -1,11 +1,17 @@
1
1
  from typing import List, Optional
2
2
  from geovisio.utils import auth, db
3
- from geovisio.utils.annotations import AnnotationCreationParameter, creation_annotation, get_annotation, update_annotation
3
+ from geovisio.utils.annotations import (
4
+ AnnotationCreationParameter,
5
+ creation_annotation,
6
+ get_annotation,
7
+ update_annotation,
8
+ InputAnnotationShape,
9
+ delete_annotation,
10
+ )
4
11
  from geovisio.utils.tags import SemanticTagUpdate
5
- from geovisio.web.utils import accountIdOrDefault
6
12
  from geovisio.utils.params import validation_error
7
13
  from geovisio import errors
8
- from pydantic import BaseModel, ValidationError
14
+ from pydantic import BaseModel, ValidationError, Field
9
15
  from uuid import UUID
10
16
  from flask import Blueprint, current_app, request, url_for
11
17
  from flask_babel import gettext as _
@@ -14,86 +20,144 @@ from flask_babel import gettext as _
14
20
  bp = Blueprint("annotations", __name__, url_prefix="/api")
15
21
 
16
22
 
17
- @bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations", methods=["POST"])
23
+ class AnnotationPostParameter(BaseModel):
24
+ shape: InputAnnotationShape
25
+ """Shape defining the annotation.
26
+ The annotation shape is either a full geojson geometry or only a bounding box (4 floats).
27
+
28
+ The coordinates should be given in pixel, starting from the bottom left of the picture.
29
+
30
+ Note that the API will always output geometry as geojson geometry (thus will transform the bbox into a polygon).
31
+ """
32
+
33
+ semantics: List[SemanticTagUpdate] = Field(default_factory=list)
34
+ """Semantic tags associated to the annotation"""
35
+
36
+
37
+ @bp.route("/pictures/<uuid:itemId>/annotations", methods=["POST"])
18
38
  @auth.login_required()
19
- def postAnnotation(collectionId, itemId, account):
39
+ def postAnnotationNonStacAlias(itemId, account):
20
40
  """Create an annotation on a picture.
21
41
 
22
42
  The geometry can be provided as a bounding box (a list of 4 integers, minx, miny, maxx, maxy) or as a geojson geometry.
23
43
  All coordinates must be in pixel, starting from the top left of the picture.
24
44
 
25
45
  If an annotation already exists on the picture with the same shape, it will be used.
26
- ---
27
- tags:
28
- - Editing
29
- - Semantics
30
- parameters:
31
- - name: collectionId
32
- in: path
33
- description: ID of collection to retrieve
34
- required: true
35
- schema:
36
- type: string
37
- - name: itemId
38
- in: path
39
- description: ID of item to retrieve
40
- required: true
41
- schema:
42
- type: string
43
- requestBody:
46
+
47
+ The is an alias to the `/api/collections/<collectionId>/items/<itemId>/annotations` endpoint (but you don't need to know the collection ID here).
48
+ ---
49
+ tags:
50
+ - Editing
51
+ - Semantics
52
+ parameters:
53
+ - name: itemId
54
+ in: path
55
+ description: ID of item to retrieve
56
+ required: true
57
+ schema:
58
+ type: string
59
+ requestBody:
60
+ content:
61
+ application/json:
62
+ schema:
63
+ $ref: '#/components/schemas/GeoVisioPostAnnotation'
64
+ security:
65
+ - bearerToken: []
66
+ - cookieAuth: []
67
+ responses:
68
+ 200:
69
+ description: the annotation metadata
44
70
  content:
45
71
  application/json:
46
72
  schema:
47
- $ref: '#/components/schemas/GeoVisioPostAnnotation'
48
- security:
49
- - bearerToken: []
50
- - cookieAuth: []
51
- responses:
52
- 200:
53
- description: the annotation metadata
54
- content:
55
- application/json:
56
- schema:
57
- $ref: '#/components/schemas/GeoVisioAnnotation'
73
+ $ref: '#/components/schemas/GeoVisioAnnotation'
58
74
  """
59
75
 
60
- account_id = UUID(accountIdOrDefault(account))
61
-
76
+ account_id = account.id
62
77
  pic = db.fetchone(
63
78
  current_app,
64
- "SELECT 1 FROM sequences_pictures WHERE seq_id = %(seq)s AND pic_id = %(pic)s",
65
- {"seq": collectionId, "pic": itemId},
79
+ "SELECT 1 FROM pictures WHERE id = %(pic)s",
80
+ {"pic": itemId},
66
81
  )
67
82
  if not pic:
68
83
  raise errors.InvalidAPIUsage(_("Picture %(p)s wasn't found in database", p=itemId), status_code=404)
69
84
 
70
85
  if request.is_json and request.json is not None:
71
86
  try:
72
- params = AnnotationCreationParameter(**request.json, account_id=account_id, picture_id=itemId)
87
+ post_params = AnnotationPostParameter(**request.json, account_id=account_id, picture_id=itemId)
73
88
  except ValidationError as ve:
74
89
  raise errors.InvalidAPIUsage(_("Impossible to create an annotation"), payload=validation_error(ve))
75
90
  else:
76
91
  raise errors.InvalidAPIUsage(_("Parameter for creating an annotation should be a valid JSON"), status_code=415)
77
92
 
78
- annotation = creation_annotation(params)
79
-
80
- return (
81
- annotation.model_dump_json(exclude_none=True),
82
- 200,
83
- {
84
- "Content-Type": "application/json",
85
- "Access-Control-Expose-Headers": "Location", # Needed for allowing web browsers access Location header
86
- "Location": url_for(
87
- "annotations.getAnnotation", _external=True, annotationId=annotation.id, collectionId=collectionId, itemId=itemId
88
- ),
89
- },
93
+ creation_params = AnnotationCreationParameter(
94
+ account_id=account_id, picture_id=itemId, shape=post_params.shape, semantics=post_params.semantics
90
95
  )
91
96
 
97
+ with db.conn(current_app) as conn:
98
+ annotation = creation_annotation(creation_params, conn)
99
+
100
+ return (
101
+ annotation.model_dump_json(exclude_none=True),
102
+ 200,
103
+ {
104
+ "Content-Type": "application/json",
105
+ "Access-Control-Expose-Headers": "Location", # Needed for allowing web browsers access Location header
106
+ "Location": url_for("annotations.getAnnotationById", _external=True, annotationId=annotation.id),
107
+ },
108
+ )
109
+
110
+
111
+ @bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations", methods=["POST"])
112
+ @auth.login_required()
113
+ def postAnnotation(collectionId, itemId, account):
114
+ """Create an annotation on a picture.
115
+
116
+ The geometry can be provided as a bounding box (a list of 4 integers, minx, miny, maxx, maxy) or as a geojson geometry.
117
+ All coordinates must be in pixel, starting from the top left of the picture.
118
+
119
+ If an annotation already exists on the picture with the same shape, it will be used.
120
+ ---
121
+ tags:
122
+ - Editing
123
+ - Semantics
124
+ parameters:
125
+ - name: collectionId
126
+ in: path
127
+ description: ID of collection to retrieve
128
+ required: true
129
+ schema:
130
+ type: string
131
+ - name: itemId
132
+ in: path
133
+ description: ID of item to retrieve
134
+ required: true
135
+ schema:
136
+ type: string
137
+ requestBody:
138
+ content:
139
+ application/json:
140
+ schema:
141
+ $ref: '#/components/schemas/GeoVisioPostAnnotation'
142
+ security:
143
+ - bearerToken: []
144
+ - cookieAuth: []
145
+ responses:
146
+ 200:
147
+ description: the annotation metadata
148
+ content:
149
+ application/json:
150
+ schema:
151
+ $ref: '#/components/schemas/GeoVisioAnnotation'
152
+ """
153
+ return postAnnotationNonStacAlias(itemId=itemId, account=account)
154
+
92
155
 
93
156
  @bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations/<uuid:annotationId>", methods=["GET"])
94
157
  def getAnnotation(collectionId, itemId, annotationId):
95
158
  """Get an annotation
96
159
 
160
+ Note that this is the same route as `/api/annotations/<uuid:annotationId>` but you need to know the picture's and collection's IDs.
97
161
  ---
98
162
  tags:
99
163
  - Semantics
@@ -103,19 +167,19 @@ def getAnnotation(collectionId, itemId, annotationId):
103
167
  description: ID of collection
104
168
  required: true
105
169
  schema:
106
- type: string
170
+ type: string
107
171
  - name: itemId
108
172
  in: path
109
173
  description: ID of item
110
174
  required: true
111
175
  schema:
112
- type: string
176
+ type: string
113
177
  - name: annotationId
114
178
  in: path
115
179
  description: ID of annotation
116
180
  required: true
117
181
  schema:
118
- type: string
182
+ type: string
119
183
  security:
120
184
  - bearerToken: []
121
185
  - cookieAuth: []
@@ -136,6 +200,41 @@ def getAnnotation(collectionId, itemId, annotationId):
136
200
  return annotation.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/json"}
137
201
 
138
202
 
203
+ @bp.route("/annotations/<uuid:annotationId>", methods=["GET"])
204
+ def getAnnotationById(annotationId):
205
+ """Get an annotation.
206
+
207
+ This is the same route as `/api/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations/<uuid:annotationId>` but you don't need to know the picture's and collection's IDs.
208
+
209
+ ---
210
+ tags:
211
+ - Semantics
212
+ parameters:
213
+ - name: annotationId
214
+ in: path
215
+ description: ID of annotation
216
+ required: true
217
+ schema:
218
+ type: string
219
+ security:
220
+ - bearerToken: []
221
+ - cookieAuth: []
222
+ responses:
223
+ 200:
224
+ description: the annotation metadata
225
+ content:
226
+ application/json:
227
+ schema:
228
+ $ref: '#/components/schemas/GeoVisioAnnotation'
229
+ """
230
+ with db.conn(current_app) as conn:
231
+ annotation = get_annotation(conn, annotationId)
232
+ if not annotation:
233
+ raise errors.InvalidAPIUsage(_("Annotation %(p)s not found", p=annotationId), status_code=404)
234
+
235
+ return annotation.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/json"}
236
+
237
+
139
238
  class AnnotationPatchParameter(BaseModel):
140
239
  """Parameters used to update an annotation"""
141
240
 
@@ -152,12 +251,70 @@ class AnnotationPatchParameter(BaseModel):
152
251
  """
153
252
 
154
253
 
254
+ @bp.route("/annotations/<uuid:annotationId>", methods=["PATCH"])
255
+ @auth.login_required()
256
+ def patchAnnotationNonStacAlias(annotationId, account):
257
+ """Patch an annotation
258
+
259
+ Note that if the annotation has no associated tags anymore, it will be deleted.
260
+
261
+ Note that is an alias to the `/api/collections/<collectionId>/items/<itemId>/annotations/<annotationId>` endpoint (but you don't need to know the collection/item ID here).
262
+ ---
263
+ tags:
264
+ - Semantics
265
+ parameters:
266
+ - name: annotationId
267
+ in: path
268
+ description: ID of annotation
269
+ required: true
270
+ schema:
271
+ type: string
272
+ requestBody:
273
+ content:
274
+ application/json:
275
+ schema:
276
+ $ref: '#/components/schemas/GeoVisioPatchAnnotation'
277
+ security:
278
+ - bearerToken: []
279
+ - cookieAuth: []
280
+ responses:
281
+ 200:
282
+ description: the annotation metadata
283
+ content:
284
+ application/json:
285
+ schema:
286
+ $ref: '#/components/schemas/GeoVisioAnnotation'
287
+ 204:
288
+ description: The annotation was empty, it has been correctly deleted
289
+ """
290
+ if request.is_json and request.json is not None:
291
+ try:
292
+ params = AnnotationPatchParameter(**request.json)
293
+ except ValidationError as ve:
294
+ raise errors.InvalidAPIUsage(_("Impossible to patch annotation, invalid parameters"), payload=validation_error(ve))
295
+ else:
296
+ raise errors.InvalidAPIUsage(_("Parameter for updating an annotation should be a valid JSON"), status_code=415)
297
+
298
+ with db.conn(current_app) as conn:
299
+
300
+ annotation = get_annotation(conn, annotationId)
301
+ if not annotation:
302
+ raise errors.InvalidAPIUsage(_("Annotation %(p)s not found", p=annotationId), status_code=404)
303
+
304
+ a = update_annotation(annotation, params.semantics, account.id)
305
+ if a is None:
306
+ return "", 204
307
+ return a.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/json"}
308
+
309
+
155
310
  @bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations/<uuid:annotationId>", methods=["PATCH"])
156
311
  @auth.login_required()
157
312
  def patchAnnotation(collectionId, itemId, annotationId, account):
158
313
  """Patch an annotation
159
314
 
160
315
  Note that if the annotation has no associated tags anymore, it will be deleted.
316
+
317
+ Note that is the an alias to the `/api/annotations/<annotationId>` endpoint (but you need to know the collection/item ID here).
161
318
  ---
162
319
  tags:
163
320
  - Semantics
@@ -167,19 +324,19 @@ def patchAnnotation(collectionId, itemId, annotationId, account):
167
324
  description: ID of collection
168
325
  required: true
169
326
  schema:
170
- type: string
327
+ type: string
171
328
  - name: itemId
172
329
  in: path
173
330
  description: ID of item
174
331
  required: true
175
332
  schema:
176
- type: string
333
+ type: string
177
334
  - name: annotationId
178
335
  in: path
179
336
  description: ID of annotation
180
337
  required: true
181
338
  schema:
182
- type: string
339
+ type: string
183
340
  security:
184
341
  - bearerToken: []
185
342
  - cookieAuth: []
@@ -193,21 +350,98 @@ def patchAnnotation(collectionId, itemId, annotationId, account):
193
350
  204:
194
351
  description: The annotation was empty, it has been correctly deleted
195
352
  """
196
- if request.is_json and request.json is not None:
197
- try:
198
- params = AnnotationPatchParameter(**request.json)
199
- except ValidationError as ve:
200
- raise errors.InvalidAPIUsage(_("Impossible to patch annotation, invalid parameters"), payload=validation_error(ve))
201
- else:
202
- raise errors.InvalidAPIUsage(_("Parameter for updating an annotation should be a valid JSON"), status_code=415)
353
+ return patchAnnotationNonStacAlias(annotationId=annotationId, account=account)
354
+
355
+
356
+ @bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations/<uuid:annotationId>", methods=["DELETE"])
357
+ @auth.login_required()
358
+ def deleteAnnotation(collectionId, itemId, annotationId, account):
359
+ """Delete an annotation
203
360
 
361
+ It is mandatory to be authenticated to delete an annotation, but anyone can delete do it. The changes are tracked in the history.
362
+
363
+ Note that this is the same route as `DELETE /api/annotations/<uuid:annotationId>` but you need to know the picture's and collection's IDs.
364
+ ---
365
+ tags:
366
+ - Semantics
367
+ parameters:
368
+ - name: collectionId
369
+ in: path
370
+ description: ID of collection
371
+ required: true
372
+ schema:
373
+ type: string
374
+ - name: itemId
375
+ in: path
376
+ description: ID of item
377
+ required: true
378
+ schema:
379
+ type: string
380
+ - name: annotationId
381
+ in: path
382
+ description: ID of annotation
383
+ required: true
384
+ schema:
385
+ type: string
386
+ security:
387
+ - bearerToken: []
388
+ - cookieAuth: []
389
+ responses:
390
+ 204:
391
+ description: The Annotation has been correctly deleted
392
+ """
204
393
  with db.conn(current_app) as conn:
205
394
 
206
395
  annotation = get_annotation(conn, annotationId)
207
396
  if not annotation or annotation.picture_id != itemId:
208
- raise errors.InvalidAPIUsage(_("Annotation %(p)s not found", p=itemId), status_code=404)
397
+ raise errors.InvalidAPIUsage(_("Annotation %(p)s not found", p=annotationId), status_code=404)
209
398
 
210
- a = update_annotation(annotation, params.semantics, account.id)
211
- if a is None:
212
- return "", 204
213
- return a.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/json"}
399
+ delete_annotation(conn, annotation)
400
+
401
+ return "", 204
402
+
403
+
404
+ @bp.route("/annotations/<uuid:annotationId>", methods=["DELETE"])
405
+ @auth.login_required()
406
+ def deleteAnnotationNonStacAlias(annotationId, account):
407
+ """Delete an annotation.
408
+
409
+ It is mandatory to be authenticated to delete an annotation, but anyone can delete do it. The changes are tracked in the history.
410
+
411
+ The is an alias to the `DELETE /api/collections/<collectionId>/items/<itemId>/annotations/<annotationId>` endpoint (but you don't need to know the collection/item ID here).
412
+ ---
413
+ tags:
414
+ - Semantics
415
+ parameters:
416
+ - name: collectionId
417
+ in: path
418
+ description: ID of collection
419
+ required: true
420
+ schema:
421
+ type: string
422
+ - name: itemId
423
+ in: path
424
+ description: ID of item
425
+ required: true
426
+ schema:
427
+ type: string
428
+ - name: annotationId
429
+ in: path
430
+ description: ID of annotation
431
+ required: true
432
+ schema:
433
+ type: string
434
+ security:
435
+ - bearerToken: []
436
+ - cookieAuth: []
437
+ responses:
438
+ 204:
439
+ description: The Annotation has been correctly deleted
440
+ """
441
+ with db.conn(current_app) as conn:
442
+ annotation = get_annotation(conn, annotationId)
443
+ if not annotation:
444
+ raise errors.InvalidAPIUsage(_("Annotation %(p)s not found", p=annotationId), status_code=404)
445
+ delete_annotation(conn, annotation, account.id)
446
+
447
+ return "", 204
geovisio/web/auth.py CHANGED
@@ -112,7 +112,7 @@ def auth():
112
112
  def logout():
113
113
  """Log out from geovisio
114
114
  * If the OAuth Provider is keycloak, this will redirect to a keycloak confirmation page,
115
- and uppon confirmation keycloak will call post_logout_redirect that will invalidate the session
115
+ and upon confirmation keycloak will call post_logout_redirect that will invalidate the session
116
116
  * If the OAuth Provider is not keycloak, this will invalidate the session
117
117
  ---
118
118
  tags: