goodmap 1.3.1__py3-none-any.whl → 1.5.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.
- goodmap/admin_api.py +6 -2
- goodmap/core_api.py +64 -28
- goodmap/feature_flags.py +7 -0
- goodmap/goodmap.py +21 -18
- {goodmap-1.3.1.dist-info → goodmap-1.5.0.dist-info}/METADATA +3 -3
- {goodmap-1.3.1.dist-info → goodmap-1.5.0.dist-info}/RECORD +8 -7
- {goodmap-1.3.1.dist-info → goodmap-1.5.0.dist-info}/LICENSE.md +0 -0
- {goodmap-1.3.1.dist-info → goodmap-1.5.0.dist-info}/WHEEL +0 -0
goodmap/admin_api.py
CHANGED
|
@@ -34,7 +34,9 @@ def _clean_model_name(model: Type[Any]) -> str:
|
|
|
34
34
|
def _handle_location_validation_error(e: LocationValidationError):
|
|
35
35
|
"""Handle LocationValidationError and return appropriate response."""
|
|
36
36
|
logger.warning(
|
|
37
|
-
"Location validation failed",
|
|
37
|
+
"Location validation failed - uuid: %s, errors: %s",
|
|
38
|
+
e.uuid,
|
|
39
|
+
e.validation_errors,
|
|
38
40
|
extra={"uuid": e.uuid, "errors": e.validation_errors},
|
|
39
41
|
)
|
|
40
42
|
return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
|
|
@@ -126,7 +128,9 @@ def _update_suggestion_handler(database, suggestion_id):
|
|
|
126
128
|
database.update_suggestion(suggestion_id, status)
|
|
127
129
|
except LocationValidationError as e:
|
|
128
130
|
logger.warning(
|
|
129
|
-
"Location validation failed in suggestion",
|
|
131
|
+
"Location validation failed in suggestion - uuid: %s, errors: %s",
|
|
132
|
+
e.uuid,
|
|
133
|
+
e.validation_errors,
|
|
130
134
|
extra={"uuid": e.uuid, "errors": e.validation_errors},
|
|
131
135
|
)
|
|
132
136
|
return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
|
goodmap/core_api.py
CHANGED
|
@@ -7,7 +7,9 @@ import numpy
|
|
|
7
7
|
import pysupercluster
|
|
8
8
|
from flask import Blueprint, jsonify, make_response, request
|
|
9
9
|
from flask_babel import gettext
|
|
10
|
-
from platzky
|
|
10
|
+
from platzky import FeatureFlagSet
|
|
11
|
+
from platzky.attachment import AttachmentProtocol
|
|
12
|
+
from platzky.config import AttachmentConfig, LanguagesMapping
|
|
11
13
|
from spectree import Response, SpecTree
|
|
12
14
|
|
|
13
15
|
from goodmap.api_models import (
|
|
@@ -23,6 +25,7 @@ from goodmap.clustering import (
|
|
|
23
25
|
match_clusters_uuids,
|
|
24
26
|
)
|
|
25
27
|
from goodmap.exceptions import LocationValidationError
|
|
28
|
+
from goodmap.feature_flags import CategoriesHelp
|
|
26
29
|
from goodmap.formatter import prepare_pin
|
|
27
30
|
from goodmap.json_security import (
|
|
28
31
|
MAX_JSON_DEPTH_LOCATION,
|
|
@@ -80,15 +83,21 @@ def core_pages(
|
|
|
80
83
|
notifier_function,
|
|
81
84
|
csrf_generator,
|
|
82
85
|
location_model,
|
|
83
|
-
|
|
86
|
+
photo_attachment_class: type[AttachmentProtocol],
|
|
87
|
+
photo_attachment_config: AttachmentConfig,
|
|
88
|
+
feature_flags: FeatureFlagSet,
|
|
84
89
|
) -> Blueprint:
|
|
85
90
|
core_api_blueprint = Blueprint("api", __name__, url_prefix="/api")
|
|
86
91
|
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
# Build photo error message from config
|
|
93
|
+
allowed_ext = ", ".join(sorted(photo_attachment_config.allowed_extensions or []))
|
|
94
|
+
max_size_mb = photo_attachment_config.max_size / (1024 * 1024)
|
|
95
|
+
error_invalid_photo = (
|
|
96
|
+
f"Invalid photo. Allowed formats: {allowed_ext}. Max size: {max_size_mb:.0f}MB."
|
|
97
|
+
)
|
|
90
98
|
|
|
91
|
-
|
|
99
|
+
# Initialize Spectree for API documentation and validation
|
|
100
|
+
def _clean_model_name(model: type) -> str:
|
|
92
101
|
return model.__name__
|
|
93
102
|
|
|
94
103
|
spec = SpecTree(
|
|
@@ -111,6 +120,9 @@ def core_pages(
|
|
|
111
120
|
import json as json_lib
|
|
112
121
|
|
|
113
122
|
try:
|
|
123
|
+
# Initialize photo attachment (only populated for multipart/form-data)
|
|
124
|
+
photo_attachment = None
|
|
125
|
+
|
|
114
126
|
# Handle both multipart/form-data (with file uploads) and JSON
|
|
115
127
|
if request.content_type and request.content_type.startswith("multipart/form-data"):
|
|
116
128
|
# Parse form data dynamically
|
|
@@ -146,8 +158,24 @@ def core_pages(
|
|
|
146
158
|
# If not JSON, use as-is (simple string values)
|
|
147
159
|
suggested_location[key] = value
|
|
148
160
|
|
|
149
|
-
#
|
|
150
|
-
|
|
161
|
+
# Extract and validate photo attachment if present
|
|
162
|
+
photo_file = request.files.get("photo")
|
|
163
|
+
if photo_file and photo_file.filename:
|
|
164
|
+
photo_content = photo_file.read()
|
|
165
|
+
photo_mime = photo_file.content_type or "application/octet-stream"
|
|
166
|
+
|
|
167
|
+
# Validate using configured Attachment class
|
|
168
|
+
try:
|
|
169
|
+
photo_attachment = photo_attachment_class(
|
|
170
|
+
photo_file.filename, photo_content, photo_mime
|
|
171
|
+
)
|
|
172
|
+
except ValueError as e:
|
|
173
|
+
logger.warning(
|
|
174
|
+
"Rejected photo: %s",
|
|
175
|
+
e,
|
|
176
|
+
extra={"photo_filename": photo_file.filename},
|
|
177
|
+
)
|
|
178
|
+
return make_response(jsonify({"message": error_invalid_photo}), 400)
|
|
151
179
|
else:
|
|
152
180
|
# Parse JSON data with security checks (depth/size protection)
|
|
153
181
|
raw_data = request.get_data(as_text=True)
|
|
@@ -186,10 +214,18 @@ def core_pages(
|
|
|
186
214
|
database.add_suggestion(location.model_dump())
|
|
187
215
|
message = gettext("A new location has been suggested with details")
|
|
188
216
|
notifier_message = f"{message}: {json_lib.dumps(suggested_location, indent=2)}"
|
|
189
|
-
|
|
217
|
+
attachments = [photo_attachment] if photo_attachment else None
|
|
218
|
+
notifier_function(notifier_message, attachments=attachments)
|
|
190
219
|
except LocationValidationError as e:
|
|
220
|
+
# NOTE: validation_errors includes input values from the location model fields:
|
|
221
|
+
# - Core fields: position (lat/long), uuid, remark
|
|
222
|
+
# - Dynamic fields: categories and obligatory_fields configured per deployment
|
|
223
|
+
# These are geographic/categorical data, NOT PII (no email, phone, names of people).
|
|
224
|
+
# Safe to log for debugging. If PII fields are ever added to the location model,
|
|
225
|
+
# strip 'input' from validation_errors before logging.
|
|
191
226
|
logger.warning(
|
|
192
|
-
"Location validation failed in suggest endpoint",
|
|
227
|
+
"Location validation failed in suggest endpoint: %s",
|
|
228
|
+
e.validation_errors,
|
|
193
229
|
extra={"errors": e.validation_errors},
|
|
194
230
|
)
|
|
195
231
|
return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
|
|
@@ -349,15 +385,15 @@ def core_pages(
|
|
|
349
385
|
raw_categories = database.get_categories()
|
|
350
386
|
categories = make_tuple_translation(raw_categories)
|
|
351
387
|
|
|
352
|
-
if not feature_flags
|
|
388
|
+
if CategoriesHelp not in feature_flags:
|
|
353
389
|
return jsonify(categories)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
390
|
+
|
|
391
|
+
category_data = database.get_category_data()
|
|
392
|
+
categories_help = category_data.get("categories_help")
|
|
393
|
+
proper_categories_help = []
|
|
394
|
+
if categories_help is not None:
|
|
395
|
+
for option in categories_help:
|
|
396
|
+
proper_categories_help.append({option: gettext(f"categories_help_{option}")})
|
|
361
397
|
|
|
362
398
|
return jsonify({"categories": categories, "categories_help": proper_categories_help})
|
|
363
399
|
|
|
@@ -381,7 +417,7 @@ def core_pages(
|
|
|
381
417
|
"options": make_tuple_translation(options),
|
|
382
418
|
}
|
|
383
419
|
|
|
384
|
-
if feature_flags
|
|
420
|
+
if CategoriesHelp in feature_flags:
|
|
385
421
|
option_help_list = categories_options_help.get(key, [])
|
|
386
422
|
proper_options_help = []
|
|
387
423
|
for option in option_help_list:
|
|
@@ -394,7 +430,7 @@ def core_pages(
|
|
|
394
430
|
|
|
395
431
|
response = {"categories": result}
|
|
396
432
|
|
|
397
|
-
if feature_flags
|
|
433
|
+
if CategoriesHelp in feature_flags:
|
|
398
434
|
categories_help = categories_data.get("categories_help", [])
|
|
399
435
|
proper_categories_help = []
|
|
400
436
|
for option in categories_help:
|
|
@@ -432,15 +468,15 @@ def core_pages(
|
|
|
432
468
|
proper_categories_options_help.append(
|
|
433
469
|
{option: gettext(f"categories_options_help_{option}")}
|
|
434
470
|
)
|
|
435
|
-
if not feature_flags
|
|
471
|
+
if CategoriesHelp not in feature_flags:
|
|
436
472
|
return jsonify(local_data)
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
473
|
+
|
|
474
|
+
return jsonify(
|
|
475
|
+
{
|
|
476
|
+
"categories_options": local_data,
|
|
477
|
+
"categories_options_help": proper_categories_options_help,
|
|
478
|
+
}
|
|
479
|
+
)
|
|
444
480
|
|
|
445
481
|
# Register Spectree with blueprint after all routes are defined
|
|
446
482
|
spec.register(core_api_blueprint)
|
goodmap/feature_flags.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from platzky import FeatureFlag
|
|
2
|
+
|
|
3
|
+
CategoriesHelp = FeatureFlag(alias="CATEGORIES_HELP", description="Show category help text")
|
|
4
|
+
UseLazyLoading = FeatureFlag(
|
|
5
|
+
alias="USE_LAZY_LOADING", description="Enable lazy loading of location fields"
|
|
6
|
+
)
|
|
7
|
+
EnableAdminPanel = FeatureFlag(alias="ENABLE_ADMIN_PANEL", description="Enable admin panel")
|
goodmap/goodmap.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""Goodmap engine with location management and admin interface."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import os
|
|
4
5
|
|
|
5
6
|
from flask import Blueprint, redirect, render_template, session
|
|
6
7
|
from flask_wtf.csrf import CSRFProtect, generate_csrf
|
|
7
8
|
from platzky import platzky
|
|
8
|
-
from platzky.
|
|
9
|
+
from platzky.attachment import create_attachment_class
|
|
10
|
+
from platzky.config import AttachmentConfig, languages_dict
|
|
9
11
|
from platzky.models import CmsModule
|
|
10
12
|
|
|
11
13
|
from goodmap.admin_api import admin_pages
|
|
@@ -16,6 +18,9 @@ from goodmap.db import (
|
|
|
16
18
|
extend_db_with_goodmap_queries,
|
|
17
19
|
get_location_obligatory_fields,
|
|
18
20
|
)
|
|
21
|
+
from goodmap.feature_flags import EnableAdminPanel, UseLazyLoading
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
def create_app(config_path: str) -> platzky.Engine:
|
|
@@ -31,20 +36,6 @@ def create_app(config_path: str) -> platzky.Engine:
|
|
|
31
36
|
return create_app_from_config(config)
|
|
32
37
|
|
|
33
38
|
|
|
34
|
-
# TODO Checking if there is a feature flag secition should be part of configs logic not client app
|
|
35
|
-
def is_feature_enabled(config: GoodmapConfig, feature: str) -> bool:
|
|
36
|
-
"""Check if a feature flag is enabled in the configuration.
|
|
37
|
-
|
|
38
|
-
Args:
|
|
39
|
-
config: Goodmap configuration object
|
|
40
|
-
feature: Name of the feature flag to check
|
|
41
|
-
|
|
42
|
-
Returns:
|
|
43
|
-
bool: True if feature is enabled, False otherwise
|
|
44
|
-
"""
|
|
45
|
-
return config.feature_flags.get(feature, False) if config.feature_flags else False
|
|
46
|
-
|
|
47
|
-
|
|
48
39
|
def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
|
|
49
40
|
"""Create and configure Goodmap application from config object.
|
|
50
41
|
|
|
@@ -69,7 +60,7 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
|
|
|
69
60
|
if "MAX_CONTENT_LENGTH" not in app.config:
|
|
70
61
|
app.config["MAX_CONTENT_LENGTH"] = 100 * 1024 # 100KB
|
|
71
62
|
|
|
72
|
-
if
|
|
63
|
+
if app.is_enabled(UseLazyLoading):
|
|
73
64
|
location_obligatory_fields = get_location_obligatory_fields(app.db)
|
|
74
65
|
# Extend db with goodmap queries first so we can use the bound method
|
|
75
66
|
location_model = create_location_model(location_obligatory_fields, {})
|
|
@@ -97,12 +88,24 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
|
|
|
97
88
|
|
|
98
89
|
CSRFProtect(app)
|
|
99
90
|
|
|
91
|
+
# Create Attachment class for photo uploads
|
|
92
|
+
# JPEG-only: universal browser/device support, good compression for location photos,
|
|
93
|
+
# no transparency needed. PNG/WebP can be added if user demand warrants it.
|
|
94
|
+
photo_attachment_config = AttachmentConfig(
|
|
95
|
+
allowed_mime_types=frozenset({"image/jpeg"}),
|
|
96
|
+
allowed_extensions=frozenset({"jpg", "jpeg"}),
|
|
97
|
+
max_size=5 * 1024 * 1024, # 5MB - reasonable for location photos
|
|
98
|
+
)
|
|
99
|
+
PhotoAttachment = create_attachment_class(photo_attachment_config)
|
|
100
|
+
|
|
100
101
|
cp = core_pages(
|
|
101
102
|
app.db,
|
|
102
103
|
languages_dict(config.languages),
|
|
103
104
|
app.notify,
|
|
104
105
|
generate_csrf,
|
|
105
106
|
location_model,
|
|
107
|
+
photo_attachment_class=PhotoAttachment,
|
|
108
|
+
photo_attachment_config=photo_attachment_config,
|
|
106
109
|
feature_flags=config.feature_flags,
|
|
107
110
|
)
|
|
108
111
|
app.register_blueprint(cp)
|
|
@@ -159,7 +162,7 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
|
|
|
159
162
|
Returns:
|
|
160
163
|
Rendered goodmap-admin.html template or redirect to login
|
|
161
164
|
"""
|
|
162
|
-
if not
|
|
165
|
+
if not app.is_enabled(EnableAdminPanel):
|
|
163
166
|
return redirect("/")
|
|
164
167
|
|
|
165
168
|
user = session.get("user", None)
|
|
@@ -178,7 +181,7 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
|
|
|
178
181
|
|
|
179
182
|
app.register_blueprint(goodmap)
|
|
180
183
|
|
|
181
|
-
if
|
|
184
|
+
if app.is_enabled(EnableAdminPanel):
|
|
182
185
|
admin_bp = admin_pages(app.db, location_model)
|
|
183
186
|
app.register_blueprint(admin_bp)
|
|
184
187
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: goodmap
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Map engine to serve all the people :)
|
|
5
5
|
Author: Krzysztof Kolodzinski
|
|
6
6
|
Author-email: krzysztof.kolodzinski@problematy.pl
|
|
@@ -20,11 +20,11 @@ Requires-Dist: aiohttp (>=3.8.4,<4.0.0)
|
|
|
20
20
|
Requires-Dist: deprecation (>=2.1.0,<3.0.0)
|
|
21
21
|
Requires-Dist: google-cloud-storage (>=2.7.0,<3.0.0)
|
|
22
22
|
Requires-Dist: gql (>=3.4.0,<4.0.0)
|
|
23
|
-
Requires-Dist: gunicorn (>=20.1,<
|
|
23
|
+
Requires-Dist: gunicorn (>=20.1,<25.0)
|
|
24
24
|
Requires-Dist: humanize (>=4.6.0,<5.0.0)
|
|
25
25
|
Requires-Dist: myst-parser (>=4.0.0,<5.0.0) ; extra == "docs"
|
|
26
26
|
Requires-Dist: numpy (>=2.2.0,<3.0.0)
|
|
27
|
-
Requires-Dist: platzky (>=1.
|
|
27
|
+
Requires-Dist: platzky (>=1.4.1,<2.0.0)
|
|
28
28
|
Requires-Dist: pydantic (>=2.7.1,<3.0.0)
|
|
29
29
|
Requires-Dist: pysupercluster-problematy (>=0.7.8,<0.8.0)
|
|
30
30
|
Requires-Dist: scipy (>=1.15.1,<2.0.0)
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
goodmap/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
2
|
-
goodmap/admin_api.py,sha256=
|
|
2
|
+
goodmap/admin_api.py,sha256=5tvHeqknG8WmhBYmIHlQHTOUA-zaT8FKaAyyLdvX2EE,10290
|
|
3
3
|
goodmap/api_models.py,sha256=Bv4OTGuckNneCrxaQ1Y_PMeu7YFLvGUqU2EorvDlUjY,3438
|
|
4
4
|
goodmap/clustering.py,sha256=ULB-fPNOUDblgpBK4vzuo0o2yqIcvG84F3R6Za2X_l4,2905
|
|
5
5
|
goodmap/config.py,sha256=CsmC1zuvVab90VW50dtARHbFJpy2vfsIfbque8Zgc-U,1313
|
|
6
6
|
goodmap/core.py,sha256=AgdGLfeJvL7TlTX893NR2YdCS8EuXx93Gx6ndvWws7s,2673
|
|
7
|
-
goodmap/core_api.py,sha256=
|
|
7
|
+
goodmap/core_api.py,sha256=CnQHXzSxym-URHE4teVkyOAT-iIMWWEX0cP7R_01Tg4,19981
|
|
8
8
|
goodmap/data_models/location.py,sha256=_I27R06ovEL9ctv_SZ3yoLL-RwmyE3VDsVOG4a89q50,6798
|
|
9
9
|
goodmap/data_validator.py,sha256=lBmVAPxvSmEOdUGeVYSjUvVVmKfPyq4CWoHfczTtEMM,4090
|
|
10
10
|
goodmap/db.py,sha256=TcqYGbK5yk6S735Si1AzjNqcbB1nsd9pFGOy5qN9Vec,46589
|
|
11
11
|
goodmap/exceptions.py,sha256=jkFAUoc5LHk8iPjxHxbcRp8W6qFCSEA25A8XaSwxwyo,2906
|
|
12
|
+
goodmap/feature_flags.py,sha256=-hiqTX4OlhfY_4M1Kvy-_z1Fx6YTaFi3SVGYa0Pamcw,334
|
|
12
13
|
goodmap/formatter.py,sha256=4rqcg9A9Y9opAi7eb8kMDdUC03M3uzZgCxx29cvvIag,1403
|
|
13
|
-
goodmap/goodmap.py,sha256=
|
|
14
|
+
goodmap/goodmap.py,sha256=f69aUloRe4bpx2JRwZFiHOeUSk0Exq-Qv2FCdwiwLA0,7541
|
|
14
15
|
goodmap/json_security.py,sha256=EHAxNlb16AVwphgf4F7yObtMZpbR9M538dwn_STRcMo,3275
|
|
15
16
|
goodmap/templates/goodmap-admin.html,sha256=LSiOZ9-n29CnlfVNwdgmXwT7Xe7t5gvGh1xSrFGqOIY,35669
|
|
16
17
|
goodmap/templates/map.html,sha256=Uk7FFrZwvHZvG0DDaQrGW5ZrIMD21XrJzMub76uIlAg,4348
|
|
17
|
-
goodmap-1.
|
|
18
|
-
goodmap-1.
|
|
19
|
-
goodmap-1.
|
|
20
|
-
goodmap-1.
|
|
18
|
+
goodmap-1.5.0.dist-info/LICENSE.md,sha256=nkCQOR7uheLRvHRfXmwx9LhBnMcPeBU9d4ebLojDiQU,1067
|
|
19
|
+
goodmap-1.5.0.dist-info/METADATA,sha256=PpLVvzHAcIIKEUVAqBkTZ1zcLXNnFFUivPUFIieiu6k,5798
|
|
20
|
+
goodmap-1.5.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
21
|
+
goodmap-1.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|