goodmap 1.2.0__py3-none-any.whl → 1.3.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/admin_api.py +251 -0
- goodmap/core_api.py +43 -191
- goodmap/goodmap.py +22 -11
- {goodmap-1.2.0.dist-info → goodmap-1.3.1.dist-info}/METADATA +1 -1
- {goodmap-1.2.0.dist-info → goodmap-1.3.1.dist-info}/RECORD +7 -6
- {goodmap-1.2.0.dist-info → goodmap-1.3.1.dist-info}/LICENSE.md +0 -0
- {goodmap-1.2.0.dist-info → goodmap-1.3.1.dist-info}/WHEEL +0 -0
goodmap/admin_api.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Any, Type
|
|
4
|
+
|
|
5
|
+
from flask import Blueprint, jsonify, make_response, request
|
|
6
|
+
from spectree import Response, SpecTree
|
|
7
|
+
from werkzeug.exceptions import BadRequest
|
|
8
|
+
|
|
9
|
+
from goodmap.api_models import (
|
|
10
|
+
ErrorResponse,
|
|
11
|
+
ReportUpdateRequest,
|
|
12
|
+
SuggestionStatusRequest,
|
|
13
|
+
)
|
|
14
|
+
from goodmap.exceptions import (
|
|
15
|
+
LocationAlreadyExistsError,
|
|
16
|
+
LocationNotFoundError,
|
|
17
|
+
LocationValidationError,
|
|
18
|
+
ReportNotFoundError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Error message constants
|
|
22
|
+
ERROR_INVALID_REQUEST_DATA = "Invalid request data"
|
|
23
|
+
ERROR_INVALID_LOCATION_DATA = "Invalid location data"
|
|
24
|
+
ERROR_INTERNAL_ERROR = "An internal error occurred"
|
|
25
|
+
ERROR_LOCATION_NOT_FOUND = "Location not found"
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _clean_model_name(model: Type[Any]) -> str:
|
|
31
|
+
return model.__name__
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _handle_location_validation_error(e: LocationValidationError):
|
|
35
|
+
"""Handle LocationValidationError and return appropriate response."""
|
|
36
|
+
logger.warning(
|
|
37
|
+
"Location validation failed",
|
|
38
|
+
extra={"uuid": e.uuid, "errors": e.validation_errors},
|
|
39
|
+
)
|
|
40
|
+
return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _get_locations_handler(database):
|
|
44
|
+
"""Handle GET /locations request."""
|
|
45
|
+
query_params = request.args.to_dict(flat=False)
|
|
46
|
+
if "sort_by" not in query_params:
|
|
47
|
+
query_params["sort_by"] = ["name"]
|
|
48
|
+
result = database.get_locations_paginated(query_params)
|
|
49
|
+
return jsonify(result)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _create_location_handler(database, location_model):
|
|
53
|
+
"""Handle POST /locations request."""
|
|
54
|
+
location_data = request.get_json()
|
|
55
|
+
if location_data is None:
|
|
56
|
+
logger.warning("Empty or invalid JSON in admin create location endpoint")
|
|
57
|
+
return make_response(jsonify({"message": ERROR_INVALID_REQUEST_DATA}), 400)
|
|
58
|
+
# TODO: Catch pydantic.ValidationError separately to return 400 instead of 500
|
|
59
|
+
try:
|
|
60
|
+
location_data.update({"uuid": str(uuid.uuid4())})
|
|
61
|
+
location = location_model.model_validate(location_data)
|
|
62
|
+
database.add_location(location.model_dump())
|
|
63
|
+
except LocationValidationError as e:
|
|
64
|
+
return _handle_location_validation_error(e)
|
|
65
|
+
except Exception:
|
|
66
|
+
logger.error("Error creating location", exc_info=True)
|
|
67
|
+
return make_response(jsonify({"message": ERROR_INTERNAL_ERROR}), 500)
|
|
68
|
+
return jsonify(location.model_dump())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _update_location_handler(database, location_model, location_id):
|
|
72
|
+
"""Handle PUT /locations/<location_id> request."""
|
|
73
|
+
location_data = request.get_json()
|
|
74
|
+
if location_data is None:
|
|
75
|
+
logger.warning("Empty or invalid JSON in admin update location endpoint")
|
|
76
|
+
return make_response(jsonify({"message": ERROR_INVALID_REQUEST_DATA}), 400)
|
|
77
|
+
# TODO: Catch pydantic.ValidationError separately to return 400 instead of 500
|
|
78
|
+
try:
|
|
79
|
+
location_data.update({"uuid": location_id})
|
|
80
|
+
location = location_model.model_validate(location_data)
|
|
81
|
+
database.update_location(location_id, location.model_dump())
|
|
82
|
+
except LocationValidationError as e:
|
|
83
|
+
return _handle_location_validation_error(e)
|
|
84
|
+
except LocationNotFoundError as e:
|
|
85
|
+
logger.info("Location not found for update", extra={"uuid": e.uuid})
|
|
86
|
+
return make_response(jsonify({"message": ERROR_LOCATION_NOT_FOUND}), 404)
|
|
87
|
+
except Exception:
|
|
88
|
+
logger.error("Error updating location", exc_info=True)
|
|
89
|
+
return make_response(jsonify({"message": ERROR_INTERNAL_ERROR}), 500)
|
|
90
|
+
return jsonify(location.model_dump())
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _delete_location_handler(database, location_id):
|
|
94
|
+
"""Handle DELETE /locations/<location_id> request."""
|
|
95
|
+
try:
|
|
96
|
+
database.delete_location(location_id)
|
|
97
|
+
except LocationNotFoundError as e:
|
|
98
|
+
logger.info("Location not found for deletion", extra={"uuid": e.uuid})
|
|
99
|
+
return make_response(jsonify({"message": ERROR_LOCATION_NOT_FOUND}), 404)
|
|
100
|
+
except Exception:
|
|
101
|
+
logger.error("Error deleting location", exc_info=True)
|
|
102
|
+
return make_response(jsonify({"message": ERROR_INTERNAL_ERROR}), 500)
|
|
103
|
+
return "", 204
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _get_suggestions_handler(database):
|
|
107
|
+
"""Handle GET /suggestions request."""
|
|
108
|
+
query_params = request.args.to_dict(flat=False)
|
|
109
|
+
result = database.get_suggestions_paginated(query_params)
|
|
110
|
+
return jsonify(result)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _update_suggestion_handler(database, suggestion_id):
|
|
114
|
+
"""Handle PUT /suggestions/<suggestion_id> request."""
|
|
115
|
+
try:
|
|
116
|
+
data = request.get_json()
|
|
117
|
+
status = data["status"] # Validated by Spectree
|
|
118
|
+
suggestion = database.get_suggestion(suggestion_id)
|
|
119
|
+
if not suggestion:
|
|
120
|
+
return make_response(jsonify({"message": "Suggestion not found"}), 404)
|
|
121
|
+
if suggestion.get("status") != "pending":
|
|
122
|
+
return make_response(jsonify({"message": "Suggestion already processed"}), 409)
|
|
123
|
+
if status == "accepted":
|
|
124
|
+
suggestion_data = {k: v for k, v in suggestion.items() if k != "status"}
|
|
125
|
+
database.add_location(suggestion_data)
|
|
126
|
+
database.update_suggestion(suggestion_id, status)
|
|
127
|
+
except LocationValidationError as e:
|
|
128
|
+
logger.warning(
|
|
129
|
+
"Location validation failed in suggestion",
|
|
130
|
+
extra={"uuid": e.uuid, "errors": e.validation_errors},
|
|
131
|
+
)
|
|
132
|
+
return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
|
|
133
|
+
except LocationAlreadyExistsError as e:
|
|
134
|
+
logger.warning(
|
|
135
|
+
"Attempted to create duplicate location from suggestion", extra={"uuid": e.uuid}
|
|
136
|
+
)
|
|
137
|
+
return make_response(jsonify({"message": "Location already exists"}), 409)
|
|
138
|
+
except Exception:
|
|
139
|
+
logger.error("Error processing suggestion", exc_info=True)
|
|
140
|
+
return make_response(jsonify({"message": ERROR_INTERNAL_ERROR}), 500)
|
|
141
|
+
return jsonify(database.get_suggestion(suggestion_id))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _get_reports_handler(database):
|
|
145
|
+
"""Handle GET /reports request."""
|
|
146
|
+
query_params = request.args.to_dict(flat=False)
|
|
147
|
+
result = database.get_reports_paginated(query_params)
|
|
148
|
+
return jsonify(result)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _update_report_handler(database, report_id):
|
|
152
|
+
"""Handle PUT /reports/<report_id> request."""
|
|
153
|
+
try:
|
|
154
|
+
data = request.get_json()
|
|
155
|
+
status = data.get("status")
|
|
156
|
+
priority = data.get("priority")
|
|
157
|
+
report = database.get_report(report_id)
|
|
158
|
+
if not report:
|
|
159
|
+
return make_response(jsonify({"message": "Report not found"}), 404)
|
|
160
|
+
database.update_report(report_id, status=status, priority=priority)
|
|
161
|
+
except BadRequest:
|
|
162
|
+
logger.warning("Invalid JSON in report update endpoint")
|
|
163
|
+
return make_response(jsonify({"message": ERROR_INVALID_REQUEST_DATA}), 400)
|
|
164
|
+
except ReportNotFoundError as e:
|
|
165
|
+
logger.info("Report not found for update", extra={"uuid": e.uuid})
|
|
166
|
+
return make_response(jsonify({"message": "Report not found"}), 404)
|
|
167
|
+
except Exception:
|
|
168
|
+
logger.error("Error updating report", exc_info=True)
|
|
169
|
+
return make_response(jsonify({"message": ERROR_INTERNAL_ERROR}), 500)
|
|
170
|
+
return jsonify(database.get_report(report_id))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def admin_pages(database, location_model) -> Blueprint:
|
|
174
|
+
"""Create and return the admin API blueprint.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
database: Database instance for data operations
|
|
178
|
+
location_model: Pydantic model for location validation
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Blueprint: Flask blueprint with all admin endpoints
|
|
182
|
+
"""
|
|
183
|
+
admin_api_blueprint = Blueprint("admin_api", __name__, url_prefix="/api/admin")
|
|
184
|
+
|
|
185
|
+
spec = SpecTree(
|
|
186
|
+
"flask",
|
|
187
|
+
title="Goodmap Admin API",
|
|
188
|
+
version="0.1",
|
|
189
|
+
path="doc",
|
|
190
|
+
annotations=True,
|
|
191
|
+
naming_strategy=_clean_model_name,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
@admin_api_blueprint.route("/locations", methods=["GET"])
|
|
195
|
+
@spec.validate()
|
|
196
|
+
def admin_get_locations():
|
|
197
|
+
"""Get paginated list of all locations for admin panel."""
|
|
198
|
+
return _get_locations_handler(database)
|
|
199
|
+
|
|
200
|
+
@admin_api_blueprint.route("/locations", methods=["POST"])
|
|
201
|
+
@spec.validate(resp=Response(HTTP_400=ErrorResponse))
|
|
202
|
+
def admin_create_location():
|
|
203
|
+
"""Create a new location (admin only)."""
|
|
204
|
+
return _create_location_handler(database, location_model)
|
|
205
|
+
|
|
206
|
+
@admin_api_blueprint.route("/locations/<location_id>", methods=["PUT"])
|
|
207
|
+
@spec.validate(resp=Response(HTTP_400=ErrorResponse, HTTP_404=ErrorResponse))
|
|
208
|
+
def admin_update_location(location_id):
|
|
209
|
+
"""Update an existing location (admin only)."""
|
|
210
|
+
return _update_location_handler(database, location_model, location_id)
|
|
211
|
+
|
|
212
|
+
@admin_api_blueprint.route("/locations/<location_id>", methods=["DELETE"])
|
|
213
|
+
@spec.validate(resp=Response(HTTP_404=ErrorResponse))
|
|
214
|
+
def admin_delete_location(location_id):
|
|
215
|
+
"""Delete a location (admin only)."""
|
|
216
|
+
return _delete_location_handler(database, location_id)
|
|
217
|
+
|
|
218
|
+
@admin_api_blueprint.route("/suggestions", methods=["GET"])
|
|
219
|
+
@spec.validate()
|
|
220
|
+
def admin_get_suggestions():
|
|
221
|
+
"""Get paginated list of location suggestions (admin only)."""
|
|
222
|
+
return _get_suggestions_handler(database)
|
|
223
|
+
|
|
224
|
+
@admin_api_blueprint.route("/suggestions/<suggestion_id>", methods=["PUT"])
|
|
225
|
+
@spec.validate(
|
|
226
|
+
json=SuggestionStatusRequest,
|
|
227
|
+
resp=Response(HTTP_400=ErrorResponse, HTTP_404=ErrorResponse, HTTP_409=ErrorResponse),
|
|
228
|
+
)
|
|
229
|
+
def admin_update_suggestion(suggestion_id):
|
|
230
|
+
"""Accept or reject a location suggestion (admin only)."""
|
|
231
|
+
return _update_suggestion_handler(database, suggestion_id)
|
|
232
|
+
|
|
233
|
+
@admin_api_blueprint.route("/reports", methods=["GET"])
|
|
234
|
+
@spec.validate()
|
|
235
|
+
def admin_get_reports():
|
|
236
|
+
"""Get paginated list of location reports (admin only)."""
|
|
237
|
+
return _get_reports_handler(database)
|
|
238
|
+
|
|
239
|
+
@admin_api_blueprint.route("/reports/<report_id>", methods=["PUT"])
|
|
240
|
+
@spec.validate(
|
|
241
|
+
json=ReportUpdateRequest,
|
|
242
|
+
resp=Response(HTTP_400=ErrorResponse, HTTP_404=ErrorResponse),
|
|
243
|
+
)
|
|
244
|
+
def admin_update_report(report_id):
|
|
245
|
+
"""Update a report's status and/or priority (admin only)."""
|
|
246
|
+
return _update_report_handler(database, report_id)
|
|
247
|
+
|
|
248
|
+
# Register Spectree with blueprint after all routes are defined
|
|
249
|
+
spec.register(admin_api_blueprint)
|
|
250
|
+
|
|
251
|
+
return admin_api_blueprint
|
goodmap/core_api.py
CHANGED
|
@@ -9,28 +9,20 @@ from flask import Blueprint, jsonify, make_response, request
|
|
|
9
9
|
from flask_babel import gettext
|
|
10
10
|
from platzky.config import LanguagesMapping
|
|
11
11
|
from spectree import Response, SpecTree
|
|
12
|
-
from werkzeug.exceptions import BadRequest
|
|
13
12
|
|
|
14
13
|
from goodmap.api_models import (
|
|
15
14
|
CSRFTokenResponse,
|
|
16
15
|
ErrorResponse,
|
|
17
16
|
LocationReportRequest,
|
|
18
17
|
LocationReportResponse,
|
|
19
|
-
ReportUpdateRequest,
|
|
20
18
|
SuccessResponse,
|
|
21
|
-
SuggestionStatusRequest,
|
|
22
19
|
VersionResponse,
|
|
23
20
|
)
|
|
24
21
|
from goodmap.clustering import (
|
|
25
22
|
map_clustering_data_to_proper_lazy_loading_object,
|
|
26
23
|
match_clusters_uuids,
|
|
27
24
|
)
|
|
28
|
-
from goodmap.exceptions import
|
|
29
|
-
LocationAlreadyExistsError,
|
|
30
|
-
LocationNotFoundError,
|
|
31
|
-
LocationValidationError,
|
|
32
|
-
ReportNotFoundError,
|
|
33
|
-
)
|
|
25
|
+
from goodmap.exceptions import LocationValidationError
|
|
34
26
|
from goodmap.formatter import prepare_pin
|
|
35
27
|
from goodmap.json_security import (
|
|
36
28
|
MAX_JSON_DEPTH_LOCATION,
|
|
@@ -48,7 +40,6 @@ CLUSTER_EXTENT = 512
|
|
|
48
40
|
# Error message constants
|
|
49
41
|
ERROR_INVALID_REQUEST_DATA = "Invalid request data"
|
|
50
42
|
ERROR_INVALID_LOCATION_DATA = "Invalid location data"
|
|
51
|
-
ERROR_INTERNAL_ERROR = "An internal error occurred"
|
|
52
43
|
ERROR_LOCATION_NOT_FOUND = "Location not found"
|
|
53
44
|
|
|
54
45
|
logger = logging.getLogger(__name__)
|
|
@@ -370,6 +361,48 @@ def core_pages(
|
|
|
370
361
|
|
|
371
362
|
return jsonify({"categories": categories, "categories_help": proper_categories_help})
|
|
372
363
|
|
|
364
|
+
@core_api_blueprint.route("/categories-full", methods=["GET"])
|
|
365
|
+
@spec.validate()
|
|
366
|
+
def get_categories_full():
|
|
367
|
+
"""Get all categories with their subcategory options in a single request.
|
|
368
|
+
|
|
369
|
+
Returns combined category data to reduce API calls for filter panel loading.
|
|
370
|
+
This endpoint eliminates the need for multiple sequential requests.
|
|
371
|
+
"""
|
|
372
|
+
categories_data = database.get_category_data()
|
|
373
|
+
result = []
|
|
374
|
+
|
|
375
|
+
categories_options_help = categories_data.get("categories_options_help", {})
|
|
376
|
+
|
|
377
|
+
for key, options in categories_data["categories"].items():
|
|
378
|
+
category_entry = {
|
|
379
|
+
"key": key,
|
|
380
|
+
"name": gettext(key),
|
|
381
|
+
"options": make_tuple_translation(options),
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if feature_flags.get("CATEGORIES_HELP", False):
|
|
385
|
+
option_help_list = categories_options_help.get(key, [])
|
|
386
|
+
proper_options_help = []
|
|
387
|
+
for option in option_help_list:
|
|
388
|
+
proper_options_help.append(
|
|
389
|
+
{option: gettext(f"categories_options_help_{option}")}
|
|
390
|
+
)
|
|
391
|
+
category_entry["options_help"] = proper_options_help
|
|
392
|
+
|
|
393
|
+
result.append(category_entry)
|
|
394
|
+
|
|
395
|
+
response = {"categories": result}
|
|
396
|
+
|
|
397
|
+
if feature_flags.get("CATEGORIES_HELP", False):
|
|
398
|
+
categories_help = categories_data.get("categories_help", [])
|
|
399
|
+
proper_categories_help = []
|
|
400
|
+
for option in categories_help:
|
|
401
|
+
proper_categories_help.append({option: gettext(f"categories_help_{option}")})
|
|
402
|
+
response["categories_help"] = proper_categories_help
|
|
403
|
+
|
|
404
|
+
return jsonify(response)
|
|
405
|
+
|
|
373
406
|
@core_api_blueprint.route("/languages", methods=["GET"])
|
|
374
407
|
@spec.validate()
|
|
375
408
|
def get_languages():
|
|
@@ -409,187 +442,6 @@ def core_pages(
|
|
|
409
442
|
}
|
|
410
443
|
)
|
|
411
444
|
|
|
412
|
-
@core_api_blueprint.route("/admin/locations", methods=["GET"])
|
|
413
|
-
@spec.validate()
|
|
414
|
-
def admin_get_locations():
|
|
415
|
-
"""Get paginated list of all locations for admin panel.
|
|
416
|
-
|
|
417
|
-
Supports server-side pagination, sorting, and filtering.
|
|
418
|
-
Defaults to sorting by name.
|
|
419
|
-
"""
|
|
420
|
-
query_params = request.args.to_dict(flat=False)
|
|
421
|
-
if "sort_by" not in query_params:
|
|
422
|
-
query_params["sort_by"] = ["name"]
|
|
423
|
-
result = database.get_locations_paginated(query_params)
|
|
424
|
-
return jsonify(result)
|
|
425
|
-
|
|
426
|
-
@core_api_blueprint.route("/admin/locations", methods=["POST"])
|
|
427
|
-
@spec.validate(resp=Response(HTTP_400=ErrorResponse))
|
|
428
|
-
def admin_create_location():
|
|
429
|
-
"""Create a new location (admin only).
|
|
430
|
-
|
|
431
|
-
Validates location data using Pydantic model and
|
|
432
|
-
adds it to the database.
|
|
433
|
-
"""
|
|
434
|
-
location_data = request.get_json()
|
|
435
|
-
if location_data is None:
|
|
436
|
-
logger.warning("Empty or invalid JSON in admin create location endpoint")
|
|
437
|
-
return make_response(jsonify({"message": ERROR_INVALID_REQUEST_DATA}), 400)
|
|
438
|
-
try:
|
|
439
|
-
location_data.update({"uuid": str(uuid.uuid4())})
|
|
440
|
-
location = location_model.model_validate(location_data)
|
|
441
|
-
database.add_location(location.model_dump())
|
|
442
|
-
except LocationValidationError as e:
|
|
443
|
-
logger.warning(
|
|
444
|
-
"Location validation failed",
|
|
445
|
-
extra={"uuid": e.uuid, "errors": e.validation_errors},
|
|
446
|
-
)
|
|
447
|
-
return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
|
|
448
|
-
except Exception:
|
|
449
|
-
logger.error("Error creating location", exc_info=True)
|
|
450
|
-
return make_response(jsonify({"message": ERROR_INTERNAL_ERROR}), 500)
|
|
451
|
-
return jsonify(location.model_dump())
|
|
452
|
-
|
|
453
|
-
@core_api_blueprint.route("/admin/locations/<location_id>", methods=["PUT"])
|
|
454
|
-
@spec.validate(resp=Response(HTTP_400=ErrorResponse, HTTP_404=ErrorResponse))
|
|
455
|
-
def admin_update_location(location_id):
|
|
456
|
-
"""Update an existing location (admin only).
|
|
457
|
-
|
|
458
|
-
Validates updated location data and persists changes.
|
|
459
|
-
Returns 404 if location not found.
|
|
460
|
-
"""
|
|
461
|
-
location_data = request.get_json()
|
|
462
|
-
if location_data is None:
|
|
463
|
-
logger.warning("Empty or invalid JSON in admin update location endpoint")
|
|
464
|
-
return make_response(jsonify({"message": ERROR_INVALID_REQUEST_DATA}), 400)
|
|
465
|
-
try:
|
|
466
|
-
location_data.update({"uuid": location_id})
|
|
467
|
-
location = location_model.model_validate(location_data)
|
|
468
|
-
database.update_location(location_id, location.model_dump())
|
|
469
|
-
except LocationValidationError as e:
|
|
470
|
-
logger.warning(
|
|
471
|
-
"Location validation failed",
|
|
472
|
-
extra={"uuid": e.uuid, "errors": e.validation_errors},
|
|
473
|
-
)
|
|
474
|
-
return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
|
|
475
|
-
except LocationNotFoundError as e:
|
|
476
|
-
logger.info("Location not found for update", extra={"uuid": e.uuid})
|
|
477
|
-
return make_response(jsonify({"message": ERROR_LOCATION_NOT_FOUND}), 404)
|
|
478
|
-
except Exception:
|
|
479
|
-
logger.error("Error updating location", exc_info=True)
|
|
480
|
-
return make_response(jsonify({"message": ERROR_INTERNAL_ERROR}), 500)
|
|
481
|
-
return jsonify(location.model_dump())
|
|
482
|
-
|
|
483
|
-
@core_api_blueprint.route("/admin/locations/<location_id>", methods=["DELETE"])
|
|
484
|
-
@spec.validate(resp=Response(HTTP_404=ErrorResponse))
|
|
485
|
-
def admin_delete_location(location_id):
|
|
486
|
-
"""Delete a location (admin only).
|
|
487
|
-
|
|
488
|
-
Permanently removes location from database.
|
|
489
|
-
Returns 204 on success, 404 if not found.
|
|
490
|
-
"""
|
|
491
|
-
try:
|
|
492
|
-
database.delete_location(location_id)
|
|
493
|
-
except LocationNotFoundError as e:
|
|
494
|
-
logger.info("Location not found for deletion", extra={"uuid": e.uuid})
|
|
495
|
-
return make_response(jsonify({"message": ERROR_LOCATION_NOT_FOUND}), 404)
|
|
496
|
-
except Exception:
|
|
497
|
-
logger.error("Error deleting location", exc_info=True)
|
|
498
|
-
return make_response(jsonify({"message": ERROR_INTERNAL_ERROR}), 500)
|
|
499
|
-
return "", 204
|
|
500
|
-
|
|
501
|
-
@core_api_blueprint.route("/admin/suggestions", methods=["GET"])
|
|
502
|
-
@spec.validate()
|
|
503
|
-
def admin_get_suggestions():
|
|
504
|
-
"""Get paginated list of location suggestions (admin only).
|
|
505
|
-
|
|
506
|
-
Supports server-side pagination, sorting, and filtering by status.
|
|
507
|
-
"""
|
|
508
|
-
query_params = request.args.to_dict(flat=False)
|
|
509
|
-
result = database.get_suggestions_paginated(query_params)
|
|
510
|
-
return jsonify(result)
|
|
511
|
-
|
|
512
|
-
@core_api_blueprint.route("/admin/suggestions/<suggestion_id>", methods=["PUT"])
|
|
513
|
-
@spec.validate(
|
|
514
|
-
json=SuggestionStatusRequest,
|
|
515
|
-
resp=Response(HTTP_400=ErrorResponse, HTTP_404=ErrorResponse, HTTP_409=ErrorResponse),
|
|
516
|
-
)
|
|
517
|
-
def admin_update_suggestion(suggestion_id):
|
|
518
|
-
"""Accept or reject a location suggestion (admin only).
|
|
519
|
-
|
|
520
|
-
Accepted suggestions are added to locations database.
|
|
521
|
-
Rejected suggestions are marked as rejected.
|
|
522
|
-
"""
|
|
523
|
-
try:
|
|
524
|
-
data = request.get_json()
|
|
525
|
-
status = data["status"] # Validated by Spectree
|
|
526
|
-
suggestion = database.get_suggestion(suggestion_id)
|
|
527
|
-
if not suggestion:
|
|
528
|
-
return make_response(jsonify({"message": "Suggestion not found"}), 404)
|
|
529
|
-
if suggestion.get("status") != "pending":
|
|
530
|
-
return make_response(jsonify({"message": "Suggestion already processed"}), 409)
|
|
531
|
-
if status == "accepted":
|
|
532
|
-
suggestion_data = {k: v for k, v in suggestion.items() if k != "status"}
|
|
533
|
-
database.add_location(suggestion_data)
|
|
534
|
-
database.update_suggestion(suggestion_id, status)
|
|
535
|
-
except LocationValidationError as e:
|
|
536
|
-
logger.warning(
|
|
537
|
-
"Location validation failed in suggestion",
|
|
538
|
-
extra={"uuid": e.uuid, "errors": e.validation_errors},
|
|
539
|
-
)
|
|
540
|
-
return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
|
|
541
|
-
except LocationAlreadyExistsError as e:
|
|
542
|
-
logger.warning(
|
|
543
|
-
"Attempted to create duplicate location from suggestion", extra={"uuid": e.uuid}
|
|
544
|
-
)
|
|
545
|
-
return make_response(jsonify({"message": "Location already exists"}), 409)
|
|
546
|
-
except Exception:
|
|
547
|
-
logger.error("Error processing suggestion", exc_info=True)
|
|
548
|
-
return make_response(jsonify({"message": ERROR_INTERNAL_ERROR}), 500)
|
|
549
|
-
return jsonify(database.get_suggestion(suggestion_id))
|
|
550
|
-
|
|
551
|
-
@core_api_blueprint.route("/admin/reports", methods=["GET"])
|
|
552
|
-
@spec.validate()
|
|
553
|
-
def admin_get_reports():
|
|
554
|
-
"""Get paginated list of location reports (admin only).
|
|
555
|
-
|
|
556
|
-
Supports server-side pagination, sorting,
|
|
557
|
-
and filtering by status/priority.
|
|
558
|
-
"""
|
|
559
|
-
query_params = request.args.to_dict(flat=False)
|
|
560
|
-
result = database.get_reports_paginated(query_params)
|
|
561
|
-
return jsonify(result)
|
|
562
|
-
|
|
563
|
-
@core_api_blueprint.route("/admin/reports/<report_id>", methods=["PUT"])
|
|
564
|
-
@spec.validate(
|
|
565
|
-
json=ReportUpdateRequest,
|
|
566
|
-
resp=Response(HTTP_400=ErrorResponse, HTTP_404=ErrorResponse),
|
|
567
|
-
)
|
|
568
|
-
def admin_update_report(report_id):
|
|
569
|
-
"""Update a report's status and/or priority (admin only).
|
|
570
|
-
|
|
571
|
-
Allows changing report status to resolved/rejected
|
|
572
|
-
and adjusting priority level.
|
|
573
|
-
"""
|
|
574
|
-
try:
|
|
575
|
-
data = request.get_json()
|
|
576
|
-
status = data.get("status")
|
|
577
|
-
priority = data.get("priority")
|
|
578
|
-
report = database.get_report(report_id)
|
|
579
|
-
if not report:
|
|
580
|
-
return make_response(jsonify({"message": "Report not found"}), 404)
|
|
581
|
-
database.update_report(report_id, status=status, priority=priority)
|
|
582
|
-
except BadRequest:
|
|
583
|
-
logger.warning("Invalid JSON in report update endpoint")
|
|
584
|
-
return make_response(jsonify({"message": ERROR_INVALID_REQUEST_DATA}), 400)
|
|
585
|
-
except ReportNotFoundError as e:
|
|
586
|
-
logger.info("Report not found for update", extra={"uuid": e.uuid})
|
|
587
|
-
return make_response(jsonify({"message": "Report not found"}), 404)
|
|
588
|
-
except Exception:
|
|
589
|
-
logger.error("Error updating report", exc_info=True)
|
|
590
|
-
return make_response(jsonify({"message": ERROR_INTERNAL_ERROR}), 500)
|
|
591
|
-
return jsonify(database.get_report(report_id))
|
|
592
|
-
|
|
593
445
|
# Register Spectree with blueprint after all routes are defined
|
|
594
446
|
spec.register(core_api_blueprint)
|
|
595
447
|
|
goodmap/goodmap.py
CHANGED
|
@@ -8,6 +8,7 @@ from platzky import platzky
|
|
|
8
8
|
from platzky.config import languages_dict
|
|
9
9
|
from platzky.models import CmsModule
|
|
10
10
|
|
|
11
|
+
from goodmap.admin_api import admin_pages
|
|
11
12
|
from goodmap.config import GoodmapConfig
|
|
12
13
|
from goodmap.core_api import core_pages
|
|
13
14
|
from goodmap.data_models.location import create_location_model
|
|
@@ -76,7 +77,7 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
|
|
|
76
77
|
|
|
77
78
|
# Use the extended db method directly (already bound by extend_db_with_goodmap_queries)
|
|
78
79
|
try:
|
|
79
|
-
category_data = app.db.get_category_data()
|
|
80
|
+
category_data = app.db.get_category_data() # type: ignore[attr-defined]
|
|
80
81
|
categories = category_data.get("categories", {})
|
|
81
82
|
except (KeyError, AttributeError):
|
|
82
83
|
# Handle case where categories don't exist in the data
|
|
@@ -105,6 +106,7 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
|
|
|
105
106
|
feature_flags=config.feature_flags,
|
|
106
107
|
)
|
|
107
108
|
app.register_blueprint(cp)
|
|
109
|
+
|
|
108
110
|
goodmap = Blueprint("goodmap", __name__, url_prefix="/", template_folder="templates")
|
|
109
111
|
|
|
110
112
|
@goodmap.route("/")
|
|
@@ -119,7 +121,7 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
|
|
|
119
121
|
"""
|
|
120
122
|
# Prepare location schema for frontend dynamic forms
|
|
121
123
|
# Include full schema from Pydantic model for better type information
|
|
122
|
-
category_data = app.db.get_category_data()
|
|
124
|
+
category_data = app.db.get_category_data() # type: ignore[attr-defined]
|
|
123
125
|
categories = category_data.get("categories", {})
|
|
124
126
|
|
|
125
127
|
# Get full JSON schema from Pydantic model
|
|
@@ -152,10 +154,14 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
|
|
|
152
154
|
|
|
153
155
|
Requires user to be logged in (redirects to /admin if not).
|
|
154
156
|
Provides admin panel for managing locations, suggestions, and reports.
|
|
157
|
+
Only available when ENABLE_ADMIN_PANEL feature flag is enabled.
|
|
155
158
|
|
|
156
159
|
Returns:
|
|
157
160
|
Rendered goodmap-admin.html template or redirect to login
|
|
158
161
|
"""
|
|
162
|
+
if not is_feature_enabled(config, "ENABLE_ADMIN_PANEL"):
|
|
163
|
+
return redirect("/")
|
|
164
|
+
|
|
159
165
|
user = session.get("user", None)
|
|
160
166
|
if not user:
|
|
161
167
|
return redirect("/admin")
|
|
@@ -171,14 +177,19 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
|
|
|
171
177
|
)
|
|
172
178
|
|
|
173
179
|
app.register_blueprint(goodmap)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
|
|
181
|
+
if is_feature_enabled(config, "ENABLE_ADMIN_PANEL"):
|
|
182
|
+
admin_bp = admin_pages(app.db, location_model)
|
|
183
|
+
app.register_blueprint(admin_bp)
|
|
184
|
+
|
|
185
|
+
goodmap_cms_modules = CmsModule.model_validate(
|
|
186
|
+
{
|
|
187
|
+
"name": "Map admin panel",
|
|
188
|
+
"description": "Admin panel for managing map data",
|
|
189
|
+
"slug": "goodmap-admin",
|
|
190
|
+
"template": "goodmap-admin.html",
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
app.add_cms_module(goodmap_cms_modules)
|
|
183
194
|
|
|
184
195
|
return app
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
goodmap/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
2
|
+
goodmap/admin_api.py,sha256=dt0R_ZVREDy06RtnE-fTYYezU4NaZe5CBl2KGJx87Jw,10146
|
|
2
3
|
goodmap/api_models.py,sha256=Bv4OTGuckNneCrxaQ1Y_PMeu7YFLvGUqU2EorvDlUjY,3438
|
|
3
4
|
goodmap/clustering.py,sha256=ULB-fPNOUDblgpBK4vzuo0o2yqIcvG84F3R6Za2X_l4,2905
|
|
4
5
|
goodmap/config.py,sha256=CsmC1zuvVab90VW50dtARHbFJpy2vfsIfbque8Zgc-U,1313
|
|
5
6
|
goodmap/core.py,sha256=AgdGLfeJvL7TlTX893NR2YdCS8EuXx93Gx6ndvWws7s,2673
|
|
6
|
-
goodmap/core_api.py,sha256=
|
|
7
|
+
goodmap/core_api.py,sha256=Av-kim3NjkHQD4B139O3IyXGl7PABzkyVui1hmkiv5I,18089
|
|
7
8
|
goodmap/data_models/location.py,sha256=_I27R06ovEL9ctv_SZ3yoLL-RwmyE3VDsVOG4a89q50,6798
|
|
8
9
|
goodmap/data_validator.py,sha256=lBmVAPxvSmEOdUGeVYSjUvVVmKfPyq4CWoHfczTtEMM,4090
|
|
9
10
|
goodmap/db.py,sha256=TcqYGbK5yk6S735Si1AzjNqcbB1nsd9pFGOy5qN9Vec,46589
|
|
10
11
|
goodmap/exceptions.py,sha256=jkFAUoc5LHk8iPjxHxbcRp8W6qFCSEA25A8XaSwxwyo,2906
|
|
11
12
|
goodmap/formatter.py,sha256=4rqcg9A9Y9opAi7eb8kMDdUC03M3uzZgCxx29cvvIag,1403
|
|
12
|
-
goodmap/goodmap.py,sha256=
|
|
13
|
+
goodmap/goodmap.py,sha256=uWjnOt00gWwAFydDw6EMD-EgAmlp-6ZSEdhSevDUBKg,7273
|
|
13
14
|
goodmap/json_security.py,sha256=EHAxNlb16AVwphgf4F7yObtMZpbR9M538dwn_STRcMo,3275
|
|
14
15
|
goodmap/templates/goodmap-admin.html,sha256=LSiOZ9-n29CnlfVNwdgmXwT7Xe7t5gvGh1xSrFGqOIY,35669
|
|
15
16
|
goodmap/templates/map.html,sha256=Uk7FFrZwvHZvG0DDaQrGW5ZrIMD21XrJzMub76uIlAg,4348
|
|
16
|
-
goodmap-1.
|
|
17
|
-
goodmap-1.
|
|
18
|
-
goodmap-1.
|
|
19
|
-
goodmap-1.
|
|
17
|
+
goodmap-1.3.1.dist-info/LICENSE.md,sha256=nkCQOR7uheLRvHRfXmwx9LhBnMcPeBU9d4ebLojDiQU,1067
|
|
18
|
+
goodmap-1.3.1.dist-info/METADATA,sha256=bdXqFG7LTW-rAtkmXCKgu5pq9KINVOvRAUsaCuHHEHQ,5798
|
|
19
|
+
goodmap-1.3.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
20
|
+
goodmap-1.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|