goodmap 1.5.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 +3 -1
- goodmap/core_api.py +29 -1
- goodmap/db.py +29 -0
- goodmap/goodmap.py +5 -0
- {goodmap-1.5.0.dist-info → goodmap-1.6.0.dist-info}/METADATA +1 -1
- {goodmap-1.5.0.dist-info → goodmap-1.6.0.dist-info}/RECORD +8 -9
- goodmap/data_validator.py +0 -119
- {goodmap-1.5.0.dist-info → goodmap-1.6.0.dist-info}/LICENSE.md +0 -0
- {goodmap-1.5.0.dist-info → goodmap-1.6.0.dist-info}/WHEEL +0 -0
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(
|
|
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
|
@@ -40,14 +40,29 @@ MAX_ZOOM = 16
|
|
|
40
40
|
CLUSTER_RADIUS = 200
|
|
41
41
|
CLUSTER_EXTENT = 512
|
|
42
42
|
|
|
43
|
+
# Report description validation constants
|
|
44
|
+
MAX_DESCRIPTION_LENGTH = 500
|
|
45
|
+
|
|
43
46
|
# Error message constants
|
|
44
47
|
ERROR_INVALID_REQUEST_DATA = "Invalid request data"
|
|
45
48
|
ERROR_INVALID_LOCATION_DATA = "Invalid location data"
|
|
46
49
|
ERROR_LOCATION_NOT_FOUND = "Location not found"
|
|
50
|
+
ERROR_INVALID_DESCRIPTION = "Invalid report description"
|
|
47
51
|
|
|
48
52
|
logger = logging.getLogger(__name__)
|
|
49
53
|
|
|
50
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
|
+
|
|
51
66
|
def make_tuple_translation(keys_to_translate):
|
|
52
67
|
return [(x, gettext(x)) for x in keys_to_translate]
|
|
53
68
|
|
|
@@ -249,10 +264,23 @@ def core_pages(
|
|
|
249
264
|
"""
|
|
250
265
|
try:
|
|
251
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
|
+
|
|
252
280
|
report = {
|
|
253
281
|
"uuid": str(uuid.uuid4()),
|
|
254
282
|
"location_id": location_report["id"],
|
|
255
|
-
"description":
|
|
283
|
+
"description": description,
|
|
256
284
|
"status": "pending",
|
|
257
285
|
"priority": "medium",
|
|
258
286
|
}
|
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))
|
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
|
|
@@ -136,12 +137,16 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
|
|
|
136
137
|
name: spec for name, spec in properties.items() if name not in ("uuid", "position")
|
|
137
138
|
}
|
|
138
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
|
+
|
|
139
143
|
location_schema = { # TODO remove backward compatibility - deprecation
|
|
140
144
|
"obligatory_fields": app.extensions["goodmap"][
|
|
141
145
|
"location_obligatory_fields"
|
|
142
146
|
], # Backward compatibility
|
|
143
147
|
"categories": categories, # Backward compatibility
|
|
144
148
|
"fields": form_fields,
|
|
149
|
+
"reported_issue_types": reported_issue_types,
|
|
145
150
|
}
|
|
146
151
|
|
|
147
152
|
return render_template(
|
|
@@ -1,21 +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=
|
|
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=
|
|
7
|
+
goodmap/core_api.py,sha256=ao80Tynclv6IV51hXMPA0W3RQUwJPwxZFXcs-H287KE,21105
|
|
8
8
|
goodmap/data_models/location.py,sha256=_I27R06ovEL9ctv_SZ3yoLL-RwmyE3VDsVOG4a89q50,6798
|
|
9
|
-
goodmap/
|
|
10
|
-
goodmap/db.py,sha256=TcqYGbK5yk6S735Si1AzjNqcbB1nsd9pFGOy5qN9Vec,46589
|
|
9
|
+
goodmap/db.py,sha256=ZXkonYpiWwtCGsOk9XAXlaNk_db4ZGrJjgPspBdm9gc,47408
|
|
11
10
|
goodmap/exceptions.py,sha256=jkFAUoc5LHk8iPjxHxbcRp8W6qFCSEA25A8XaSwxwyo,2906
|
|
12
11
|
goodmap/feature_flags.py,sha256=-hiqTX4OlhfY_4M1Kvy-_z1Fx6YTaFi3SVGYa0Pamcw,334
|
|
13
12
|
goodmap/formatter.py,sha256=4rqcg9A9Y9opAi7eb8kMDdUC03M3uzZgCxx29cvvIag,1403
|
|
14
|
-
goodmap/goodmap.py,sha256=
|
|
13
|
+
goodmap/goodmap.py,sha256=Hi8np9oKkPzeRFYVNPiYvhW1chsfJgIoGwUuhZ_3Q-g,7811
|
|
15
14
|
goodmap/json_security.py,sha256=EHAxNlb16AVwphgf4F7yObtMZpbR9M538dwn_STRcMo,3275
|
|
16
15
|
goodmap/templates/goodmap-admin.html,sha256=LSiOZ9-n29CnlfVNwdgmXwT7Xe7t5gvGh1xSrFGqOIY,35669
|
|
17
16
|
goodmap/templates/map.html,sha256=Uk7FFrZwvHZvG0DDaQrGW5ZrIMD21XrJzMub76uIlAg,4348
|
|
18
|
-
goodmap-1.
|
|
19
|
-
goodmap-1.
|
|
20
|
-
goodmap-1.
|
|
21
|
-
goodmap-1.
|
|
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)
|
|
File without changes
|
|
File without changes
|