goodmap 0.5.0__py3-none-any.whl → 0.5.2__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 CHANGED
@@ -13,6 +13,74 @@ def make_tuple_translation(keys_to_translate):
13
13
  return [(x, gettext(x)) for x in keys_to_translate]
14
14
 
15
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
+
16
84
  def core_pages(
17
85
  database, languages: LanguagesMapping, notifier_function, csrf_generator, location_model
18
86
  ) -> Blueprint:
@@ -46,6 +114,7 @@ def core_pages(
46
114
  suggested_location = request.get_json()
47
115
  suggested_location.update({"uuid": str(uuid.uuid4())})
48
116
  location = location_model.model_validate(suggested_location)
117
+ database.add_suggestion(location.model_dump())
49
118
  message = (
50
119
  f"A new location has been suggested under uuid: '{location.uuid}' "
51
120
  f"at position: {location.position}"
@@ -64,11 +133,22 @@ def core_pages(
64
133
  """Report location"""
65
134
  try:
66
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)
67
144
  message = (
68
145
  f"A location has been reported: '{location_report['id']}' "
69
146
  f"with problem: {location_report['description']}"
70
147
  )
71
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)
72
152
  except Exception as e:
73
153
  error_message = gettext("Error sending notification")
74
154
  return make_response(jsonify({"message": f"{error_message} : {e}"}), 400)
@@ -137,4 +217,136 @@ def core_pages(
137
217
  csrf_token = csrf_generator()
138
218
  return {"csrf_token": csrf_token}
139
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
+
140
352
  return core_api_blueprint
@@ -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: str = ""
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]: