goodmap 0.4.4__py3-none-any.whl → 0.5.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.
- goodmap/core_api.py +212 -25
- goodmap/data_models/location.py +2 -1
- goodmap/data_validator.py +1 -1
- goodmap/db.py +448 -0
- goodmap/goodmap.py +27 -1
- goodmap/templates/goodmap-admin.html +743 -0
- {goodmap-0.4.4.dist-info → goodmap-0.5.1.dist-info}/METADATA +11 -3
- goodmap-0.5.1.dist-info/RECORD +14 -0
- {goodmap-0.4.4.dist-info → goodmap-0.5.1.dist-info}/WHEEL +1 -1
- goodmap/templates/admin.html +0 -0
- goodmap-0.4.4.dist-info/RECORD +0 -14
- {goodmap-0.4.4.dist-info → goodmap-0.5.1.dist-info}/LICENSE.md +0 -0
goodmap/core_api.py
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import importlib.metadata
|
|
2
2
|
import uuid
|
|
3
3
|
|
|
4
|
-
import deprecation
|
|
5
4
|
from flask import Blueprint, jsonify, make_response, request
|
|
6
5
|
from flask_babel import gettext
|
|
7
6
|
from flask_restx import Api, Resource, fields
|
|
8
7
|
from platzky.config import LanguagesMapping
|
|
9
8
|
|
|
10
|
-
from goodmap.core import get_queried_data
|
|
11
9
|
from goodmap.formatter import prepare_pin
|
|
12
10
|
|
|
13
11
|
|
|
@@ -15,6 +13,74 @@ def make_tuple_translation(keys_to_translate):
|
|
|
15
13
|
return [(x, gettext(x)) for x in keys_to_translate]
|
|
16
14
|
|
|
17
15
|
|
|
16
|
+
def paginate_results(items, raw_params, sort_by_default=None):
|
|
17
|
+
"""
|
|
18
|
+
Apply pagination and sorting to a list of items.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
items: The list of items to paginate
|
|
22
|
+
raw_params: The query parameters dictionary
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Tuple of (paginated_items, pagination_metadata)
|
|
26
|
+
"""
|
|
27
|
+
# Extract pagination parameters
|
|
28
|
+
try:
|
|
29
|
+
page = int(raw_params.pop("page", ["1"])[0])
|
|
30
|
+
except ValueError:
|
|
31
|
+
page = 1
|
|
32
|
+
|
|
33
|
+
per_page_raw = raw_params.pop("per_page", [None])[0]
|
|
34
|
+
if per_page_raw is None:
|
|
35
|
+
per_page = 20
|
|
36
|
+
elif per_page_raw == "all":
|
|
37
|
+
per_page = None
|
|
38
|
+
else:
|
|
39
|
+
try:
|
|
40
|
+
per_page = max(1, int(per_page_raw))
|
|
41
|
+
except ValueError:
|
|
42
|
+
per_page = 20
|
|
43
|
+
|
|
44
|
+
sort_by = raw_params.pop("sort_by", [None])[0] or sort_by_default
|
|
45
|
+
sort_order = raw_params.pop("sort_order", ["asc"])[0].lower()
|
|
46
|
+
|
|
47
|
+
def get_sort_key(item):
|
|
48
|
+
if not sort_by:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
value = None
|
|
52
|
+
if isinstance(item, dict):
|
|
53
|
+
value = item.get(sort_by)
|
|
54
|
+
else:
|
|
55
|
+
value = getattr(item, sort_by, None)
|
|
56
|
+
|
|
57
|
+
return (value is not None, value)
|
|
58
|
+
|
|
59
|
+
if sort_by:
|
|
60
|
+
reverse = sort_order == "desc"
|
|
61
|
+
items.sort(key=get_sort_key, reverse=reverse)
|
|
62
|
+
|
|
63
|
+
# Apply pagination
|
|
64
|
+
total = len(items)
|
|
65
|
+
if per_page:
|
|
66
|
+
start = (page - 1) * per_page
|
|
67
|
+
end = start + per_page
|
|
68
|
+
page_items = items[start:end]
|
|
69
|
+
total_pages = (total + per_page - 1) // per_page
|
|
70
|
+
else:
|
|
71
|
+
page_items = items
|
|
72
|
+
total_pages = 1
|
|
73
|
+
page = 1
|
|
74
|
+
per_page = total
|
|
75
|
+
|
|
76
|
+
return page_items, {
|
|
77
|
+
"total": total,
|
|
78
|
+
"page": page,
|
|
79
|
+
"per_page": per_page,
|
|
80
|
+
"total_pages": total_pages,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
18
84
|
def core_pages(
|
|
19
85
|
database, languages: LanguagesMapping, notifier_function, csrf_generator, location_model
|
|
20
86
|
) -> Blueprint:
|
|
@@ -48,6 +114,7 @@ def core_pages(
|
|
|
48
114
|
suggested_location = request.get_json()
|
|
49
115
|
suggested_location.update({"uuid": str(uuid.uuid4())})
|
|
50
116
|
location = location_model.model_validate(suggested_location)
|
|
117
|
+
database.add_suggestion(location.model_dump())
|
|
51
118
|
message = (
|
|
52
119
|
f"A new location has been suggested under uuid: '{location.uuid}' "
|
|
53
120
|
f"at position: {location.position}"
|
|
@@ -66,39 +133,27 @@ def core_pages(
|
|
|
66
133
|
"""Report location"""
|
|
67
134
|
try:
|
|
68
135
|
location_report = request.get_json()
|
|
136
|
+
report = {
|
|
137
|
+
"uuid": str(uuid.uuid4()),
|
|
138
|
+
"location_id": location_report["id"],
|
|
139
|
+
"description": location_report["description"],
|
|
140
|
+
"status": "pending",
|
|
141
|
+
"priority": "medium",
|
|
142
|
+
}
|
|
143
|
+
database.add_report(report)
|
|
69
144
|
message = (
|
|
70
145
|
f"A location has been reported: '{location_report['id']}' "
|
|
71
146
|
f"with problem: {location_report['description']}"
|
|
72
147
|
)
|
|
73
148
|
notifier_function(message)
|
|
149
|
+
except KeyError as e:
|
|
150
|
+
error_message = gettext("Error reporting location")
|
|
151
|
+
return make_response(jsonify({"message": f"{error_message} : {e}"}), 400)
|
|
74
152
|
except Exception as e:
|
|
75
153
|
error_message = gettext("Error sending notification")
|
|
76
154
|
return make_response(jsonify({"message": f"{error_message} : {e}"}), 400)
|
|
77
155
|
return make_response(jsonify({"message": gettext("Location reported")}), 200)
|
|
78
156
|
|
|
79
|
-
@core_api.route("/data")
|
|
80
|
-
class Data(Resource):
|
|
81
|
-
@deprecation.deprecated(
|
|
82
|
-
deprecated_in="0.4.1",
|
|
83
|
-
removed_in="0.5.0",
|
|
84
|
-
current_version=importlib.metadata.version("goodmap"),
|
|
85
|
-
details="Use /locations or /location/<point_id> instead",
|
|
86
|
-
)
|
|
87
|
-
def get(self):
|
|
88
|
-
"""
|
|
89
|
-
Shows all data filtered by query parameters
|
|
90
|
-
e.g. /api/data?category=category1&category=category2
|
|
91
|
-
"""
|
|
92
|
-
all_data = database.get_data()
|
|
93
|
-
query_params = request.args.to_dict(flat=False)
|
|
94
|
-
data = all_data["data"]
|
|
95
|
-
categories = all_data["categories"]
|
|
96
|
-
visible_data = all_data["visible_data"]
|
|
97
|
-
meta_data = all_data["meta_data"]
|
|
98
|
-
queried_data = get_queried_data(data, categories, query_params)
|
|
99
|
-
formatted_data = [prepare_pin(x, visible_data, meta_data) for x in queried_data]
|
|
100
|
-
return jsonify(formatted_data)
|
|
101
|
-
|
|
102
157
|
@core_api.route("/locations")
|
|
103
158
|
class GetLocations(Resource):
|
|
104
159
|
def get(self):
|
|
@@ -162,4 +217,136 @@ def core_pages(
|
|
|
162
217
|
csrf_token = csrf_generator()
|
|
163
218
|
return {"csrf_token": csrf_token}
|
|
164
219
|
|
|
220
|
+
@core_api.route("/admin/locations")
|
|
221
|
+
class AdminManageLocations(Resource):
|
|
222
|
+
def get(self):
|
|
223
|
+
"""
|
|
224
|
+
Shows full list of locations, with optional server-side pagination, sorting,
|
|
225
|
+
and filtering.
|
|
226
|
+
"""
|
|
227
|
+
# Raw query params from request
|
|
228
|
+
raw_params = request.args.to_dict(flat=False)
|
|
229
|
+
all_locations = database.get_locations(raw_params)
|
|
230
|
+
page_items, pagination = paginate_results(
|
|
231
|
+
all_locations, raw_params, sort_by_default="name"
|
|
232
|
+
)
|
|
233
|
+
items = [x.model_dump() for x in page_items]
|
|
234
|
+
return jsonify({"items": items, **pagination})
|
|
235
|
+
|
|
236
|
+
def post(self):
|
|
237
|
+
"""
|
|
238
|
+
Creates a new location
|
|
239
|
+
"""
|
|
240
|
+
location_data = request.get_json()
|
|
241
|
+
try:
|
|
242
|
+
location_data.update({"uuid": str(uuid.uuid4())})
|
|
243
|
+
location = location_model.model_validate(location_data)
|
|
244
|
+
database.add_location(location.model_dump())
|
|
245
|
+
except ValueError as e:
|
|
246
|
+
return make_response(jsonify({"message": f"Invalid location data: {e}"}), 400)
|
|
247
|
+
except Exception as e:
|
|
248
|
+
return make_response(jsonify({"message": f"Error creating location: {e}"}), 400)
|
|
249
|
+
return jsonify(location.model_dump())
|
|
250
|
+
|
|
251
|
+
@core_api.route("/admin/locations/<location_id>")
|
|
252
|
+
class AdminManageLocation(Resource):
|
|
253
|
+
def put(self, location_id):
|
|
254
|
+
"""
|
|
255
|
+
Updates a single location
|
|
256
|
+
"""
|
|
257
|
+
location_data = request.get_json()
|
|
258
|
+
try:
|
|
259
|
+
location_data.update({"uuid": location_id})
|
|
260
|
+
location = location_model.model_validate(location_data)
|
|
261
|
+
database.update_location(location_id, location.model_dump())
|
|
262
|
+
except ValueError as e:
|
|
263
|
+
return make_response(jsonify({"message": f"Invalid location data: {e}"}), 400)
|
|
264
|
+
except Exception as e:
|
|
265
|
+
return make_response(jsonify({"message": f"Error updating location: {e}"}), 400)
|
|
266
|
+
return jsonify(location.model_dump())
|
|
267
|
+
|
|
268
|
+
def delete(self, location_id):
|
|
269
|
+
"""
|
|
270
|
+
Deletes a single location
|
|
271
|
+
"""
|
|
272
|
+
try:
|
|
273
|
+
database.delete_location(location_id)
|
|
274
|
+
except ValueError as e:
|
|
275
|
+
return make_response(jsonify({"message": f"Location not found: {e}"}), 404)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
return make_response(jsonify({"message": f"Error deleting location: {e}"}), 400)
|
|
278
|
+
return "", 204
|
|
279
|
+
|
|
280
|
+
@core_api.route("/admin/suggestions")
|
|
281
|
+
class AdminManageSuggestions(Resource):
|
|
282
|
+
def get(self):
|
|
283
|
+
"""
|
|
284
|
+
List location suggestions, with optional server-side pagination, sorting,
|
|
285
|
+
and filtering by status.
|
|
286
|
+
"""
|
|
287
|
+
raw_params = request.args.to_dict(flat=False)
|
|
288
|
+
suggestions = database.get_suggestions(raw_params)
|
|
289
|
+
page_items, pagination = paginate_results(suggestions, raw_params)
|
|
290
|
+
return jsonify({"items": page_items, **pagination})
|
|
291
|
+
|
|
292
|
+
@core_api.route("/admin/suggestions/<suggestion_id>")
|
|
293
|
+
class AdminManageSuggestion(Resource):
|
|
294
|
+
def put(self, suggestion_id):
|
|
295
|
+
"""
|
|
296
|
+
Accept or reject a location suggestion
|
|
297
|
+
"""
|
|
298
|
+
try:
|
|
299
|
+
data = request.get_json()
|
|
300
|
+
status = data.get("status")
|
|
301
|
+
if status not in ("accepted", "rejected"):
|
|
302
|
+
return make_response(jsonify({"message": f"Invalid status: {status}"}), 400)
|
|
303
|
+
suggestion = database.get_suggestion(suggestion_id)
|
|
304
|
+
if not suggestion:
|
|
305
|
+
return make_response(jsonify({"message": "Suggestion not found"}), 404)
|
|
306
|
+
if suggestion.get("status") != "pending":
|
|
307
|
+
return make_response(jsonify({"message": "Suggestion already processed"}), 400)
|
|
308
|
+
if status == "accepted":
|
|
309
|
+
suggestion_data = {k: v for k, v in suggestion.items() if k != "status"}
|
|
310
|
+
database.add_location(suggestion_data)
|
|
311
|
+
database.update_suggestion(suggestion_id, status)
|
|
312
|
+
except ValueError as e:
|
|
313
|
+
return make_response(jsonify({"message": f"{e}"}), 400)
|
|
314
|
+
return jsonify(database.get_suggestion(suggestion_id))
|
|
315
|
+
|
|
316
|
+
@core_api.route("/admin/reports")
|
|
317
|
+
class AdminManageReports(Resource):
|
|
318
|
+
def get(self):
|
|
319
|
+
"""
|
|
320
|
+
List location reports, with optional server-side pagination, sorting,
|
|
321
|
+
and filtering by status/priority.
|
|
322
|
+
"""
|
|
323
|
+
raw_params = request.args.to_dict(flat=False)
|
|
324
|
+
reports = database.get_reports(raw_params)
|
|
325
|
+
page_items, pagination = paginate_results(reports, raw_params)
|
|
326
|
+
return jsonify({"items": page_items, **pagination})
|
|
327
|
+
|
|
328
|
+
@core_api.route("/admin/reports/<report_id>")
|
|
329
|
+
class AdminManageReport(Resource):
|
|
330
|
+
def put(self, report_id):
|
|
331
|
+
"""
|
|
332
|
+
Update a report's status and/or priority
|
|
333
|
+
"""
|
|
334
|
+
try:
|
|
335
|
+
data = request.get_json()
|
|
336
|
+
status = data.get("status")
|
|
337
|
+
priority = data.get("priority")
|
|
338
|
+
valid_status = ("resolved", "rejected")
|
|
339
|
+
valid_priority = ("critical", "high", "medium", "low")
|
|
340
|
+
if status and status not in valid_status:
|
|
341
|
+
return make_response(jsonify({"message": f"Invalid status: {status}"}), 400)
|
|
342
|
+
if priority and priority not in valid_priority:
|
|
343
|
+
return make_response(jsonify({"message": f"Invalid priority: {priority}"}), 400)
|
|
344
|
+
report = database.get_report(report_id)
|
|
345
|
+
if not report:
|
|
346
|
+
return make_response(jsonify({"message": "Report not found"}), 404)
|
|
347
|
+
database.update_report(report_id, status=status, priority=priority)
|
|
348
|
+
except ValueError as e:
|
|
349
|
+
return make_response(jsonify({"message": f"{e}"}), 400)
|
|
350
|
+
return jsonify(database.get_report(report_id))
|
|
351
|
+
|
|
165
352
|
return core_api_blueprint
|
goodmap/data_models/location.py
CHANGED
|
@@ -6,6 +6,7 @@ from pydantic import BaseModel, Field, create_model, field_validator
|
|
|
6
6
|
class LocationBase(BaseModel, extra="allow"):
|
|
7
7
|
position: tuple[float, float]
|
|
8
8
|
uuid: str
|
|
9
|
+
remark: bool = False
|
|
9
10
|
|
|
10
11
|
@field_validator("position")
|
|
11
12
|
@classmethod
|
|
@@ -17,7 +18,7 @@ class LocationBase(BaseModel, extra="allow"):
|
|
|
17
18
|
return v
|
|
18
19
|
|
|
19
20
|
def basic_info(self):
|
|
20
|
-
return {"uuid": self.uuid, "position": self.position}
|
|
21
|
+
return {"uuid": self.uuid, "position": self.position, "remark": bool(self.remark)}
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def create_location_model(obligatory_fields: list[tuple[str, Type[Any]]]) -> Type[BaseModel]:
|
goodmap/data_validator.py
CHANGED
|
@@ -50,7 +50,7 @@ def get_invalid_value_in_category_violations(p, categories):
|
|
|
50
50
|
for category in categories & p.keys():
|
|
51
51
|
category_value_in_point = p[category]
|
|
52
52
|
valid_values_set = categories[category]
|
|
53
|
-
if
|
|
53
|
+
if isinstance(category_value_in_point, list):
|
|
54
54
|
for attribute_value in category_value_in_point:
|
|
55
55
|
if attribute_value not in valid_values_set:
|
|
56
56
|
violations.append(
|