goodmap 1.4.0__py3-none-any.whl → 1.6.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/api_models.py CHANGED
@@ -14,7 +14,9 @@ class LocationReportRequest(BaseModel):
14
14
  """Request model for reporting a location issue."""
15
15
 
16
16
  id: str = Field(..., description="Location UUID to report")
17
- description: str = Field(..., min_length=1, description="Description of the problem")
17
+ description: str = Field(
18
+ ..., min_length=1, max_length=500, description="Description of the problem"
19
+ )
18
20
 
19
21
 
20
22
  class LocationReportResponse(BaseModel):
goodmap/core_api.py CHANGED
@@ -7,6 +7,7 @@ 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 import FeatureFlagSet
10
11
  from platzky.attachment import AttachmentProtocol
11
12
  from platzky.config import AttachmentConfig, LanguagesMapping
12
13
  from spectree import Response, SpecTree
@@ -24,6 +25,7 @@ from goodmap.clustering import (
24
25
  match_clusters_uuids,
25
26
  )
26
27
  from goodmap.exceptions import LocationValidationError
28
+ from goodmap.feature_flags import CategoriesHelp
27
29
  from goodmap.formatter import prepare_pin
28
30
  from goodmap.json_security import (
29
31
  MAX_JSON_DEPTH_LOCATION,
@@ -38,14 +40,29 @@ MAX_ZOOM = 16
38
40
  CLUSTER_RADIUS = 200
39
41
  CLUSTER_EXTENT = 512
40
42
 
43
+ # Report description validation constants
44
+ MAX_DESCRIPTION_LENGTH = 500
45
+
41
46
  # Error message constants
42
47
  ERROR_INVALID_REQUEST_DATA = "Invalid request data"
43
48
  ERROR_INVALID_LOCATION_DATA = "Invalid location data"
44
49
  ERROR_LOCATION_NOT_FOUND = "Location not found"
50
+ ERROR_INVALID_DESCRIPTION = "Invalid report description"
45
51
 
46
52
  logger = logging.getLogger(__name__)
47
53
 
48
54
 
55
+ @deprecation.deprecated(
56
+ deprecated_in="1.5.0",
57
+ removed_in="2.0.0",
58
+ details="Configure 'reported_issue_types' in the database instead. "
59
+ "The hardcoded fallback will be removed in a future release.",
60
+ )
61
+ def get_default_issue_options():
62
+ """Return hardcoded fallback issue options for backward compatibility."""
63
+ return ["notHere", "overload", "broken", "other"]
64
+
65
+
49
66
  def make_tuple_translation(keys_to_translate):
50
67
  return [(x, gettext(x)) for x in keys_to_translate]
51
68
 
@@ -83,7 +100,7 @@ def core_pages(
83
100
  location_model,
84
101
  photo_attachment_class: type[AttachmentProtocol],
85
102
  photo_attachment_config: AttachmentConfig,
86
- feature_flags={},
103
+ feature_flags: FeatureFlagSet,
87
104
  ) -> Blueprint:
88
105
  core_api_blueprint = Blueprint("api", __name__, url_prefix="/api")
89
106
 
@@ -247,10 +264,23 @@ def core_pages(
247
264
  """
248
265
  try:
249
266
  location_report = request.get_json()
267
+ description = location_report["description"]
268
+
269
+ # Validate description against configured issue options
270
+ issue_options = database.get_issue_options()
271
+ if not issue_options:
272
+ issue_options = get_default_issue_options()
273
+
274
+ if description not in issue_options:
275
+ if "other" not in issue_options:
276
+ return make_response(jsonify({"message": ERROR_INVALID_DESCRIPTION}), 400)
277
+ if len(description) > MAX_DESCRIPTION_LENGTH:
278
+ return make_response(jsonify({"message": ERROR_INVALID_DESCRIPTION}), 400)
279
+
250
280
  report = {
251
281
  "uuid": str(uuid.uuid4()),
252
282
  "location_id": location_report["id"],
253
- "description": location_report["description"],
283
+ "description": description,
254
284
  "status": "pending",
255
285
  "priority": "medium",
256
286
  }
@@ -383,15 +413,15 @@ def core_pages(
383
413
  raw_categories = database.get_categories()
384
414
  categories = make_tuple_translation(raw_categories)
385
415
 
386
- if not feature_flags.get("CATEGORIES_HELP", False):
416
+ if CategoriesHelp not in feature_flags:
387
417
  return jsonify(categories)
388
- else:
389
- category_data = database.get_category_data()
390
- categories_help = category_data.get("categories_help")
391
- proper_categories_help = []
392
- if categories_help is not None:
393
- for option in categories_help:
394
- proper_categories_help.append({option: gettext(f"categories_help_{option}")})
418
+
419
+ category_data = database.get_category_data()
420
+ categories_help = category_data.get("categories_help")
421
+ proper_categories_help = []
422
+ if categories_help is not None:
423
+ for option in categories_help:
424
+ proper_categories_help.append({option: gettext(f"categories_help_{option}")})
395
425
 
396
426
  return jsonify({"categories": categories, "categories_help": proper_categories_help})
397
427
 
@@ -415,7 +445,7 @@ def core_pages(
415
445
  "options": make_tuple_translation(options),
416
446
  }
417
447
 
418
- if feature_flags.get("CATEGORIES_HELP", False):
448
+ if CategoriesHelp in feature_flags:
419
449
  option_help_list = categories_options_help.get(key, [])
420
450
  proper_options_help = []
421
451
  for option in option_help_list:
@@ -428,7 +458,7 @@ def core_pages(
428
458
 
429
459
  response = {"categories": result}
430
460
 
431
- if feature_flags.get("CATEGORIES_HELP", False):
461
+ if CategoriesHelp in feature_flags:
432
462
  categories_help = categories_data.get("categories_help", [])
433
463
  proper_categories_help = []
434
464
  for option in categories_help:
@@ -466,15 +496,15 @@ def core_pages(
466
496
  proper_categories_options_help.append(
467
497
  {option: gettext(f"categories_options_help_{option}")}
468
498
  )
469
- if not feature_flags.get("CATEGORIES_HELP", False):
499
+ if CategoriesHelp not in feature_flags:
470
500
  return jsonify(local_data)
471
- else:
472
- return jsonify(
473
- {
474
- "categories_options": local_data,
475
- "categories_options_help": proper_categories_options_help,
476
- }
477
- )
501
+
502
+ return jsonify(
503
+ {
504
+ "categories_options": local_data,
505
+ "categories_options_help": proper_categories_options_help,
506
+ }
507
+ )
478
508
 
479
509
  # Register Spectree with blueprint after all routes are defined
480
510
  spec.register(core_api_blueprint)
goodmap/db.py CHANGED
@@ -335,6 +335,34 @@ def get_location_obligatory_fields(db):
335
335
  return globals()[f"{db.module_name}_get_location_obligatory_fields"](db)
336
336
 
337
337
 
338
+ # ------------------------------------------------
339
+ # get_issue_options
340
+
341
+
342
+ def json_db_get_issue_options(self):
343
+ return self.data.get("reported_issue_types", [])
344
+
345
+
346
+ def json_file_db_get_issue_options(self):
347
+ with open(self.data_file_path, "r") as file:
348
+ return json.load(file)["map"].get("reported_issue_types", [])
349
+
350
+
351
+ def google_json_db_get_issue_options(self):
352
+ return self.data.get("map", {}).get("reported_issue_types", [])
353
+
354
+
355
+ def mongodb_db_get_issue_options(self):
356
+ config_doc = self.db.config.find_one({"_id": "map_config"})
357
+ if config_doc and "reported_issue_types" in config_doc:
358
+ return config_doc["reported_issue_types"]
359
+ return []
360
+
361
+
362
+ def get_issue_options(db):
363
+ return globals()[f"{db.module_name}_get_issue_options"]
364
+
365
+
338
366
  # ------------------------------------------------
339
367
  # get_data
340
368
 
@@ -1440,6 +1468,7 @@ def delete_report(db, report_id):
1440
1468
 
1441
1469
 
1442
1470
  def extend_db_with_goodmap_queries(db, location_model):
1471
+ db.extend("get_issue_options", get_issue_options(db))
1443
1472
  db.extend("get_data", get_data(db))
1444
1473
  db.extend("get_visible_data", get_visible_data(db))
1445
1474
  db.extend("get_meta_data", get_meta_data(db))
@@ -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
@@ -4,6 +4,7 @@ import logging
4
4
  import os
5
5
 
6
6
  from flask import Blueprint, redirect, render_template, session
7
+ from flask_babel import gettext
7
8
  from flask_wtf.csrf import CSRFProtect, generate_csrf
8
9
  from platzky import platzky
9
10
  from platzky.attachment import create_attachment_class
@@ -18,6 +19,7 @@ from goodmap.db import (
18
19
  extend_db_with_goodmap_queries,
19
20
  get_location_obligatory_fields,
20
21
  )
22
+ from goodmap.feature_flags import EnableAdminPanel, UseLazyLoading
21
23
 
22
24
  logger = logging.getLogger(__name__)
23
25
 
@@ -35,20 +37,6 @@ def create_app(config_path: str) -> platzky.Engine:
35
37
  return create_app_from_config(config)
36
38
 
37
39
 
38
- # TODO Checking if there is a feature flag secition should be part of configs logic not client app
39
- def is_feature_enabled(config: GoodmapConfig, feature: str) -> bool:
40
- """Check if a feature flag is enabled in the configuration.
41
-
42
- Args:
43
- config: Goodmap configuration object
44
- feature: Name of the feature flag to check
45
-
46
- Returns:
47
- bool: True if feature is enabled, False otherwise
48
- """
49
- return config.feature_flags.get(feature, False) if config.feature_flags else False
50
-
51
-
52
40
  def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
53
41
  """Create and configure Goodmap application from config object.
54
42
 
@@ -73,7 +61,7 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
73
61
  if "MAX_CONTENT_LENGTH" not in app.config:
74
62
  app.config["MAX_CONTENT_LENGTH"] = 100 * 1024 # 100KB
75
63
 
76
- if is_feature_enabled(config, "USE_LAZY_LOADING"):
64
+ if app.is_enabled(UseLazyLoading):
77
65
  location_obligatory_fields = get_location_obligatory_fields(app.db)
78
66
  # Extend db with goodmap queries first so we can use the bound method
79
67
  location_model = create_location_model(location_obligatory_fields, {})
@@ -149,12 +137,16 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
149
137
  name: spec for name, spec in properties.items() if name not in ("uuid", "position")
150
138
  }
151
139
 
140
+ issue_options_raw = app.db.get_issue_options() # type: ignore[attr-defined]
141
+ reported_issue_types = [{"value": t, "label": gettext(t)} for t in issue_options_raw]
142
+
152
143
  location_schema = { # TODO remove backward compatibility - deprecation
153
144
  "obligatory_fields": app.extensions["goodmap"][
154
145
  "location_obligatory_fields"
155
146
  ], # Backward compatibility
156
147
  "categories": categories, # Backward compatibility
157
148
  "fields": form_fields,
149
+ "reported_issue_types": reported_issue_types,
158
150
  }
159
151
 
160
152
  return render_template(
@@ -175,7 +167,7 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
175
167
  Returns:
176
168
  Rendered goodmap-admin.html template or redirect to login
177
169
  """
178
- if not is_feature_enabled(config, "ENABLE_ADMIN_PANEL"):
170
+ if not app.is_enabled(EnableAdminPanel):
179
171
  return redirect("/")
180
172
 
181
173
  user = session.get("user", None)
@@ -194,7 +186,7 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
194
186
 
195
187
  app.register_blueprint(goodmap)
196
188
 
197
- if is_feature_enabled(config, "ENABLE_ADMIN_PANEL"):
189
+ if app.is_enabled(EnableAdminPanel):
198
190
  admin_bp = admin_pages(app.db, location_model)
199
191
  app.register_blueprint(admin_bp)
200
192
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: goodmap
3
- Version: 1.4.0
3
+ Version: 1.6.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,<24.0)
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.3.0,<2.0.0)
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,20 @@
1
1
  goodmap/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
2
  goodmap/admin_api.py,sha256=5tvHeqknG8WmhBYmIHlQHTOUA-zaT8FKaAyyLdvX2EE,10290
3
- goodmap/api_models.py,sha256=Bv4OTGuckNneCrxaQ1Y_PMeu7YFLvGUqU2EorvDlUjY,3438
3
+ goodmap/api_models.py,sha256=drE0i6AWHat7siwTpnvpSrWVSC5uW0pTKQzo68xwb34,3468
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=CgAVRJP4Ggd5b33yAVNxAIJtEVy34NqOGKq1JRtNSLo,20006
7
+ goodmap/core_api.py,sha256=ao80Tynclv6IV51hXMPA0W3RQUwJPwxZFXcs-H287KE,21105
8
8
  goodmap/data_models/location.py,sha256=_I27R06ovEL9ctv_SZ3yoLL-RwmyE3VDsVOG4a89q50,6798
9
- goodmap/data_validator.py,sha256=lBmVAPxvSmEOdUGeVYSjUvVVmKfPyq4CWoHfczTtEMM,4090
10
- goodmap/db.py,sha256=TcqYGbK5yk6S735Si1AzjNqcbB1nsd9pFGOy5qN9Vec,46589
9
+ goodmap/db.py,sha256=ZXkonYpiWwtCGsOk9XAXlaNk_db4ZGrJjgPspBdm9gc,47408
11
10
  goodmap/exceptions.py,sha256=jkFAUoc5LHk8iPjxHxbcRp8W6qFCSEA25A8XaSwxwyo,2906
11
+ goodmap/feature_flags.py,sha256=-hiqTX4OlhfY_4M1Kvy-_z1Fx6YTaFi3SVGYa0Pamcw,334
12
12
  goodmap/formatter.py,sha256=4rqcg9A9Y9opAi7eb8kMDdUC03M3uzZgCxx29cvvIag,1403
13
- goodmap/goodmap.py,sha256=OZWxVXtvjftbNPFoJ3myCshv4BOHe0J3x0yLg2Cxm9A,8030
13
+ goodmap/goodmap.py,sha256=Hi8np9oKkPzeRFYVNPiYvhW1chsfJgIoGwUuhZ_3Q-g,7811
14
14
  goodmap/json_security.py,sha256=EHAxNlb16AVwphgf4F7yObtMZpbR9M538dwn_STRcMo,3275
15
15
  goodmap/templates/goodmap-admin.html,sha256=LSiOZ9-n29CnlfVNwdgmXwT7Xe7t5gvGh1xSrFGqOIY,35669
16
16
  goodmap/templates/map.html,sha256=Uk7FFrZwvHZvG0DDaQrGW5ZrIMD21XrJzMub76uIlAg,4348
17
- goodmap-1.4.0.dist-info/LICENSE.md,sha256=nkCQOR7uheLRvHRfXmwx9LhBnMcPeBU9d4ebLojDiQU,1067
18
- goodmap-1.4.0.dist-info/METADATA,sha256=M8Y6Fq8-gh2--o1VnhPd35NznVOC0R9DNGZo7LAEMbU,5798
19
- goodmap-1.4.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
20
- goodmap-1.4.0.dist-info/RECORD,,
17
+ goodmap-1.6.0.dist-info/LICENSE.md,sha256=nkCQOR7uheLRvHRfXmwx9LhBnMcPeBU9d4ebLojDiQU,1067
18
+ goodmap-1.6.0.dist-info/METADATA,sha256=ljeI70gs0U8RV8dm-VDX8Vt4MHy5_KxfB7tlik3l-0E,5798
19
+ goodmap-1.6.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
20
+ goodmap-1.6.0.dist-info/RECORD,,
goodmap/data_validator.py DELETED
@@ -1,119 +0,0 @@
1
- import json
2
- from enum import Enum
3
- from sys import argv, stderr
4
-
5
-
6
- class ViolationType(Enum):
7
- INVALID_JSON_FORMAT = 0
8
- MISSING_OBLIGATORY_FIELD = 1
9
- INVALID_VALUE_IN_CATEGORY = 2
10
- NULL_VALUE = 3
11
-
12
- def get_error_message(self):
13
- error_message_dict = {
14
- 0: "invalid json format",
15
- 1: "missing obligatory field",
16
- 2: "invalid value in category",
17
- 3: "attribute has null value",
18
- }
19
- return error_message_dict[self.value]
20
-
21
-
22
- class DataViolation:
23
- def __init__(self, violation_type: ViolationType):
24
- self.violation_type = violation_type
25
-
26
-
27
- class FormatViolation(DataViolation):
28
- def __init__(self, decoding_error):
29
- super().__init__(ViolationType.INVALID_JSON_FORMAT)
30
- self.decoding_error = decoding_error
31
-
32
-
33
- class FieldViolation(DataViolation):
34
- def __init__(self, violation_type: ViolationType, datapoint, violating_field):
35
- super().__init__(violation_type)
36
- self.datapoint = datapoint
37
- self.violating_field = violating_field
38
-
39
-
40
- def get_missing_obligatory_fields_violations(p, obligatory_fields):
41
- violations = []
42
- for field in obligatory_fields:
43
- if field not in p.keys():
44
- violations.append(FieldViolation(ViolationType.MISSING_OBLIGATORY_FIELD, p, field))
45
- return violations
46
-
47
-
48
- def get_invalid_value_in_category_violations(p, categories):
49
- violations = []
50
- for category in categories & p.keys():
51
- category_value_in_point = p[category]
52
- valid_values_set = categories[category]
53
- if isinstance(category_value_in_point, list):
54
- for attribute_value in category_value_in_point:
55
- if attribute_value not in valid_values_set:
56
- violations.append(
57
- FieldViolation(ViolationType.INVALID_VALUE_IN_CATEGORY, p, category)
58
- )
59
- else:
60
- if category_value_in_point not in valid_values_set:
61
- violations.append(
62
- FieldViolation(ViolationType.INVALID_VALUE_IN_CATEGORY, p, category)
63
- )
64
- return violations
65
-
66
-
67
- def get_null_values_violations(p):
68
- violations = []
69
- for attribute, value in p.items():
70
- if value is None:
71
- violations.append(FieldViolation(ViolationType.NULL_VALUE, p, attribute))
72
- return violations
73
-
74
-
75
- def report_data_violations_from_json(json_database):
76
- map_data = json_database["map"]
77
- datapoints = map_data["data"]
78
- categories = map_data["categories"]
79
- obligatory_fields = map_data["location_obligatory_fields"]
80
-
81
- data_violations = []
82
-
83
- for p in datapoints:
84
- data_violations += get_missing_obligatory_fields_violations(p, obligatory_fields)
85
- data_violations += get_invalid_value_in_category_violations(p, categories)
86
- data_violations += get_null_values_violations(p)
87
-
88
- return data_violations
89
-
90
-
91
- def report_data_violations_from_json_file(path_to_json_file):
92
- with open(path_to_json_file) as json_file:
93
- try:
94
- return report_data_violations_from_json(json.load(json_file))
95
- except json.JSONDecodeError as e:
96
- return [FormatViolation(e)]
97
-
98
-
99
- def print_reported_violations(data_violations): # pragma: no cover
100
- for violation in data_violations:
101
- violation_type = violation.violation_type
102
- if violation_type == ViolationType.INVALID_JSON_FORMAT:
103
- print("DATA ERROR: invalid json format", file=stderr)
104
- print(violation.decoding_error, file=stderr)
105
- else:
106
- violating_field = violation.violating_field
107
- violation_type_error = violation_type.get_error_message()
108
- print(
109
- f"DATA ERROR: {violation_type_error} {violating_field} in datapoint:", file=stderr
110
- )
111
- print(violation.datapoint, file=stderr)
112
-
113
-
114
- if __name__ == "__main__": # pragma: no cover
115
- data_violations = report_data_violations_from_json_file(argv[1])
116
- if data_violations == []:
117
- print("All data is valid", file=stderr)
118
- else:
119
- print_reported_violations(data_violations)