geovisio 2.6.0__py3-none-any.whl → 2.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geovisio/__init__.py +36 -7
- geovisio/admin_cli/db.py +1 -4
- geovisio/config_app.py +40 -1
- geovisio/db_migrations.py +24 -3
- geovisio/templates/main.html +13 -13
- geovisio/templates/viewer.html +3 -3
- geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/de/LC_MESSAGES/messages.po +667 -0
- geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/en/LC_MESSAGES/messages.po +730 -0
- geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/es/LC_MESSAGES/messages.po +778 -0
- geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fi/LC_MESSAGES/messages.po +589 -0
- geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.po +814 -0
- geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ko/LC_MESSAGES/messages.po +685 -0
- geovisio/translations/messages.pot +686 -0
- geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/nl/LC_MESSAGES/messages.po +594 -0
- geovisio/utils/__init__.py +1 -1
- geovisio/utils/auth.py +50 -11
- geovisio/utils/db.py +65 -0
- geovisio/utils/excluded_areas.py +83 -0
- geovisio/utils/extent.py +30 -0
- geovisio/utils/fields.py +1 -1
- geovisio/utils/filesystems.py +0 -1
- geovisio/utils/link.py +14 -0
- geovisio/utils/params.py +20 -0
- geovisio/utils/pictures.py +92 -68
- geovisio/utils/reports.py +171 -0
- geovisio/utils/sequences.py +264 -126
- geovisio/utils/tokens.py +37 -42
- geovisio/utils/upload_set.py +654 -0
- geovisio/web/auth.py +37 -37
- geovisio/web/collections.py +286 -302
- geovisio/web/configuration.py +14 -0
- geovisio/web/docs.py +241 -14
- geovisio/web/excluded_areas.py +377 -0
- geovisio/web/items.py +156 -108
- geovisio/web/map.py +20 -20
- geovisio/web/params.py +69 -26
- geovisio/web/pictures.py +14 -31
- geovisio/web/reports.py +399 -0
- geovisio/web/rss.py +13 -7
- geovisio/web/stac.py +129 -134
- geovisio/web/tokens.py +98 -109
- geovisio/web/upload_set.py +768 -0
- geovisio/web/users.py +100 -73
- geovisio/web/utils.py +28 -9
- geovisio/workers/runner_pictures.py +252 -204
- {geovisio-2.6.0.dist-info → geovisio-2.7.0.dist-info}/METADATA +16 -13
- geovisio-2.7.0.dist-info/RECORD +66 -0
- geovisio-2.6.0.dist-info/RECORD +0 -41
- {geovisio-2.6.0.dist-info → geovisio-2.7.0.dist-info}/LICENSE +0 -0
- {geovisio-2.6.0.dist-info → geovisio-2.7.0.dist-info}/WHEEL +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))
|