geovisio 2.6.0__py3-none-any.whl → 2.7.1__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 (62) hide show
  1. geovisio/__init__.py +36 -7
  2. geovisio/admin_cli/cleanup.py +2 -2
  3. geovisio/admin_cli/db.py +1 -4
  4. geovisio/config_app.py +40 -1
  5. geovisio/db_migrations.py +24 -3
  6. geovisio/templates/main.html +13 -13
  7. geovisio/templates/viewer.html +3 -3
  8. geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
  9. geovisio/translations/de/LC_MESSAGES/messages.po +804 -0
  10. geovisio/translations/el/LC_MESSAGES/messages.mo +0 -0
  11. geovisio/translations/el/LC_MESSAGES/messages.po +685 -0
  12. geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
  13. geovisio/translations/en/LC_MESSAGES/messages.po +738 -0
  14. geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
  15. geovisio/translations/es/LC_MESSAGES/messages.po +778 -0
  16. geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
  17. geovisio/translations/fi/LC_MESSAGES/messages.po +589 -0
  18. geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
  19. geovisio/translations/fr/LC_MESSAGES/messages.po +814 -0
  20. geovisio/translations/hu/LC_MESSAGES/messages.mo +0 -0
  21. geovisio/translations/hu/LC_MESSAGES/messages.po +773 -0
  22. geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
  23. geovisio/translations/ko/LC_MESSAGES/messages.po +685 -0
  24. geovisio/translations/messages.pot +694 -0
  25. geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
  26. geovisio/translations/nl/LC_MESSAGES/messages.po +602 -0
  27. geovisio/utils/__init__.py +1 -1
  28. geovisio/utils/auth.py +50 -11
  29. geovisio/utils/db.py +65 -0
  30. geovisio/utils/excluded_areas.py +83 -0
  31. geovisio/utils/extent.py +30 -0
  32. geovisio/utils/fields.py +1 -1
  33. geovisio/utils/filesystems.py +0 -1
  34. geovisio/utils/link.py +14 -0
  35. geovisio/utils/params.py +20 -0
  36. geovisio/utils/pictures.py +110 -88
  37. geovisio/utils/reports.py +171 -0
  38. geovisio/utils/sequences.py +262 -126
  39. geovisio/utils/tokens.py +37 -42
  40. geovisio/utils/upload_set.py +642 -0
  41. geovisio/web/auth.py +37 -37
  42. geovisio/web/collections.py +304 -304
  43. geovisio/web/configuration.py +14 -0
  44. geovisio/web/docs.py +276 -15
  45. geovisio/web/excluded_areas.py +377 -0
  46. geovisio/web/items.py +169 -112
  47. geovisio/web/map.py +104 -36
  48. geovisio/web/params.py +69 -26
  49. geovisio/web/pictures.py +14 -31
  50. geovisio/web/reports.py +399 -0
  51. geovisio/web/rss.py +13 -7
  52. geovisio/web/stac.py +129 -134
  53. geovisio/web/tokens.py +98 -109
  54. geovisio/web/upload_set.py +771 -0
  55. geovisio/web/users.py +100 -73
  56. geovisio/web/utils.py +28 -9
  57. geovisio/workers/runner_pictures.py +241 -207
  58. {geovisio-2.6.0.dist-info → geovisio-2.7.1.dist-info}/METADATA +17 -14
  59. geovisio-2.7.1.dist-info/RECORD +70 -0
  60. {geovisio-2.6.0.dist-info → geovisio-2.7.1.dist-info}/WHEEL +1 -1
  61. geovisio-2.6.0.dist-info/RECORD +0 -41
  62. {geovisio-2.6.0.dist-info → geovisio-2.7.1.dist-info}/LICENSE +0 -0
@@ -0,0 +1,377 @@
1
+ from uuid import UUID
2
+ from flask import Blueprint, current_app, request
3
+ from flask_babel import gettext as _
4
+ from typing import Optional, Union
5
+ from psycopg.rows import class_row
6
+ from psycopg.sql import SQL, Literal, Identifier
7
+ from geovisio.utils import db, auth
8
+ from geovisio.utils.excluded_areas import (
9
+ list_excluded_areas,
10
+ delete_excluded_area,
11
+ ExcludedAreaFeature,
12
+ )
13
+ from geovisio.utils.params import validation_error
14
+ from geovisio.errors import InvalidAPIUsage, InternalError
15
+ from pydantic import BaseModel, ConfigDict, ValidationError
16
+ from geojson_pydantic import FeatureCollection, Feature, Polygon, MultiPolygon
17
+
18
+ bp = Blueprint("excluded_areas", __name__, url_prefix="/api")
19
+
20
+
21
+ class ExcludedAreaCreateParameters(BaseModel):
22
+ """An excluded area is a geographical boundary where pictures should not be accepted."""
23
+
24
+ label: Optional[str] = None
25
+ is_public: bool = False
26
+
27
+ model_config = ConfigDict()
28
+
29
+
30
+ ExcludedAreaCreateFeature = Feature[Union[Polygon, MultiPolygon], ExcludedAreaCreateParameters]
31
+ ExcludedAreaCreateCollection = FeatureCollection[ExcludedAreaCreateFeature]
32
+
33
+
34
+ def create_excluded_area(params: ExcludedAreaCreateFeature, accountId: Optional[UUID] = None) -> ExcludedAreaFeature:
35
+ params_as_dict = params.properties.model_dump(exclude_none=True)
36
+ if accountId:
37
+ params_as_dict["account_id"] = accountId
38
+
39
+ fields = [Identifier(f) for f in params_as_dict.keys()]
40
+ values = [Literal(v) for v in params_as_dict.values()]
41
+
42
+ # Handle geometry
43
+ fields.append(Identifier("geom"))
44
+ values.append(SQL("ST_Multi(ST_GeomFromText({}))").format(Literal(params.geometry.wkt)))
45
+
46
+ return db.fetchone(
47
+ current_app,
48
+ SQL(
49
+ """INSERT INTO excluded_areas({fields}) VALUES({values})
50
+ RETURNING
51
+ 'Feature' as type,
52
+ json_build_object(
53
+ 'id', id,
54
+ 'label', label,
55
+ 'is_public', is_public,
56
+ 'account_id', account_id
57
+ ) as properties,
58
+ ST_AsGeoJSON(geom)::json as geometry"""
59
+ ).format(fields=SQL(", ").join(fields), values=SQL(", ").join(values)),
60
+ row_factory=class_row(ExcludedAreaFeature),
61
+ )
62
+
63
+
64
+ def replace_excluded_areas(params: ExcludedAreaCreateCollection, invert: bool = False):
65
+ with db.conn(current_app) as conn, conn.transaction(), conn.cursor(row_factory=class_row(ExcludedAreaFeature)) as cursor:
66
+ # Remove all general areas
67
+ cursor.execute("DROP INDEX excluded_areas_geom_idx")
68
+ cursor.execute("DELETE FROM excluded_areas WHERE account_id IS NULL")
69
+
70
+ # Append new ones
71
+ # Invert given geometries if necessary
72
+ if invert:
73
+ # Insert geometries into a tmp table
74
+ cursor.execute("CREATE TEMPORARY TABLE allowed_areas(geom GEOMETRY(MultiPolygon, 4326))")
75
+ with cursor.copy("COPY allowed_areas(geom) FROM STDIN") as copy:
76
+ for f in params.features:
77
+ copy.write_row([f.geometry.wkt])
78
+
79
+ # Compute excluded areas and save in final table
80
+ cursor.execute(
81
+ """INSERT INTO excluded_areas(is_public, geom)
82
+ SELECT true, ST_Subdivide(
83
+ ST_Difference(
84
+ ST_SetSRID(ST_MakeEnvelope(-180, -90, 180, 90), 4326),
85
+ ST_Union(geom)
86
+ ),
87
+ 500
88
+ ) AS geom
89
+ FROM allowed_areas"""
90
+ )
91
+
92
+ # Send areas as is if no invert required
93
+ else:
94
+ with cursor.copy("COPY excluded_areas(label, is_public, geom) FROM STDIN") as copy:
95
+ for f in params.features:
96
+ copy.write_row(
97
+ (f.properties.label, f.properties.is_public if f.properties.is_public is not None else True, f.geometry.wkt)
98
+ )
99
+
100
+ # Restore index
101
+ cursor.execute("CREATE INDEX excluded_areas_geom_idx ON excluded_areas USING GIST(geom)")
102
+
103
+
104
+ @bp.route("/configuration/excluded_areas")
105
+ def getExcludedAreas():
106
+ """List excluded areas
107
+ ---
108
+ tags:
109
+ - Excluded Areas
110
+ - Metadata
111
+ parameters:
112
+ - name: all
113
+ in: query
114
+ description: To fetch all areas, including not public ones. all=true needs admin rights for access.
115
+ required: false
116
+ schema:
117
+ type: boolean
118
+ responses:
119
+ 200:
120
+ description: the list of excluded areas, as GeoJSON
121
+ content:
122
+ application/geo+json:
123
+ schema:
124
+ $ref: '#/components/schemas/GeoVisioExcludedAreas'
125
+ """
126
+
127
+ allAreas = request.args.get("all", "false").lower() == "true"
128
+ account = auth.get_current_account()
129
+
130
+ # Check access rights for listing all excluded areas
131
+ if allAreas:
132
+ if not account:
133
+ raise InvalidAPIUsage(_("You must be logged-in as admin to access all excluded areas"), status_code=401)
134
+ elif not account.can_edit_excluded_areas():
135
+ raise InvalidAPIUsage(_("You're not authorized to access all excluded areas"), status_code=403)
136
+
137
+ # Send result
138
+ areas = list_excluded_areas(is_public=None if allAreas else True)
139
+ return areas.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/geo+json"}
140
+
141
+
142
+ @bp.route("/configuration/excluded_areas", methods=["POST"])
143
+ @auth.login_required_with_redirect()
144
+ def postExcludedArea(account):
145
+ """Add a new general excluded area.
146
+
147
+ This call is only available for account with admin role.
148
+ ---
149
+ tags:
150
+ - Excluded Areas
151
+ requestBody:
152
+ content:
153
+ application/geo+json:
154
+ schema:
155
+ $ref: '#/components/schemas/GeoVisioExcludedAreaCreateFeature'
156
+ security:
157
+ - bearerToken: []
158
+ - cookieAuth: []
159
+ responses:
160
+ 200:
161
+ description: the list of excluded areas, as GeoJSON
162
+ content:
163
+ application/geo+json:
164
+ schema:
165
+ $ref: '#/components/schemas/GeoVisioExcludedArea'
166
+ """
167
+
168
+ if request.is_json and request.json is not None:
169
+ try:
170
+ params = ExcludedAreaCreateFeature(**request.json)
171
+ except ValidationError as ve:
172
+ raise InvalidAPIUsage(_("Impossible to create an Excluded Area"), payload=validation_error(ve))
173
+ else:
174
+ raise InvalidAPIUsage(_("Parameter for creating an Excluded Area should be a valid JSON"), status_code=415)
175
+
176
+ if not account.can_edit_excluded_areas():
177
+ raise InvalidAPIUsage(_("You must be logged-in as admin to edit excluded areas"), 403)
178
+
179
+ try:
180
+ area = create_excluded_area(params)
181
+ except Exception as e:
182
+ raise InternalError(_("Impossible to create an Excluded Area"), status_code=500, payload={"details": str(e)})
183
+
184
+ return (
185
+ area.model_dump_json(exclude_none=True),
186
+ 200,
187
+ {
188
+ "Content-Type": "application/geo+json",
189
+ },
190
+ )
191
+
192
+
193
+ @bp.route("/configuration/excluded_areas", methods=["PUT"])
194
+ @auth.login_required_with_redirect()
195
+ def replaceExcludedAreas(account):
196
+ """Replace the whole set of general excluded areas with given ones.
197
+
198
+ This call is only available for account with admin role.
199
+ ---
200
+ tags:
201
+ - Excluded Areas
202
+ parameters:
203
+ - name: invert
204
+ in: query
205
+ description: Set to true if you want to send allowed areas instead of excluded ones. Note that using this parameter will make all generated excluded areas as publicly visible.
206
+ required: false
207
+ schema:
208
+ type: boolean
209
+ requestBody:
210
+ content:
211
+ application/geo+json:
212
+ schema:
213
+ $ref: '#/components/schemas/GeoVisioExcludedAreaCreateCollection'
214
+ security:
215
+ - bearerToken: []
216
+ - cookieAuth: []
217
+ responses:
218
+ 200:
219
+ description: the list of excluded areas, as GeoJSON
220
+ content:
221
+ application/geo+json:
222
+ schema:
223
+ $ref: '#/components/schemas/GeoVisioExcludedAreas'
224
+ """
225
+
226
+ if request.is_json and request.json is not None:
227
+ try:
228
+ params = ExcludedAreaCreateCollection(**request.json)
229
+ except ValidationError as ve:
230
+ raise InvalidAPIUsage(_("Impossible to replace all Excluded Areas"), payload=validation_error(ve))
231
+ else:
232
+ raise InvalidAPIUsage(_("Parameter for replacing all Excluded Areas should be a valid JSON"), status_code=415)
233
+
234
+ if not account.can_edit_excluded_areas():
235
+ raise InvalidAPIUsage(_("You must be logged-in as admin to edit excluded areas"), 403)
236
+
237
+ invert = request.args.get("invert", "false").lower() == "true"
238
+
239
+ try:
240
+ replace_excluded_areas(params, invert)
241
+ except Exception as e:
242
+ raise InternalError(_("Impossible to replace all Excluded Areas"), status_code=500, payload={"details": str(e)})
243
+
244
+ # Send result
245
+ areas = list_excluded_areas(is_public=None)
246
+ return areas.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/geo+json"}
247
+
248
+
249
+ @bp.route("/configuration/excluded_areas/<uuid:areaId>", methods=["DELETE"])
250
+ @auth.login_required_with_redirect()
251
+ def deleteExcludedArea(areaId, account):
252
+ """Delete an existing excluded area
253
+ ---
254
+ tags:
255
+ - Excluded Areas
256
+ parameters:
257
+ - name: areaId
258
+ in: path
259
+ description: ID of excluded area to delete
260
+ required: true
261
+ schema:
262
+ type: string
263
+ security:
264
+ - bearerToken: []
265
+ - cookieAuth: []
266
+ responses:
267
+ 204:
268
+ description: The object has been correctly deleted
269
+ """
270
+
271
+ if not account.can_edit_excluded_areas():
272
+ raise InvalidAPIUsage(_("You must be logged-in as admin to delete excluded areas"), 403)
273
+
274
+ return delete_excluded_area(areaId)
275
+
276
+
277
+ @bp.route("/users/me/excluded_areas", methods=["GET"])
278
+ @auth.login_required_with_redirect()
279
+ def getUserExcludedAreas(account):
280
+ """List excluded areas for current user.
281
+
282
+ This only includes user-specific areas. For general excluded areas, see /api/configuration/excluded_areas.
283
+ ---
284
+ tags:
285
+ - Excluded Areas
286
+ - Users
287
+ security:
288
+ - bearerToken: []
289
+ - cookieAuth: []
290
+ responses:
291
+ 200:
292
+ description: the list of user-specific excluded areas, as GeoJSON
293
+ content:
294
+ application/geo+json:
295
+ schema:
296
+ $ref: '#/components/schemas/GeoVisioExcludedAreas'
297
+ """
298
+
299
+ # Send result
300
+ areas = list_excluded_areas(account_id=account.id)
301
+ return areas.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/geo+json"}
302
+
303
+
304
+ @bp.route("/users/me/excluded_areas", methods=["POST"])
305
+ @auth.login_required_with_redirect()
306
+ def postUserExcludedArea(account):
307
+ """Add a new excluded area for a specific user.
308
+
309
+ Note that this excluded area will only apply to pictures uploaded by given user.
310
+ For general excluded areas, use POST/PUT /api/configuration/excluded_areas instead.
311
+ ---
312
+ tags:
313
+ - Excluded Areas
314
+ - Users
315
+ requestBody:
316
+ content:
317
+ application/geo+json:
318
+ schema:
319
+ $ref: '#/components/schemas/GeoVisioExcludedAreaCreateFeature'
320
+ security:
321
+ - bearerToken: []
322
+ - cookieAuth: []
323
+ responses:
324
+ 200:
325
+ description: the added excluded area
326
+ content:
327
+ application/geo+json:
328
+ schema:
329
+ $ref: '#/components/schemas/GeoVisioExcludedArea'
330
+ """
331
+
332
+ if request.is_json and request.json is not None:
333
+ try:
334
+ params = ExcludedAreaCreateFeature(**request.json)
335
+ except ValidationError as ve:
336
+ raise InvalidAPIUsage(_("Impossible to create an Excluded Area"), payload=validation_error(ve))
337
+ else:
338
+ raise InvalidAPIUsage(_("Parameter for creating an Excluded Area should be a valid JSON"), status_code=415)
339
+
340
+ try:
341
+ area = create_excluded_area(params, UUID(account.id))
342
+ except Exception as e:
343
+ raise InternalError(_("Impossible to create an Excluded Area"), status_code=500, payload={"details": str(e)})
344
+
345
+ return (
346
+ area.model_dump_json(exclude_none=True),
347
+ 200,
348
+ {
349
+ "Content-Type": "application/geo+json",
350
+ },
351
+ )
352
+
353
+
354
+ @bp.route("/users/me/excluded_areas/<uuid:areaId>", methods=["DELETE"])
355
+ @auth.login_required_with_redirect()
356
+ def deleteUserExcludedArea(areaId, account):
357
+ """Delete an existing excluded area for current user
358
+ ---
359
+ tags:
360
+ - Excluded Areas
361
+ - Users
362
+ parameters:
363
+ - name: areaId
364
+ in: path
365
+ description: ID of excluded area to delete
366
+ required: true
367
+ schema:
368
+ type: string
369
+ security:
370
+ - bearerToken: []
371
+ - cookieAuth: []
372
+ responses:
373
+ 204:
374
+ description: The object has been correctly deleted
375
+ """
376
+
377
+ return delete_excluded_area(areaId, accountId=UUID(account.id))