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/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """GeoVisio API - Main"""
2
2
 
3
- __version__ = "2.7.0"
3
+ __version__ = "2.8.0"
4
4
 
5
5
  import os
6
6
  from flask import Flask, jsonify, stream_template, send_from_directory, redirect, request
@@ -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
 
@@ -60,7 +63,7 @@ dictConfig(LOGGING_CONFIG)
60
63
  # Init i18n
61
64
  def get_locale():
62
65
  try:
63
- return request.accept_languages.best_match(["de", "fr", "en"]) or "en"
66
+ return request.accept_languages.best_match(["de", "fr", "en", "es", "hu"]) or "en"
64
67
  except:
65
68
  return "en"
66
69
 
@@ -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)
@@ -121,6 +126,9 @@ def create_app(test_config=None, app=None):
121
126
  app.register_blueprint(upload_set.bp)
122
127
  app.register_blueprint(reports.bp)
123
128
  app.register_blueprint(excluded_areas.bp)
129
+ app.register_blueprint(prepare.bp)
130
+ app.register_blueprint(pages.bp)
131
+ app.register_blueprint(annotations.bp)
124
132
 
125
133
  # Register CLI comands
126
134
  app.register_blueprint(admin_cli.bp, cli_group=None)
@@ -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
 
@@ -60,12 +60,12 @@ def cleanup(sequences=[], full=False, database=False, cache=False, permanentPics
60
60
  if database:
61
61
  log.info("Cleaning up database...")
62
62
  if allSequences:
63
- conn.execute("DELETE FROM pictures_to_process")
63
+ conn.execute("DELETE FROM job_queue")
64
64
  conn.execute("DELETE FROM sequences_pictures")
65
65
  conn.execute("DELETE FROM sequences")
66
66
  conn.execute("DELETE FROM pictures")
67
67
  else:
68
- conn.execute("DELETE FROM pictures_to_process WHERE picture_id = ANY(%s)", [pics])
68
+ conn.execute("DELETE FROM job_queue WHERE picture_id = ANY(%s)", [pics])
69
69
  conn.execute("DELETE FROM sequences_pictures WHERE seq_id = ANY(%s)", [sequences])
70
70
  conn.execute("DELETE FROM sequences WHERE id = ANY(%s)", [sequences])
71
71
  conn.execute("DELETE FROM pictures WHERE id = ANY(%s)", [pics])
@@ -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:
@@ -31,6 +38,7 @@ class DefaultConfig:
31
38
  DB_CHECK_SCHEMA = True # If True check the database schema, and do not start the api if not up to date
32
39
  API_PICTURES_LICENSE_SPDX_ID = None
33
40
  API_PICTURES_LICENSE_URL = None
41
+ API_WEBSITE_URL = None # URL to the website, used to generate links to the website. If not set, the API will presume the website runs from the same url.
34
42
  DEBUG_PICTURES_SKIP_FS_CHECKS_WITH_PUBLIC_URL = False
35
43
  SESSION_COOKIE_HTTPONLY = False
36
44
  PICTURE_PROCESS_REFRESH_CRON = (
@@ -40,6 +48,10 @@ class DefaultConfig:
40
48
  DB_MAX_CNX = 10
41
49
  DB_STATEMENT_TIMEOUT = 5 * 60 * 1000 # default statement timeout in ms (5mn)
42
50
  API_ACCEPT_DUPLICATE = False
51
+ API_ENFORCE_TOS_ACCEPTANCE = False # if True, users won't be able to upload pictures without accepting the terms of service
52
+ API_WEBSITE_URL = (
53
+ website.WEBSITE_UNDER_SAME_HOST
54
+ ) # by default we consider that there is a panoramax website on the same host as the API
43
55
 
44
56
 
45
57
  def read_config(app, test_config):
@@ -78,6 +90,9 @@ def read_config(app, test_config):
78
90
  "API_PICTURES_LICENSE_URL",
79
91
  "API_ACCEPT_DUPLICATE",
80
92
  "API_GIT_VERSION",
93
+ "API_DEFAULT_COLLABORATIVE_METADATA_EDITING",
94
+ "API_ENFORCE_TOS_ACCEPTANCE",
95
+ "API_WEBSITE_URL",
81
96
  # Picture process
82
97
  "PICTURE_PROCESS_DERIVATES_STRATEGY",
83
98
  "PICTURE_PROCESS_THREADS_LIMIT",
@@ -111,7 +126,7 @@ def read_config(app, test_config):
111
126
  "CLIENT_ID": "OAUTH_CLIENT_ID",
112
127
  "CLIENT_SECRET": "OAUTH_CLIENT_SECRET",
113
128
  "NB_PROXIES": "INFRA_NB_PROXIES",
114
- "SECRET_KEY": "FLASk_SECRET_KEY",
129
+ "SECRET_KEY": "FLASK_SECRET_KEY",
115
130
  "SESSION_COOKIE_DOMAIN": "FLASK_SESSION_COOKIE_DOMAIN",
116
131
  }
117
132
  for legacyKey, newKey in legacyVariables.items():
@@ -175,6 +190,8 @@ def read_config(app, test_config):
175
190
  f"{pageParam} variable points to invalid template '{app.config[pageParam]}' (not found in '{templateFolder}' folder)"
176
191
  )
177
192
 
193
+ app.config["API_WEBSITE_URL"] = Website(app.config.get("API_WEBSITE_URL"))
194
+
178
195
  # The default is to use only one only 1 thread to process uploaded pictures
179
196
  # if set to 0 no background worker is run, if set to -1 all cpus will be used
180
197
  app.config["PICTURE_PROCESS_THREADS_LIMIT"] = _get_threads_limit(app.config["PICTURE_PROCESS_THREADS_LIMIT"])
@@ -205,6 +222,8 @@ def read_config(app, test_config):
205
222
  raise Exception(f"PICTURE_PROCESS_REFRESH_CRON should be a valid cron syntax, got '{cron_val}'")
206
223
 
207
224
  app.config["API_ACCEPT_DUPLICATE"] = _read_bool(app.config, "API_ACCEPT_DUPLICATE")
225
+ app.config["API_ENFORCE_TOS_ACCEPTANCE"] = _read_bool(app.config, "API_ENFORCE_TOS_ACCEPTANCE")
226
+ app.config["API_DEFAULT_COLLABORATIVE_METADATA_EDITING"] = _read_bool(app.config, "API_DEFAULT_COLLABORATIVE_METADATA_EDITING")
208
227
 
209
228
  app.config["DB_STATEMENT_TIMEOUT"] = int(app.config["DB_STATEMENT_TIMEOUT"])
210
229
 
@@ -212,7 +231,21 @@ def read_config(app, test_config):
212
231
  # Add generated config vars
213
232
  #
214
233
  app.url_map.strict_slashes = False
215
- app.config["COMPRESS_MIMETYPES"].append("application/geo+json")
234
+
235
+ if app.config.get("API_COMPRESSION", True) is False:
236
+ # Note that this API_COMPRESSION variable is only used in tests
237
+ app.config["COMPRESS_MIMETYPES"] = []
238
+ else:
239
+ app.config["COMPRESS_MIMETYPES"] = [
240
+ "text/html",
241
+ "text/css",
242
+ "text/plain",
243
+ "text/xml",
244
+ "application/x-javascript",
245
+ "application/json",
246
+ "application/rss+xml",
247
+ "application/geo+json",
248
+ ]
216
249
  app.config["EXECUTOR_MAX_WORKERS"] = app.config["PICTURE_PROCESS_THREADS_LIMIT"]
217
250
  app.config["EXECUTOR_PROPAGATE_EXCEPTIONS"] = True # propagate the excecutor's exceptions, to be able to trace them
218
251
 
@@ -248,7 +281,57 @@ def _get_threads_limit(param: str) -> int:
248
281
  nb_cpu = os.cpu_count()
249
282
  if p == -1:
250
283
  if nb_cpu is None:
251
- logging.warn("Number of cpu is unknown, using only 1 thread")
284
+ logging.warning("Number of cpu is unknown, using only 1 thread")
252
285
  return 1
253
286
  return nb_cpu
254
287
  return min(p, os.cpu_count() or 1)
288
+
289
+
290
+ class DBConfiguration(BaseModel):
291
+ """Configuration persisted in the database.
292
+ Not all configurations are meant to be persisted in the database"""
293
+
294
+ collaborative_metadata: Optional[bool] = None
295
+
296
+
297
+ def persist_config(app: Flask):
298
+ """
299
+ Persist the configuration in the database if needed.
300
+
301
+ 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.
302
+ """
303
+ from geovisio.utils import db
304
+ from psycopg.rows import class_row
305
+ from psycopg.sql import SQL, Literal
306
+ from psycopg.errors import UndefinedTable
307
+
308
+ with db.conn(app) as conn, conn.transaction() as tr, conn.cursor(row_factory=class_row(DBConfiguration)) as cur:
309
+ try:
310
+ db_config = cur.execute("SELECT * FROM configurations LIMIT 1").fetchone()
311
+ except UndefinedTable:
312
+ logging.warning("Database schema has not been updated yet, configuration will not be persisted")
313
+ return
314
+ if not db_config:
315
+ raise Exception("Database has not been correctly initialized, there should always be a default")
316
+ config_to_persist = DBConfiguration()
317
+
318
+ # add the fields we want to persist here
319
+ collaborative_metadata = app.config["API_DEFAULT_COLLABORATIVE_METADATA_EDITING"]
320
+ if db_config.collaborative_metadata is None:
321
+ config_to_persist.collaborative_metadata = collaborative_metadata
322
+ elif db_config.collaborative_metadata != collaborative_metadata and collaborative_metadata is not None:
323
+ logging.warning(
324
+ "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."
325
+ )
326
+
327
+ params_as_dict = get_db_params_and_values(config_to_persist)
328
+ fields = params_as_dict.fields_for_set()
329
+ if not params_as_dict.has_updates():
330
+ return
331
+
332
+ logging.info("Persisting configuration to the database from environement variables")
333
+ # Persist all set fields in the database
334
+ cur.execute(
335
+ SQL("UPDATE configurations SET {fields} RETURNING *").format(fields=fields),
336
+ params_as_dict.params_as_dict,
337
+ )
@@ -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 } }