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.
Files changed (66) hide show
  1. geovisio/__init__.py +25 -4
  2. geovisio/admin_cli/__init__.py +3 -1
  3. geovisio/admin_cli/user.py +75 -0
  4. geovisio/config_app.py +86 -4
  5. geovisio/templates/main.html +2 -2
  6. geovisio/templates/viewer.html +3 -3
  7. geovisio/translations/br/LC_MESSAGES/messages.mo +0 -0
  8. geovisio/translations/br/LC_MESSAGES/messages.po +762 -0
  9. geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
  10. geovisio/translations/da/LC_MESSAGES/messages.po +859 -0
  11. geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
  12. geovisio/translations/de/LC_MESSAGES/messages.po +106 -1
  13. geovisio/translations/el/LC_MESSAGES/messages.mo +0 -0
  14. geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
  15. geovisio/translations/en/LC_MESSAGES/messages.po +218 -133
  16. geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
  17. geovisio/translations/eo/LC_MESSAGES/messages.po +856 -0
  18. geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
  19. geovisio/translations/es/LC_MESSAGES/messages.po +4 -3
  20. geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
  21. geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
  22. geovisio/translations/fr/LC_MESSAGES/messages.po +66 -3
  23. geovisio/translations/hu/LC_MESSAGES/messages.mo +0 -0
  24. geovisio/translations/hu/LC_MESSAGES/messages.po +4 -3
  25. geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
  26. geovisio/translations/it/LC_MESSAGES/messages.po +884 -0
  27. geovisio/translations/ja/LC_MESSAGES/messages.mo +0 -0
  28. geovisio/translations/ja/LC_MESSAGES/messages.po +807 -0
  29. geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
  30. geovisio/translations/messages.pot +191 -122
  31. geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
  32. geovisio/translations/pl/LC_MESSAGES/messages.mo +0 -0
  33. geovisio/translations/pl/LC_MESSAGES/messages.po +728 -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/loggers.py +14 -0
  39. geovisio/utils/model_query.py +55 -0
  40. geovisio/utils/params.py +7 -4
  41. geovisio/utils/pictures.py +12 -43
  42. geovisio/utils/semantics.py +120 -0
  43. geovisio/utils/sequences.py +10 -1
  44. geovisio/utils/tokens.py +5 -3
  45. geovisio/utils/upload_set.py +71 -22
  46. geovisio/utils/website.py +53 -0
  47. geovisio/web/annotations.py +17 -0
  48. geovisio/web/auth.py +11 -6
  49. geovisio/web/collections.py +217 -61
  50. geovisio/web/configuration.py +17 -1
  51. geovisio/web/docs.py +67 -67
  52. geovisio/web/items.py +220 -96
  53. geovisio/web/map.py +48 -18
  54. geovisio/web/pages.py +240 -0
  55. geovisio/web/params.py +17 -0
  56. geovisio/web/prepare.py +165 -0
  57. geovisio/web/stac.py +17 -4
  58. geovisio/web/tokens.py +14 -4
  59. geovisio/web/upload_set.py +108 -14
  60. geovisio/web/users.py +203 -44
  61. geovisio/workers/runner_pictures.py +61 -22
  62. {geovisio-2.7.1.dist-info → geovisio-2.8.1.dist-info}/METADATA +8 -6
  63. geovisio-2.8.1.dist-info/RECORD +92 -0
  64. {geovisio-2.7.1.dist-info → geovisio-2.8.1.dist-info}/WHEEL +1 -1
  65. geovisio-2.7.1.dist-info/RECORD +0 -70
  66. {geovisio-2.7.1.dist-info → geovisio-2.8.1.dist-info/licenses}/LICENSE +0 -0
geovisio/__init__.py CHANGED
@@ -1,9 +1,9 @@
1
1
  """GeoVisio API - Main"""
2
2
 
3
- __version__ = "2.7.1"
3
+ __version__ = "2.8.1"
4
4
 
5
5
  import os
6
- from flask import Flask, jsonify, stream_template, send_from_directory, redirect, request
6
+ from flask import Flask, jsonify, stream_template, send_from_directory, redirect, request, url_for
7
7
  from flask.cli import with_appcontext
8
8
  from flask_cors import CORS
9
9
  from flask_compress import Compress
@@ -28,6 +28,9 @@ from geovisio.web import (
28
28
  upload_set,
29
29
  reports,
30
30
  excluded_areas,
31
+ prepare,
32
+ pages,
33
+ annotations,
31
34
  )
32
35
  from geovisio.workers import runner_pictures
33
36
 
@@ -73,12 +76,12 @@ def create_app(test_config=None, app=None):
73
76
  if app is None:
74
77
  app = Flask(__name__, instance_relative_config=True)
75
78
  CORS(app, supports_credentials=True)
76
- Compress(app)
77
79
 
78
80
  config_app.read_config(app, test_config)
79
81
  sentry.init(app)
80
82
  db.create_db_pool(app)
81
83
  Babel(app, locale_selector=get_locale)
84
+ Compress(app)
82
85
 
83
86
  # Prepare filesystem
84
87
  createDirNoFailure(app.instance_path)
@@ -88,6 +91,8 @@ def create_app(test_config=None, app=None):
88
91
  if app.config.get("DB_CHECK_SCHEMA"):
89
92
  db_migrations.update_db_schema(app.config["DB_URL"])
90
93
 
94
+ config_app.persist_config(app)
95
+
91
96
  if app.config.get("OAUTH_PROVIDER"):
92
97
  utils.auth.make_auth(app)
93
98
  app.register_blueprint(auth.bp)
@@ -101,7 +106,9 @@ def create_app(test_config=None, app=None):
101
106
  # https://flask.palletsprojects.com/en/2.2.x/deploying/proxy_fix/
102
107
  from werkzeug.middleware.proxy_fix import ProxyFix
103
108
 
104
- app.wsgi_app = ProxyFix(app.wsgi_app, x_for=nb_proxies, x_proto=nb_proxies, x_host=nb_proxies, x_prefix=nb_proxies)
109
+ app.wsgi_app = ProxyFix(
110
+ app.wsgi_app, x_for=nb_proxies, x_proto=nb_proxies, x_host=nb_proxies, x_prefix=nb_proxies, x_port=nb_proxies
111
+ )
105
112
 
106
113
  # store the background processor in the app context
107
114
  app.background_processor = runner_pictures.PictureBackgroundProcessor(app)
@@ -121,6 +128,9 @@ def create_app(test_config=None, app=None):
121
128
  app.register_blueprint(upload_set.bp)
122
129
  app.register_blueprint(reports.bp)
123
130
  app.register_blueprint(excluded_areas.bp)
131
+ app.register_blueprint(prepare.bp)
132
+ app.register_blueprint(pages.bp)
133
+ app.register_blueprint(annotations.bp)
124
134
 
125
135
  # Register CLI comands
126
136
  app.register_blueprint(admin_cli.bp, cli_group=None)
@@ -177,6 +187,17 @@ def create_app(test_config=None, app=None):
177
187
  def favicon():
178
188
  return redirect("/static/img/favicon.ico")
179
189
 
190
+ @app.route("/api/debug_headers")
191
+ def debug_headers():
192
+ """Endpoint handy when setting a new instance to check if all the headers are set correctly,
193
+ and especially the X-Forwarded-* header that needs to be set by the proxies in order for the API to correctly build internal urls.
194
+
195
+ The headers are only printed to the console, so it's only for the instance administrator that has access to those logs.
196
+ """
197
+ logging.info(request.headers)
198
+
199
+ return jsonify({"test_url": url_for("index", _external=True)}), 200
200
+
180
201
  # Errors
181
202
  @app.errorhandler(errors.InvalidAPIUsage)
182
203
  def invalid_api_usage(e):
@@ -1,12 +1,14 @@
1
1
  from flask import Blueprint
2
+ from flask import cli
2
3
  from flask.cli import with_appcontext
3
4
  import click
4
5
  import logging
5
- from . import default_account_tokens, reorder_sequences, sequence_heading, cleanup, db
6
+ from . import default_account_tokens, reorder_sequences, sequence_heading, cleanup, db, user
6
7
 
7
8
  bp = Blueprint("cli", __name__)
8
9
 
9
10
  bp.register_blueprint(default_account_tokens.bp, cli_group="default-account-tokens")
11
+ bp.register_blueprint(user.bp, cli_group=None)
10
12
  bp.register_blueprint(db.bp, cli_group="db")
11
13
 
12
14
 
@@ -0,0 +1,75 @@
1
+ from uuid import UUID
2
+ from attr import dataclass
3
+ import click
4
+ from flask import Blueprint, current_app
5
+ from flask.cli import with_appcontext
6
+ from geovisio.utils import db
7
+ from geovisio.utils.auth import AccountRole
8
+ from psycopg.rows import dict_row
9
+
10
+ bp = Blueprint("user", __name__)
11
+
12
+
13
+ @dataclass
14
+ class Account:
15
+ id: UUID
16
+ name: str
17
+
18
+
19
+ @bp.cli.command("user")
20
+ @click.argument("account_id_or_name")
21
+ @click.option("--set-role", required=True, help="Role you want to give to the account. Must be one of: admin or user")
22
+ @click.option("--create", is_flag=True, show_default=True, default=False, help="If provided, create the account if it does not exist")
23
+ @with_appcontext
24
+ def update_user(account_id_or_name, set_role=None, create=False):
25
+ """
26
+ Update some information about a user.
27
+ To identify the account, either the account_id or the account_name must be provided.
28
+ """
29
+ with db.conn(current_app) as conn, conn.cursor(row_factory=dict_row) as cursor:
30
+
31
+ account = get_account(cursor, account_id_or_name, create)
32
+
33
+ if set_role is not None:
34
+ update_role(cursor, account, set_role)
35
+
36
+
37
+ def get_account(cursor, account_id_or_name, create):
38
+ account_id = None
39
+ account_name = None
40
+ try:
41
+ account_id = UUID(account_id_or_name)
42
+ except ValueError:
43
+ account_name = account_id_or_name
44
+
45
+ if account_id is not None:
46
+ r = cursor.execute("SELECT id, name FROM accounts WHERE id = %s", [account_id]).fetchall()
47
+ elif account_name is not None:
48
+ r = cursor.execute("SELECT id, name FROM accounts WHERE name = %s", [account_name]).fetchall()
49
+ else:
50
+ raise click.ClickException("You must provide either an account_id or an account_name")
51
+
52
+ if create and not r:
53
+ if account_id is not None:
54
+ raise click.ClickException("You cannot create an account with an account_id, a name must be provided")
55
+ r = cursor.execute("INSERT INTO accounts (name) VALUES (%s) RETURNING id, name", [account_name]).fetchall()
56
+
57
+ if not r:
58
+ raise click.ClickException(f"Account {account_id_or_name} not found")
59
+ if len(r) > 1:
60
+ print(f"Several accounts found with name {account_id_or_name}")
61
+ for i in r:
62
+ print(f" * {i['id']}")
63
+ raise click.ClickException(f"Please provide an account_id instead")
64
+
65
+ return Account(id=r[0]["id"], name=r[0]["name"])
66
+
67
+
68
+ def update_role(cursor, account, role):
69
+ try:
70
+ role = AccountRole(role)
71
+ except ValueError:
72
+ raise click.ClickException(f"Role {role} is not valid. Must be one of: admin or user")
73
+
74
+ print(f"Adding role {role.value} to account {account.name} ({account.id})")
75
+ cursor.execute("UPDATE accounts SET role = %s WHERE id = %s", [role.value, account.id])
geovisio/config_app.py CHANGED
@@ -5,10 +5,15 @@ import datetime
5
5
  import logging
6
6
  from typing import Optional, Dict
7
7
  import croniter
8
- from pydantic import BaseModel
8
+ from pydantic import BaseModel, EmailStr
9
9
  from pydantic.color import Color
10
10
  from pydantic.networks import HttpUrl
11
11
  import json
12
+ from flask import Flask, current_app
13
+
14
+ from geovisio.utils import website
15
+ from geovisio.utils.model_query import get_db_params_and_values
16
+ from geovisio.utils.website import Website
12
17
 
13
18
 
14
19
  class ApiSummary(BaseModel):
@@ -16,6 +21,8 @@ class ApiSummary(BaseModel):
16
21
  description: Dict[str, str] = {"en": "The open source photo mapping solution"}
17
22
  logo: HttpUrl = "https://gitlab.com/panoramax/gitlab-profile/-/raw/main/images/logo.svg"
18
23
  color: Color = "#bf360c"
24
+ email: EmailStr = "panoramax@panoramax.fr"
25
+ geo_coverage: Dict[str, str] = {"en": "Worldwide\nThe picture can be sent from anywhere in the world."}
19
26
 
20
27
 
21
28
  class DefaultConfig:
@@ -40,6 +47,10 @@ class DefaultConfig:
40
47
  DB_MAX_CNX = 10
41
48
  DB_STATEMENT_TIMEOUT = 5 * 60 * 1000 # default statement timeout in ms (5mn)
42
49
  API_ACCEPT_DUPLICATE = False
50
+ API_ENFORCE_TOS_ACCEPTANCE = False # if True, users won't be able to upload pictures without accepting the terms of service
51
+ API_WEBSITE_URL = (
52
+ website.WEBSITE_UNDER_SAME_HOST
53
+ ) # by default we consider that there is a panoramax website on the same host as the API
43
54
 
44
55
 
45
56
  def read_config(app, test_config):
@@ -78,6 +89,9 @@ def read_config(app, test_config):
78
89
  "API_PICTURES_LICENSE_URL",
79
90
  "API_ACCEPT_DUPLICATE",
80
91
  "API_GIT_VERSION",
92
+ "API_DEFAULT_COLLABORATIVE_METADATA_EDITING",
93
+ "API_ENFORCE_TOS_ACCEPTANCE",
94
+ "API_WEBSITE_URL",
81
95
  # Picture process
82
96
  "PICTURE_PROCESS_DERIVATES_STRATEGY",
83
97
  "PICTURE_PROCESS_THREADS_LIMIT",
@@ -111,7 +125,7 @@ def read_config(app, test_config):
111
125
  "CLIENT_ID": "OAUTH_CLIENT_ID",
112
126
  "CLIENT_SECRET": "OAUTH_CLIENT_SECRET",
113
127
  "NB_PROXIES": "INFRA_NB_PROXIES",
114
- "SECRET_KEY": "FLASk_SECRET_KEY",
128
+ "SECRET_KEY": "FLASK_SECRET_KEY",
115
129
  "SESSION_COOKIE_DOMAIN": "FLASK_SESSION_COOKIE_DOMAIN",
116
130
  }
117
131
  for legacyKey, newKey in legacyVariables.items():
@@ -175,6 +189,8 @@ def read_config(app, test_config):
175
189
  f"{pageParam} variable points to invalid template '{app.config[pageParam]}' (not found in '{templateFolder}' folder)"
176
190
  )
177
191
 
192
+ app.config["API_WEBSITE_URL"] = Website(app.config.get("API_WEBSITE_URL"))
193
+
178
194
  # The default is to use only one only 1 thread to process uploaded pictures
179
195
  # if set to 0 no background worker is run, if set to -1 all cpus will be used
180
196
  app.config["PICTURE_PROCESS_THREADS_LIMIT"] = _get_threads_limit(app.config["PICTURE_PROCESS_THREADS_LIMIT"])
@@ -205,6 +221,8 @@ def read_config(app, test_config):
205
221
  raise Exception(f"PICTURE_PROCESS_REFRESH_CRON should be a valid cron syntax, got '{cron_val}'")
206
222
 
207
223
  app.config["API_ACCEPT_DUPLICATE"] = _read_bool(app.config, "API_ACCEPT_DUPLICATE")
224
+ app.config["API_ENFORCE_TOS_ACCEPTANCE"] = _read_bool(app.config, "API_ENFORCE_TOS_ACCEPTANCE")
225
+ app.config["API_DEFAULT_COLLABORATIVE_METADATA_EDITING"] = _read_bool(app.config, "API_DEFAULT_COLLABORATIVE_METADATA_EDITING")
208
226
 
209
227
  app.config["DB_STATEMENT_TIMEOUT"] = int(app.config["DB_STATEMENT_TIMEOUT"])
210
228
 
@@ -212,7 +230,21 @@ def read_config(app, test_config):
212
230
  # Add generated config vars
213
231
  #
214
232
  app.url_map.strict_slashes = False
215
- app.config["COMPRESS_MIMETYPES"].append("application/geo+json")
233
+
234
+ if app.config.get("API_COMPRESSION", True) is False:
235
+ # Note that this API_COMPRESSION variable is only used in tests
236
+ app.config["COMPRESS_MIMETYPES"] = []
237
+ else:
238
+ app.config["COMPRESS_MIMETYPES"] = [
239
+ "text/html",
240
+ "text/css",
241
+ "text/plain",
242
+ "text/xml",
243
+ "application/x-javascript",
244
+ "application/json",
245
+ "application/rss+xml",
246
+ "application/geo+json",
247
+ ]
216
248
  app.config["EXECUTOR_MAX_WORKERS"] = app.config["PICTURE_PROCESS_THREADS_LIMIT"]
217
249
  app.config["EXECUTOR_PROPAGATE_EXCEPTIONS"] = True # propagate the excecutor's exceptions, to be able to trace them
218
250
 
@@ -248,7 +280,57 @@ def _get_threads_limit(param: str) -> int:
248
280
  nb_cpu = os.cpu_count()
249
281
  if p == -1:
250
282
  if nb_cpu is None:
251
- logging.warn("Number of cpu is unknown, using only 1 thread")
283
+ logging.warning("Number of cpu is unknown, using only 1 thread")
252
284
  return 1
253
285
  return nb_cpu
254
286
  return min(p, os.cpu_count() or 1)
287
+
288
+
289
+ class DBConfiguration(BaseModel):
290
+ """Configuration persisted in the database.
291
+ Not all configurations are meant to be persisted in the database"""
292
+
293
+ collaborative_metadata: Optional[bool] = None
294
+
295
+
296
+ def persist_config(app: Flask):
297
+ """
298
+ Persist the configuration in the database if needed.
299
+
300
+ Note that the configuration can only be initialized like this, if the configuration has been changed in the database, it will not be updated using environment variables.
301
+ """
302
+ from geovisio.utils import db
303
+ from psycopg.rows import class_row
304
+ from psycopg.sql import SQL, Literal
305
+ from psycopg.errors import UndefinedTable
306
+
307
+ with db.conn(app) as conn, conn.transaction() as tr, conn.cursor(row_factory=class_row(DBConfiguration)) as cur:
308
+ try:
309
+ db_config = cur.execute("SELECT * FROM configurations LIMIT 1").fetchone()
310
+ except UndefinedTable:
311
+ logging.warning("Database schema has not been updated yet, configuration will not be persisted")
312
+ return
313
+ if not db_config:
314
+ raise Exception("Database has not been correctly initialized, there should always be a default")
315
+ config_to_persist = DBConfiguration()
316
+
317
+ # add the fields we want to persist here
318
+ collaborative_metadata = app.config["API_DEFAULT_COLLABORATIVE_METADATA_EDITING"]
319
+ if db_config.collaborative_metadata is None:
320
+ config_to_persist.collaborative_metadata = collaborative_metadata
321
+ elif db_config.collaborative_metadata != collaborative_metadata and collaborative_metadata is not None:
322
+ logging.warning(
323
+ "The environment variable `API_DEFAULT_COLLABORATIVE_METADATA_EDITING` has a different value than its value in the database, it will be ignored. Update the `collaborative_metadata` field in the database if you want to change it."
324
+ )
325
+
326
+ params_as_dict = get_db_params_and_values(config_to_persist)
327
+ fields = params_as_dict.fields_for_set()
328
+ if not params_as_dict.has_updates():
329
+ return
330
+
331
+ logging.info("Persisting configuration to the database from environement variables")
332
+ # Persist all set fields in the database
333
+ cur.execute(
334
+ SQL("UPDATE configurations SET {fields} RETURNING *").format(fields=fields),
335
+ params_as_dict.params_as_dict,
336
+ )
@@ -29,7 +29,7 @@
29
29
 
30
30
  <script>
31
31
  var baseUrl = window.location.href.replace(/\/$/, '');
32
- var viewerUrl = "https://cdn.jsdelivr.net/npm/geovisio@~3.0/build";
32
+ var viewerUrl = "https://cdn.jsdelivr.net/npm/@panoramax/web-viewer@~3.2/build";
33
33
  function encodeHTML(html) {
34
34
  return html
35
35
  .replace(/</g, "&lt;")
@@ -68,7 +68,7 @@
68
68
  +'&lt;script>\n'
69
69
  +'\t// All options available are listed here\n'
70
70
  +'\t// https://gitlab.com/panoramax/clients/web-viewer/-/blob/develop/docs/02_Usage.md\n'
71
- +'\tvar instance = new GeoVisio.default(\n'
71
+ +'\tvar instance = new Panoramax.default(\n'
72
72
  +'\t\t"viewer",\n'
73
73
  +'\t\t"'+baseUrl+'/api",\n'
74
74
  +'\t\t{ map: true }\n'
@@ -6,7 +6,7 @@
6
6
  <link rel="shortcut icon" href="/favicon.ico" />
7
7
  <link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
8
8
  <title>GeoVisio</title>
9
- <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/geovisio@~3.0/build/index.css" />
9
+ <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@panoramax/web-viewer@~3.2/build/index.css" />
10
10
  <style>
11
11
  #viewer {
12
12
  position: absolute;
@@ -22,9 +22,9 @@
22
22
  <noscript>{%trans%}You need to enable JavaScript to run this app.{%endtrans%}</noscript>
23
23
  </div>
24
24
 
25
- <script src="https://cdn.jsdelivr.net/npm/geovisio@~3.0/build/index.js"></script>
25
+ <script src="https://cdn.jsdelivr.net/npm/@panoramax/web-viewer@~3.2/build/index.js"></script>
26
26
  <script>
27
- var instance = new GeoVisio.default(
27
+ var instance = new Panoramax.default(
28
28
  "viewer",
29
29
  "/api",
30
30
  { map: { startWide: true } }