geovisio 2.7.0__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 (64) hide show
  1. geovisio/__init__.py +11 -3
  2. geovisio/admin_cli/__init__.py +3 -1
  3. geovisio/admin_cli/cleanup.py +2 -2
  4. geovisio/admin_cli/user.py +75 -0
  5. geovisio/config_app.py +87 -4
  6. geovisio/templates/main.html +2 -2
  7. geovisio/templates/viewer.html +3 -3
  8. geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
  9. geovisio/translations/da/LC_MESSAGES/messages.po +850 -0
  10. geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
  11. geovisio/translations/de/LC_MESSAGES/messages.po +235 -2
  12. geovisio/translations/el/LC_MESSAGES/messages.mo +0 -0
  13. geovisio/translations/el/LC_MESSAGES/messages.po +685 -0
  14. geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
  15. geovisio/translations/en/LC_MESSAGES/messages.po +244 -153
  16. geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
  17. geovisio/translations/eo/LC_MESSAGES/messages.po +790 -0
  18. geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
  19. geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
  20. geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
  21. geovisio/translations/fr/LC_MESSAGES/messages.po +40 -3
  22. geovisio/translations/hu/LC_MESSAGES/messages.mo +0 -0
  23. geovisio/translations/hu/LC_MESSAGES/messages.po +773 -0
  24. geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
  25. geovisio/translations/it/LC_MESSAGES/messages.po +875 -0
  26. geovisio/translations/ja/LC_MESSAGES/messages.mo +0 -0
  27. geovisio/translations/ja/LC_MESSAGES/messages.po +719 -0
  28. geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
  29. geovisio/translations/messages.pot +225 -148
  30. geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
  31. geovisio/translations/nl/LC_MESSAGES/messages.po +24 -16
  32. geovisio/translations/pl/LC_MESSAGES/messages.mo +0 -0
  33. geovisio/translations/pl/LC_MESSAGES/messages.po +727 -0
  34. geovisio/translations/zh_Hant/LC_MESSAGES/messages.mo +0 -0
  35. geovisio/translations/zh_Hant/LC_MESSAGES/messages.po +719 -0
  36. geovisio/utils/auth.py +80 -8
  37. geovisio/utils/link.py +3 -2
  38. geovisio/utils/model_query.py +55 -0
  39. geovisio/utils/pictures.py +29 -62
  40. geovisio/utils/semantics.py +120 -0
  41. geovisio/utils/sequences.py +30 -23
  42. geovisio/utils/tokens.py +5 -3
  43. geovisio/utils/upload_set.py +87 -64
  44. geovisio/utils/website.py +50 -0
  45. geovisio/web/annotations.py +17 -0
  46. geovisio/web/auth.py +9 -5
  47. geovisio/web/collections.py +235 -63
  48. geovisio/web/configuration.py +17 -1
  49. geovisio/web/docs.py +99 -54
  50. geovisio/web/items.py +233 -100
  51. geovisio/web/map.py +129 -31
  52. geovisio/web/pages.py +240 -0
  53. geovisio/web/params.py +17 -0
  54. geovisio/web/prepare.py +165 -0
  55. geovisio/web/stac.py +17 -4
  56. geovisio/web/tokens.py +14 -4
  57. geovisio/web/upload_set.py +19 -10
  58. geovisio/web/users.py +176 -44
  59. geovisio/workers/runner_pictures.py +75 -50
  60. {geovisio-2.7.0.dist-info → geovisio-2.8.0.dist-info}/METADATA +6 -5
  61. geovisio-2.8.0.dist-info/RECORD +89 -0
  62. {geovisio-2.7.0.dist-info → geovisio-2.8.0.dist-info}/WHEEL +1 -1
  63. geovisio-2.7.0.dist-info/RECORD +0 -66
  64. {geovisio-2.7.0.dist-info → geovisio-2.8.0.dist-info}/LICENSE +0 -0
geovisio/web/map.py CHANGED
@@ -5,11 +5,12 @@ import io
5
5
  from typing import Optional, Dict, Any, Tuple, List, Union
6
6
  from uuid import UUID
7
7
  from flask import Blueprint, current_app, send_file, request, jsonify, url_for
8
- from flask_babel import gettext as _
8
+ from flask_babel import gettext as _, get_locale
9
9
  from geovisio.utils import auth, db
10
10
  from geovisio.utils.auth import Account
11
11
  from geovisio.web import params
12
12
  from geovisio.web.utils import user_dependant_response
13
+ from geovisio.web.configuration import _get_translated
13
14
  from geovisio import errors
14
15
  from psycopg import sql
15
16
 
@@ -32,7 +33,9 @@ def get_style_json(forUser: Optional[Union[UUID, str]] = None):
32
33
  tilesUrl = tilesUrl.replace("11111111", "{x}").replace("22222222", "{y}").replace("33333333", "{z}")
33
34
 
34
35
  # Display sequence on all zooms if user tiles, after grid on general tiles
35
- sequenceOpacity = ["interpolate", ["linear"], ["zoom"], ZOOM_GRID_SEQUENCES, 0, ZOOM_GRID_SEQUENCES + 1, 1] if forUser is None else 1
36
+ sequenceOpacity = (
37
+ ["interpolate", ["linear"], ["zoom"], ZOOM_GRID_SEQUENCES + 0.25, 0, ZOOM_GRID_SEQUENCES + 1, 1] if forUser is None else 1
38
+ )
36
39
 
37
40
  layers = [
38
41
  {
@@ -69,35 +72,74 @@ def get_style_json(forUser: Optional[Union[UUID, str]] = None):
69
72
  layers.append(
70
73
  {
71
74
  "id": f"{sourceId}_grid",
72
- "type": "fill",
75
+ "type": "circle",
73
76
  "source": sourceId,
74
77
  "source-layer": "grid",
78
+ "layout": {
79
+ "circle-sort-key": ["get", "coef"],
80
+ },
75
81
  "paint": {
76
- "fill-color": ["interpolate", ["linear"], ["get", "coef"], 0, "#FFCC80", 0.5, "#E65100", 1, "#BF360C"],
77
- "fill-opacity": [
82
+ "circle-radius": [
78
83
  "interpolate",
79
84
  ["linear"],
80
85
  ["zoom"],
81
- 0,
82
86
  1,
87
+ # The match get coef rule allows to hide circle if coef is set to 0
88
+ ["match", ["get", "coef"], 0, 0, 1],
83
89
  ZOOM_GRID_SEQUENCES - 2,
84
- 1,
90
+ ["match", ["get", "coef"], 0, 0, 6],
91
+ ZOOM_GRID_SEQUENCES - 1,
92
+ ["match", ["get", "coef"], 0, 0, 2.5],
85
93
  ZOOM_GRID_SEQUENCES,
86
- 0.8,
87
- ZOOM_GRID_SEQUENCES + 0.5,
94
+ ["match", ["get", "coef"], 0, 0, 4],
95
+ ZOOM_GRID_SEQUENCES + 1,
96
+ ["match", ["get", "coef"], 0, 0, 7],
97
+ ],
98
+ "circle-color": ["interpolate", ["linear"], ["get", "coef"], 0, "#FFA726", 0.5, "#E65100", 1, "#3E2723"],
99
+ "circle-opacity": [
100
+ "interpolate",
101
+ ["linear"],
102
+ ["zoom"],
103
+ ZOOM_GRID_SEQUENCES - 2,
104
+ 0.5,
105
+ ZOOM_GRID_SEQUENCES - 1,
106
+ 1,
107
+ ZOOM_GRID_SEQUENCES + 0.75,
108
+ 1,
109
+ ZOOM_GRID_SEQUENCES + 1,
88
110
  0,
89
111
  ],
90
112
  },
91
113
  }
92
114
  )
93
115
 
116
+ apiSum = current_app.config["API_SUMMARY"]
117
+ userLang = get_locale().language
118
+
94
119
  style = {
95
120
  "version": 8,
96
- "name": "GeoVisio Vector Tiles",
121
+ "name": _get_translated(apiSum.name, userLang)["label"],
122
+ "metadata": {
123
+ "panoramax:fields": {
124
+ "sequences": ["id", "account_id", "model", "type", "date", "gps_accuracy", "h_pixel_density"],
125
+ "pictures": ["id", "account_id", "ts", "heading", "sequences", "type", "model", "gps_accuracy", "h_pixel_density"],
126
+ }
127
+ },
97
128
  "sources": {sourceId: {"type": "vector", "tiles": [tilesUrl], "minzoom": 0, "maxzoom": ZOOM_PICTURES}},
98
129
  "layers": layers,
99
130
  }
100
131
 
132
+ if forUser is None:
133
+ style["metadata"]["panoramax:fields"]["grid"] = [
134
+ "id",
135
+ "nb_pictures",
136
+ "nb_360_pictures",
137
+ "nb_flat_pictures",
138
+ "coef",
139
+ "coef_360_pictures",
140
+ "coef_flat_pictures",
141
+ ]
142
+
101
143
  return jsonify(style)
102
144
 
103
145
 
@@ -173,34 +215,44 @@ def getStyle():
173
215
  def getTile(z: int, x: int, y: int, format: str):
174
216
  """Get pictures and sequences as vector tiles
175
217
 
176
- 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)
177
230
 
178
231
  Layer "sequences":
179
- - Available on zoom levels >= 6
232
+ - Available on zoom levels >= 7 (and simplified version on zoom >= 6 and < 7)
180
233
  - Available properties:
181
234
  - id (sequence ID)
182
235
  - account_id
183
236
  - model (camera make and model)
184
237
  - type (flat or equirectangular)
185
238
  - date (capture date, as YYYY-MM-DD)
239
+ - gps_accuracy (95% confidence interval of GPS position precision, in meters)
240
+ - h_pixel_density (number of pixels on horizon per field of view degree)
186
241
 
187
242
  Layer "pictures":
188
- - Available on zoom levels >= 13
243
+ - Available on zoom levels >= 15
189
244
  - Available properties:
190
245
  - id (picture ID)
191
246
  - account_id
192
247
  - ts (picture date/time)
193
248
  - heading (picture heading in degrees)
194
- - sequences (list of sequences ID this pictures belongs to)
195
249
  - type (flat or equirectangular)
250
+ - hidden (picture visibility, true or false)
196
251
  - model (camera make and model)
197
-
198
- Layer "grid":
199
- - Available on zoom levels 0 to 5 (included)
200
- - Available properties:
201
- - id
202
- - nb_pictures
203
- - coef (value from 0 to 1, relative quantity of available pictures)
252
+ - gps_accuracy (95% confidence interval of GPS position precision, in meters)
253
+ - h_pixel_density (number of pixels on horizon per field of view degree)
254
+ - sequences (list of sequences ID this pictures belongs to)
255
+ - first_sequence (sequence ID, first from the list)
204
256
 
205
257
  ---
206
258
  tags:
@@ -210,7 +262,7 @@ def getTile(z: int, x: int, y: int, format: str):
210
262
  parameters:
211
263
  - name: z
212
264
  in: path
213
- description: Zoom level (6 to 14)
265
+ description: Zoom level (6 to 15)
214
266
  required: true
215
267
  schema:
216
268
  type: number
@@ -294,16 +346,30 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
294
346
  #
295
347
 
296
348
  grid_fields = [
297
- sql.SQL("ST_AsMVTGeom(ST_Transform(geom, 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom"),
349
+ sql.SQL("ST_AsMVTGeom(ST_Transform(ST_Centroid(geom), 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom"),
298
350
  sql.SQL("id"),
299
351
  sql.SQL("nb_pictures"),
352
+ sql.SQL("nb_360_pictures"),
353
+ sql.SQL("nb_pictures - nb_360_pictures AS nb_flat_pictures"),
300
354
  sql.SQL(
301
- """
302
- ((CASE WHEN nb_pictures <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid)
355
+ """((CASE WHEN nb_pictures = 0 THEN 0 WHEN nb_pictures <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid)
303
356
  THEN nb_pictures::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid) * 0.5
304
357
  ELSE 0.5 + nb_pictures::float / (SELECT MAX(nb_pictures) FROM pictures_grid) * 0.5
305
- END) * 10)::int / 10::float AS coef
306
- """
358
+ END) * 10)::int / 10::float AS coef"""
359
+ ),
360
+ sql.SQL(
361
+ """((CASE WHEN nb_360_pictures = 0 THEN 0
362
+ WHEN nb_360_pictures <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) from pictures_grid)
363
+ THEN nb_360_pictures::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) from pictures_grid) * 0.5
364
+ ELSE 0.5 + nb_360_pictures::float / (SELECT MAX(nb_360_pictures) FROM pictures_grid) * 0.5
365
+ END) * 10)::int / 10::float AS coef_360_pictures"""
366
+ ),
367
+ sql.SQL(
368
+ """((CASE WHEN (nb_pictures - nb_360_pictures) = 0 THEN 0
369
+ WHEN (nb_pictures - nb_360_pictures) <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) from pictures_grid)
370
+ THEN (nb_pictures - nb_360_pictures)::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) from pictures_grid) * 0.5
371
+ ELSE 0.5 + (nb_pictures - nb_360_pictures)::float / (SELECT MAX((nb_pictures - nb_360_pictures)) FROM pictures_grid) * 0.5
372
+ END) * 10)::int / 10::float AS coef_flat_pictures"""
307
373
  ),
308
374
  ]
309
375
  sequences_fields = [
@@ -322,6 +388,8 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
322
388
  sql.SQL("computed_model AS model"),
323
389
  sql.SQL("computed_type AS type"),
324
390
  sql.SQL("computed_capture_date AS date"),
391
+ sql.SQL("computed_gps_accuracy AS gps_accuracy"),
392
+ sql.SQL("computed_h_pixel_density AS h_pixel_density"),
325
393
  ]
326
394
  )
327
395
 
@@ -351,15 +419,18 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
351
419
  ST_AsMVTGeom(ST_Transform(p.geom, 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom,
352
420
  p.id, p.ts, p.heading, p.account_id,
353
421
  NULLIF(p.status != 'ready' OR s.status != 'ready', FALSE) AS hidden,
354
- array_to_json(ARRAY_AGG(sp.seq_id)) AS sequences,
355
422
  p.metadata->>'type' AS type,
356
- TRIM(CONCAT(p.metadata->>'make', ' ', p.metadata->>'model')) AS model
423
+ TRIM(CONCAT(p.metadata->>'make', ' ', p.metadata->>'model')) AS model,
424
+ gps_accuracy_m AS gps_accuracy,
425
+ h_pixel_density,
426
+ array_to_json(ARRAY_AGG(sp.seq_id)) AS sequences,
427
+ MIN(sp.seq_id::varchar) AS first_sequence
357
428
  FROM pictures p
358
429
  LEFT JOIN sequences_pictures sp ON p.id = sp.pic_id
359
430
  LEFT JOIN sequences s ON s.id = sp.seq_id
360
431
  WHERE
361
432
  {pictures_filter}
362
- GROUP BY 1, 2, 3, 4, 5, 6
433
+ GROUP BY 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
363
434
  ) mvtgeompics
364
435
  ) mvtpictures
365
436
  """
@@ -468,7 +539,34 @@ def getUserStyle(userId: UUID):
468
539
  @user_dependant_response(True)
469
540
  def getUserTile(userId: UUID, z: int, x: int, y: int, format: str):
470
541
  """Get pictures and sequences as vector tiles for a specific user.
471
- 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)
472
570
 
473
571
  ---
474
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