geovisio 2.7.1__py3-none-any.whl → 2.8.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.
- geovisio/__init__.py +25 -4
- geovisio/admin_cli/__init__.py +3 -1
- geovisio/admin_cli/user.py +75 -0
- geovisio/config_app.py +86 -4
- geovisio/templates/main.html +2 -2
- geovisio/templates/viewer.html +3 -3
- geovisio/translations/br/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/br/LC_MESSAGES/messages.po +762 -0
- geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/da/LC_MESSAGES/messages.po +859 -0
- geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/de/LC_MESSAGES/messages.po +106 -1
- geovisio/translations/el/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/en/LC_MESSAGES/messages.po +218 -133
- geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/eo/LC_MESSAGES/messages.po +856 -0
- geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/es/LC_MESSAGES/messages.po +4 -3
- geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.po +66 -3
- geovisio/translations/hu/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/hu/LC_MESSAGES/messages.po +4 -3
- geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/it/LC_MESSAGES/messages.po +884 -0
- geovisio/translations/ja/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ja/LC_MESSAGES/messages.po +807 -0
- geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/messages.pot +191 -122
- geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/pl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/pl/LC_MESSAGES/messages.po +728 -0
- geovisio/translations/zh_Hant/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/zh_Hant/LC_MESSAGES/messages.po +719 -0
- geovisio/utils/auth.py +80 -8
- geovisio/utils/link.py +3 -2
- geovisio/utils/loggers.py +14 -0
- geovisio/utils/model_query.py +55 -0
- geovisio/utils/params.py +7 -4
- geovisio/utils/pictures.py +12 -43
- geovisio/utils/semantics.py +120 -0
- geovisio/utils/sequences.py +10 -1
- geovisio/utils/tokens.py +5 -3
- geovisio/utils/upload_set.py +71 -22
- geovisio/utils/website.py +53 -0
- geovisio/web/annotations.py +17 -0
- geovisio/web/auth.py +11 -6
- geovisio/web/collections.py +217 -61
- geovisio/web/configuration.py +17 -1
- geovisio/web/docs.py +67 -67
- geovisio/web/items.py +220 -96
- geovisio/web/map.py +48 -18
- geovisio/web/pages.py +240 -0
- geovisio/web/params.py +17 -0
- geovisio/web/prepare.py +165 -0
- geovisio/web/stac.py +17 -4
- geovisio/web/tokens.py +14 -4
- geovisio/web/upload_set.py +108 -14
- geovisio/web/users.py +203 -44
- geovisio/workers/runner_pictures.py +61 -22
- {geovisio-2.7.1.dist-info → geovisio-2.8.1.dist-info}/METADATA +8 -6
- geovisio-2.8.1.dist-info/RECORD +92 -0
- {geovisio-2.7.1.dist-info → geovisio-2.8.1.dist-info}/WHEEL +1 -1
- geovisio-2.7.1.dist-info/RECORD +0 -70
- {geovisio-2.7.1.dist-info → geovisio-2.8.1.dist-info/licenses}/LICENSE +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 :
|
|
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 >=
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
geovisio/web/prepare.py
ADDED
|
@@ -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=
|
|
89
|
-
description=
|
|
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"] += [
|
|
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
|
|