goodmap 0.5.3__py3-none-any.whl → 1.0.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
@@ -22,74 +22,6 @@ def get_or_none(data, *keys):
22
22
  return data
23
23
 
24
24
 
25
- def paginate_results(items, raw_params, sort_by_default=None):
26
- """
27
- Apply pagination and sorting to a list of items.
28
-
29
- Args:
30
- items: The list of items to paginate
31
- raw_params: The query parameters dictionary
32
-
33
- Returns:
34
- Tuple of (paginated_items, pagination_metadata)
35
- """
36
- # Extract pagination parameters
37
- try:
38
- page = int(raw_params.pop("page", ["1"])[0])
39
- except ValueError:
40
- page = 1
41
-
42
- per_page_raw = raw_params.pop("per_page", [None])[0]
43
- if per_page_raw is None:
44
- per_page = 20
45
- elif per_page_raw == "all":
46
- per_page = None
47
- else:
48
- try:
49
- per_page = max(1, int(per_page_raw))
50
- except ValueError:
51
- per_page = 20
52
-
53
- sort_by = raw_params.pop("sort_by", [None])[0] or sort_by_default
54
- sort_order = raw_params.pop("sort_order", ["asc"])[0].lower()
55
-
56
- def get_sort_key(item):
57
- if not sort_by:
58
- return None
59
-
60
- value = None
61
- if isinstance(item, dict):
62
- value = item.get(sort_by)
63
- else:
64
- value = getattr(item, sort_by, None)
65
-
66
- return (value is not None, value)
67
-
68
- if sort_by:
69
- reverse = sort_order == "desc"
70
- items.sort(key=get_sort_key, reverse=reverse)
71
-
72
- # Apply pagination
73
- total = len(items)
74
- if per_page:
75
- start = (page - 1) * per_page
76
- end = start + per_page
77
- page_items = items[start:end]
78
- total_pages = (total + per_page - 1) // per_page
79
- else:
80
- page_items = items
81
- total_pages = 1
82
- page = 1
83
- per_page = total
84
-
85
- return page_items, {
86
- "total": total,
87
- "page": page,
88
- "per_page": per_page,
89
- "total_pages": total_pages,
90
- }
91
-
92
-
93
25
  def core_pages(
94
26
  database,
95
27
  languages: LanguagesMapping,
@@ -207,13 +139,14 @@ def core_pages(
207
139
  class Categories(Resource):
208
140
  def get(self):
209
141
  """Shows all available categories"""
210
- all_data = database.get_data()
211
- categories = make_tuple_translation(all_data["categories"].keys())
142
+ raw_categories = database.get_categories()
143
+ categories = make_tuple_translation(raw_categories)
212
144
 
213
145
  if not feature_flags.get("CATEGORIES_HELP", False):
214
146
  return jsonify(categories)
215
147
  else:
216
- categories_help = all_data["categories_help"]
148
+ category_data = database.get_category_data()
149
+ categories_help = category_data["categories_help"]
217
150
  proper_categories_help = []
218
151
  if categories_help is not None:
219
152
  for option in categories_help:
@@ -233,11 +166,11 @@ def core_pages(
233
166
  class CategoryTypes(Resource):
234
167
  def get(self, category_type):
235
168
  """Shows all available types in category"""
236
- all_data = database.get_data()
237
- local_data = make_tuple_translation(all_data["categories"][category_type])
169
+ category_data = database.get_category_data(category_type)
170
+ local_data = make_tuple_translation(category_data["categories"][category_type])
238
171
 
239
172
  categories_options_help = get_or_none(
240
- all_data, "categories_options_help", category_type
173
+ category_data, "categories_options_help", category_type
241
174
  )
242
175
  proper_categories_options_help = []
243
176
  if categories_options_help is not None:
@@ -268,14 +201,11 @@ def core_pages(
268
201
  Shows full list of locations, with optional server-side pagination, sorting,
269
202
  and filtering.
270
203
  """
271
- # Raw query params from request
272
- raw_params = request.args.to_dict(flat=False)
273
- all_locations = database.get_locations(raw_params)
274
- page_items, pagination = paginate_results(
275
- all_locations, raw_params, sort_by_default="name"
276
- )
277
- items = [x.model_dump() for x in page_items]
278
- return jsonify({"items": items, **pagination})
204
+ query_params = request.args.to_dict(flat=False)
205
+ if "sort_by" not in query_params:
206
+ query_params["sort_by"] = ["name"]
207
+ result = database.get_locations_paginated(query_params)
208
+ return jsonify(result)
279
209
 
280
210
  def post(self):
281
211
  """
@@ -328,10 +258,9 @@ def core_pages(
328
258
  List location suggestions, with optional server-side pagination, sorting,
329
259
  and filtering by status.
330
260
  """
331
- raw_params = request.args.to_dict(flat=False)
332
- suggestions = database.get_suggestions(raw_params)
333
- page_items, pagination = paginate_results(suggestions, raw_params)
334
- return jsonify({"items": page_items, **pagination})
261
+ query_params = request.args.to_dict(flat=False)
262
+ result = database.get_suggestions_paginated(query_params)
263
+ return jsonify(result)
335
264
 
336
265
  @core_api.route("/admin/suggestions/<suggestion_id>")
337
266
  class AdminManageSuggestion(Resource):
@@ -364,10 +293,9 @@ def core_pages(
364
293
  List location reports, with optional server-side pagination, sorting,
365
294
  and filtering by status/priority.
366
295
  """
367
- raw_params = request.args.to_dict(flat=False)
368
- reports = database.get_reports(raw_params)
369
- page_items, pagination = paginate_results(reports, raw_params)
370
- return jsonify({"items": page_items, **pagination})
296
+ query_params = request.args.to_dict(flat=False)
297
+ result = database.get_reports_paginated(query_params)
298
+ return jsonify(result)
371
299
 
372
300
  @core_api.route("/admin/reports/<report_id>")
373
301
  class AdminManageReport(Resource):
goodmap/db.py CHANGED
@@ -4,11 +4,53 @@ import tempfile
4
4
  from functools import partial
5
5
 
6
6
  from goodmap.core import get_queried_data
7
+ from goodmap.data_models.location import LocationBase
7
8
 
8
9
  # TODO file is temporary solution to be compatible with old, static code,
9
10
  # it should be replaced with dynamic solution
10
11
 
11
12
 
13
+ def __parse_pagination_params(query):
14
+ """Extract and validate pagination parameters from query."""
15
+ try:
16
+ page = max(1, int(query.get("page", ["1"])[0]))
17
+ except (ValueError, IndexError, TypeError):
18
+ page = 1
19
+
20
+ per_page_raw = query.get("per_page", ["20"])[0] if query.get("per_page") else "20"
21
+ if per_page_raw == "all":
22
+ per_page = None
23
+ else:
24
+ try:
25
+ per_page = max(1, min(int(per_page_raw), 1000)) # Cap at 1000
26
+ except (ValueError, TypeError):
27
+ per_page = 20
28
+
29
+ sort_by = query.get("sort_by", [None])[0]
30
+ sort_order = query.get("sort_order", ["asc"])[0] if query.get("sort_order") else "asc"
31
+
32
+ return page, per_page, sort_by, sort_order.lower()
33
+
34
+
35
+ def __build_pagination_response(items, total, page, per_page):
36
+ """Build standardized pagination response."""
37
+ if per_page:
38
+ total_pages = (total + per_page - 1) // per_page
39
+ else:
40
+ total_pages = 1
41
+ per_page = total
42
+
43
+ return {
44
+ "items": items,
45
+ "pagination": {
46
+ "total": total,
47
+ "page": page,
48
+ "per_page": per_page,
49
+ "total_pages": total_pages,
50
+ },
51
+ }
52
+
53
+
12
54
  def json_file_atomic_dump(data, file_path):
13
55
  dir_name = os.path.dirname(file_path)
14
56
  with tempfile.NamedTemporaryFile("w", dir=dir_name, delete=False) as temp_file:
@@ -18,6 +60,264 @@ def json_file_atomic_dump(data, file_path):
18
60
  os.replace(temp_file.name, file_path)
19
61
 
20
62
 
63
+ class PaginationHelper:
64
+ """Common pagination utility to eliminate duplication across backends."""
65
+
66
+ @staticmethod
67
+ def get_sort_key(item, sort_by):
68
+ """Extract sort key from item for both dict and object types."""
69
+ if sort_by == "name" and hasattr(item, "name"):
70
+ value = item.name
71
+ elif isinstance(item, dict):
72
+ value = item.get(sort_by)
73
+ else:
74
+ value = getattr(item, sort_by, None)
75
+ return (value is not None, value or "")
76
+
77
+ @staticmethod
78
+ def apply_pagination_and_sorting(items, page, per_page, sort_by, sort_order):
79
+ """Apply sorting and pagination to a list of items."""
80
+ # Apply sorting
81
+ if sort_by:
82
+ reverse = sort_order == "desc"
83
+ items.sort(
84
+ key=lambda item: PaginationHelper.get_sort_key(item, sort_by), reverse=reverse # type: ignore
85
+ )
86
+
87
+ total_count = len(items)
88
+
89
+ # Apply pagination
90
+ if per_page:
91
+ start_idx = (page - 1) * per_page
92
+ end_idx = start_idx + per_page
93
+ paginated_items = items[start_idx:end_idx]
94
+ else:
95
+ paginated_items = items
96
+
97
+ return paginated_items, total_count
98
+
99
+ @staticmethod
100
+ def apply_filters(items, filters):
101
+ """Apply filtering based on provided filters dictionary."""
102
+ filtered_items = items
103
+
104
+ # Apply status filtering
105
+ if "status" in filters:
106
+ statuses = filters["status"]
107
+ if statuses:
108
+ filtered_items = [
109
+ item
110
+ for item in filtered_items
111
+ if (
112
+ item.get("status")
113
+ if isinstance(item, dict)
114
+ else getattr(item, "status", None)
115
+ )
116
+ in statuses
117
+ ]
118
+
119
+ # Apply priority filtering
120
+ if "priority" in filters:
121
+ priorities = filters["priority"]
122
+ if priorities:
123
+ filtered_items = [
124
+ item
125
+ for item in filtered_items
126
+ if (
127
+ item.get("priority")
128
+ if isinstance(item, dict)
129
+ else getattr(item, "priority", None)
130
+ )
131
+ in priorities
132
+ ]
133
+
134
+ return filtered_items
135
+
136
+ @staticmethod
137
+ def serialize_items(items):
138
+ """Convert items to dict if needed (for location models)."""
139
+ if items and hasattr(items[0], "model_dump"):
140
+ return [x.model_dump() for x in items]
141
+ else:
142
+ return items
143
+
144
+ @staticmethod
145
+ def create_paginated_response(items, query, extract_filters_func=None):
146
+ """Create a complete paginated response with all common logic."""
147
+ # Parse pagination parameters using the existing function
148
+ try:
149
+ page = max(1, int(query.get("page", ["1"])[0]))
150
+ except (ValueError, IndexError, TypeError):
151
+ page = 1
152
+
153
+ per_page_raw = query.get("per_page", ["20"])[0] if query.get("per_page") else "20"
154
+ if per_page_raw == "all":
155
+ per_page = None
156
+ else:
157
+ try:
158
+ per_page = max(1, min(int(per_page_raw), 1000)) # Cap at 1000
159
+ except (ValueError, TypeError):
160
+ per_page = 20
161
+
162
+ sort_by = query.get("sort_by", [None])[0]
163
+ sort_order = query.get("sort_order", ["asc"])[0] if query.get("sort_order") else "asc"
164
+ sort_order = sort_order.lower()
165
+
166
+ # Apply filters if any
167
+ filters = {}
168
+ if query:
169
+ if "status" in query:
170
+ filters["status"] = query["status"]
171
+ if "priority" in query:
172
+ filters["priority"] = query["priority"]
173
+
174
+ # Allow custom filter extraction
175
+ if extract_filters_func:
176
+ custom_filters = extract_filters_func(query)
177
+ filters.update(custom_filters)
178
+
179
+ if filters:
180
+ items = PaginationHelper.apply_filters(items, filters)
181
+
182
+ # Apply pagination and sorting
183
+ paginated_items, total_count = PaginationHelper.apply_pagination_and_sorting(
184
+ items, page, per_page, sort_by, sort_order
185
+ )
186
+
187
+ # Serialize items if needed
188
+ serialized_items = PaginationHelper.serialize_items(paginated_items)
189
+
190
+ # Build pagination response directly
191
+ if per_page:
192
+ total_pages = (total_count + per_page - 1) // per_page
193
+ else:
194
+ total_pages = 1
195
+ per_page = total_count
196
+
197
+ return {
198
+ "items": serialized_items,
199
+ "pagination": {
200
+ "total": total_count,
201
+ "page": page,
202
+ "per_page": per_page,
203
+ "total_pages": total_pages,
204
+ },
205
+ }
206
+
207
+
208
+ class FileIOHelper:
209
+ """Common file I/O utilities to eliminate duplication."""
210
+
211
+ @staticmethod
212
+ def read_json_file(file_path):
213
+ """Read and parse JSON file."""
214
+ with open(file_path, "r") as file:
215
+ return json.load(file)
216
+
217
+ @staticmethod
218
+ def write_json_file_atomic(data, file_path):
219
+ """Write JSON data to file atomically."""
220
+ json_file_atomic_dump(data, file_path)
221
+
222
+ @staticmethod
223
+ def get_data_from_file(file_path, data_key="map"):
224
+ """Get data from JSON file with specified key structure."""
225
+ json_data = FileIOHelper.read_json_file(file_path)
226
+ return json_data.get(data_key, {})
227
+
228
+
229
+ class ErrorHelper:
230
+ """Common error handling utilities."""
231
+
232
+ @staticmethod
233
+ def raise_already_exists_error(item_type, uuid):
234
+ """Raise standardized 'already exists' error."""
235
+ raise ValueError(f"{item_type} with uuid {uuid} already exists")
236
+
237
+ @staticmethod
238
+ def raise_not_found_error(item_type, uuid):
239
+ """Raise standardized 'not found' error."""
240
+ raise ValueError(f"{item_type} with uuid {uuid} not found")
241
+
242
+ @staticmethod
243
+ def check_item_exists(items, uuid, item_type):
244
+ """Check if item with UUID exists and raise error if it does."""
245
+ existing = next(
246
+ (
247
+ item
248
+ for item in items
249
+ if (item.get("uuid") if isinstance(item, dict) else getattr(item, "uuid", None))
250
+ == uuid
251
+ ),
252
+ None,
253
+ )
254
+ if existing:
255
+ ErrorHelper.raise_already_exists_error(item_type, uuid)
256
+
257
+ @staticmethod
258
+ def find_item_by_uuid(items, uuid, item_type):
259
+ """Find item by UUID and raise error if not found."""
260
+ item = next(
261
+ (
262
+ item
263
+ for item in items
264
+ if (item.get("uuid") if isinstance(item, dict) else getattr(item, "uuid", None))
265
+ == uuid
266
+ ),
267
+ None,
268
+ )
269
+ if not item:
270
+ ErrorHelper.raise_not_found_error(item_type, uuid)
271
+ return item
272
+
273
+
274
+ class CRUDHelper:
275
+ """Common CRUD operation utilities to eliminate duplication."""
276
+
277
+ @staticmethod
278
+ def add_item_to_json_db(db_data, collection_name, item_data, default_status=None):
279
+ """Add item to JSON in-memory database."""
280
+ collection = db_data.setdefault(collection_name, [])
281
+ ErrorHelper.check_item_exists(
282
+ collection, item_data.get("uuid"), collection_name.rstrip("s").capitalize()
283
+ )
284
+
285
+ record = dict(item_data)
286
+ if default_status:
287
+ record["status"] = default_status
288
+ collection.append(record)
289
+
290
+ @staticmethod
291
+ def add_item_to_json_file_db(file_path, collection_name, item_data, default_status=None):
292
+ """Add item to JSON file database."""
293
+ json_file = FileIOHelper.read_json_file(file_path)
294
+ collection = json_file["map"].get(collection_name, [])
295
+
296
+ ErrorHelper.check_item_exists(
297
+ collection, item_data.get("uuid"), collection_name.rstrip("s").capitalize()
298
+ )
299
+
300
+ record = dict(item_data)
301
+ if default_status:
302
+ record["status"] = default_status
303
+ collection.append(record)
304
+ json_file["map"][collection_name] = collection
305
+
306
+ FileIOHelper.write_json_file_atomic(json_file, file_path)
307
+
308
+ @staticmethod
309
+ def add_item_to_mongodb(db_collection, item_data, item_type, default_status=None):
310
+ """Add item to MongoDB database."""
311
+ existing = db_collection.find_one({"uuid": item_data.get("uuid")})
312
+ if existing:
313
+ ErrorHelper.raise_already_exists_error(item_type, item_data.get("uuid"))
314
+
315
+ record = dict(item_data)
316
+ if default_status:
317
+ record["status"] = default_status
318
+ db_collection.insert_one(record)
319
+
320
+
21
321
  # ------------------------------------------------
22
322
  # get_location_obligatory_fields
23
323
 
@@ -35,6 +335,13 @@ def google_json_db_get_location_obligatory_fields(db):
35
335
  return json.loads(db.blob.download_as_text(client=None))["map"]["location_obligatory_fields"]
36
336
 
37
337
 
338
+ def mongodb_db_get_location_obligatory_fields(db):
339
+ config_doc = db.db.config.find_one({"_id": "map_config"})
340
+ if config_doc and "location_obligatory_fields" in config_doc:
341
+ return config_doc["location_obligatory_fields"]
342
+ return []
343
+
344
+
38
345
  def get_location_obligatory_fields(db):
39
346
  return globals()[f"{db.module_name}_get_location_obligatory_fields"](db)
40
347
 
@@ -56,10 +363,140 @@ def json_db_get_data(self):
56
363
  return self.data
57
364
 
58
365
 
366
+ def mongodb_db_get_data(self):
367
+ config_doc = self.db.config.find_one({"_id": "map_config"})
368
+ if config_doc:
369
+ return {
370
+ "data": list(self.db.locations.find({}, {"_id": 0})),
371
+ "categories": config_doc.get("categories", {}),
372
+ "location_obligatory_fields": config_doc.get("location_obligatory_fields", []),
373
+ # Backward-compat keys expected by core_api today
374
+ "visible_data": config_doc.get("visible_data", {}),
375
+ "meta_data": config_doc.get("meta_data", {}),
376
+ }
377
+ return {
378
+ "data": [],
379
+ "categories": {},
380
+ "location_obligatory_fields": [],
381
+ "visible_data": {},
382
+ "meta_data": {},
383
+ }
384
+
385
+
59
386
  def get_data(db):
60
387
  return globals()[f"{db.module_name}_get_data"]
61
388
 
62
389
 
390
+ # ------------------------------------------------
391
+ # get_categories
392
+
393
+
394
+ def json_db_get_categories(self):
395
+ return self.data["categories"].keys()
396
+
397
+
398
+ def json_file_db_get_categories(self):
399
+ with open(self.data_file_path, "r") as file:
400
+ return json.load(file)["map"]["categories"].keys()
401
+
402
+
403
+ def google_json_db_get_categories(self):
404
+ return json.loads(self.blob.download_as_text(client=None))["map"]["categories"].keys()
405
+
406
+
407
+ def mongodb_db_get_categories(self):
408
+ config_doc = self.db.config.find_one({"_id": "map_config"})
409
+ if config_doc and "categories" in config_doc:
410
+ return list(config_doc["categories"].keys())
411
+ return []
412
+
413
+
414
+ def get_categories(db):
415
+ return globals()[f"{db.module_name}_get_categories"]
416
+
417
+
418
+ # ------------------------------------------------
419
+ # get_category_data
420
+
421
+
422
+ def json_db_get_category_data(self, category_type=None):
423
+ if category_type:
424
+ return {
425
+ "categories": {category_type: self.data["categories"].get(category_type, [])},
426
+ "categories_help": self.data.get("categories_help", []),
427
+ "categories_options_help": {
428
+ category_type: self.data.get("categories_options_help", {}).get(category_type, [])
429
+ },
430
+ }
431
+ return {
432
+ "categories": self.data["categories"],
433
+ "categories_help": self.data.get("categories_help", []),
434
+ "categories_options_help": self.data.get("categories_options_help", {}),
435
+ }
436
+
437
+
438
+ def json_file_db_get_category_data(self, category_type=None):
439
+ with open(self.data_file_path, "r") as file:
440
+ data = json.load(file)["map"]
441
+ if category_type:
442
+ return {
443
+ "categories": {category_type: data["categories"].get(category_type, [])},
444
+ "categories_help": data.get("categories_help", []),
445
+ "categories_options_help": {
446
+ category_type: data.get("categories_options_help", {}).get(category_type, [])
447
+ },
448
+ }
449
+ return {
450
+ "categories": data["categories"],
451
+ "categories_help": data.get("categories_help", []),
452
+ "categories_options_help": data.get("categories_options_help", {}),
453
+ }
454
+
455
+
456
+ def google_json_db_get_category_data(self, category_type=None):
457
+ data = json.loads(self.blob.download_as_text(client=None))["map"]
458
+ if category_type:
459
+ return {
460
+ "categories": {category_type: data["categories"].get(category_type, [])},
461
+ "categories_help": data.get("categories_help", []),
462
+ "categories_options_help": {
463
+ category_type: data.get("categories_options_help", {}).get(category_type, [])
464
+ },
465
+ }
466
+ return {
467
+ "categories": data["categories"],
468
+ "categories_help": data.get("categories_help", []),
469
+ "categories_options_help": data.get("categories_options_help", {}),
470
+ }
471
+
472
+
473
+ def mongodb_db_get_category_data(self, category_type=None):
474
+ config_doc = self.db.config.find_one({"_id": "map_config"})
475
+ if config_doc:
476
+ if category_type:
477
+ return {
478
+ "categories": {
479
+ category_type: config_doc.get("categories", {}).get(category_type, [])
480
+ },
481
+ "categories_help": config_doc.get("categories_help", []),
482
+ "categories_options_help": {
483
+ category_type: config_doc.get("categories_options_help", {}).get(
484
+ category_type, []
485
+ )
486
+ },
487
+ }
488
+ return {
489
+ "categories": config_doc.get("categories", {}),
490
+ "categories_help": config_doc.get("categories_help", []),
491
+ "categories_options_help": config_doc.get("categories_options_help", {}),
492
+ }
493
+ return {"categories": {}, "categories_help": [], "categories_options_help": {}}
494
+
495
+
496
+ def get_category_data(db):
497
+ return globals()[f"{db.module_name}_get_category_data"]
498
+
499
+
63
500
  # ------------------------------------------------
64
501
  # get_location
65
502
 
@@ -85,6 +522,11 @@ def json_db_get_location(self, uuid, location_model):
85
522
  return get_location_from_raw_data(self.data, uuid, location_model)
86
523
 
87
524
 
525
+ def mongodb_db_get_location(self, uuid, location_model):
526
+ location_doc = self.db.locations.find_one({"uuid": uuid}, {"_id": 0})
527
+ return location_model.model_validate(location_doc) if location_doc else None
528
+
529
+
88
530
  def get_location(db, location_model):
89
531
  return partial(globals()[f"{db.module_name}_get_location"], location_model=location_model)
90
532
 
@@ -113,10 +555,91 @@ def json_db_get_locations(self, query, location_model):
113
555
  return get_locations_list_from_raw_data(self.data, query, location_model)
114
556
 
115
557
 
558
+ def mongodb_db_get_locations(self, query, location_model):
559
+ mongo_query = {}
560
+ for key, values in query.items():
561
+ if values:
562
+ mongo_query[key] = {"$in": values}
563
+
564
+ projection = {"_id": 0, "uuid": 1, "position": 1, "remark": 1}
565
+ data = self.db.locations.find(mongo_query, projection)
566
+ return (LocationBase.model_validate(loc) for loc in data)
567
+
568
+
116
569
  def get_locations(db, location_model):
117
570
  return partial(globals()[f"{db.module_name}_get_locations"], location_model=location_model)
118
571
 
119
572
 
573
+ def google_json_db_get_locations_paginated(self, query, location_model):
574
+ """Google JSON locations with improved pagination."""
575
+ # Get all locations from raw data
576
+ data = json.loads(self.blob.download_as_text(client=None))["map"]
577
+ all_locations = list(get_locations_list_from_raw_data(data, query, location_model))
578
+ return PaginationHelper.create_paginated_response(all_locations, query)
579
+
580
+
581
+ def json_db_get_locations_paginated(self, query, location_model):
582
+ """JSON locations with improved pagination."""
583
+ # Get all locations from raw data
584
+ all_locations = list(get_locations_list_from_raw_data(self.data, query, location_model))
585
+ return PaginationHelper.create_paginated_response(all_locations, query)
586
+
587
+
588
+ def json_file_db_get_locations_paginated(self, query, location_model):
589
+ """JSON file locations with improved pagination."""
590
+ data = FileIOHelper.get_data_from_file(self.data_file_path)
591
+ # Get all locations from raw data
592
+ all_locations = list(get_locations_list_from_raw_data(data, query, location_model))
593
+ return PaginationHelper.create_paginated_response(all_locations, query)
594
+
595
+
596
+ def mongodb_db_get_locations_paginated(self, query, location_model):
597
+ """MongoDB locations with improved pagination."""
598
+ page, per_page, sort_by, sort_order = __parse_pagination_params(query)
599
+
600
+ # Build MongoDB query
601
+ mongo_query = {}
602
+ for key, values in query.items():
603
+ if values:
604
+ mongo_query[key] = {"$in": values}
605
+
606
+ # Get total count
607
+ total_count = self.db.locations.count_documents(mongo_query)
608
+
609
+ # Build aggregation pipeline
610
+ pipeline = [{"$match": mongo_query}]
611
+
612
+ # Add sorting
613
+ if sort_by:
614
+ sort_direction = -1 if sort_order == "desc" else 1
615
+ pipeline.append({"$sort": {sort_by: sort_direction}})
616
+
617
+ # Add pagination
618
+ if per_page:
619
+ pipeline.extend([{"$skip": (page - 1) * per_page}, {"$limit": per_page}]) # type: ignore
620
+
621
+ # Remove MongoDB _id field
622
+ pipeline.append({"$project": {"_id": 0}})
623
+
624
+ # Execute query
625
+ cursor = self.db.locations.aggregate(pipeline)
626
+ locations = [location_model.model_validate(loc) for loc in cursor]
627
+
628
+ # Convert items to dict if needed (for location models)
629
+ if locations and hasattr(locations[0], "model_dump"):
630
+ serialized_locations = [x.model_dump() for x in locations]
631
+ else:
632
+ serialized_locations = locations
633
+
634
+ return __build_pagination_response(serialized_locations, total_count, page, per_page)
635
+
636
+
637
+ def get_locations_paginated(db, location_model):
638
+ return partial(
639
+ globals()[f"{db.module_name}_get_locations_paginated"], location_model=location_model
640
+ )
641
+
642
+
120
643
  # ------------------------------------------------
121
644
  # add_location
122
645
 
@@ -154,6 +677,14 @@ def json_db_add_location(self, location_data, location_model):
154
677
  self.data["data"].append(location.model_dump())
155
678
 
156
679
 
680
+ def mongodb_db_add_location(self, location_data, location_model):
681
+ location = location_model.model_validate(location_data)
682
+ existing = self.db.locations.find_one({"uuid": location_data["uuid"]})
683
+ if existing:
684
+ raise ValueError(f"Location with uuid {location_data['uuid']} already exists")
685
+ self.db.locations.insert_one(location.model_dump())
686
+
687
+
157
688
  def add_location(db, location_data, location_model):
158
689
  return globals()[f"{db.module_name}_add_location"](db, location_data, location_model)
159
690
 
@@ -188,6 +719,13 @@ def json_db_update_location(self, uuid, location_data, location_model):
188
719
  self.data["data"][idx] = location.model_dump()
189
720
 
190
721
 
722
+ def mongodb_db_update_location(self, uuid, location_data, location_model):
723
+ location = location_model.model_validate(location_data)
724
+ result = self.db.locations.update_one({"uuid": uuid}, {"$set": location.model_dump()})
725
+ if result.matched_count == 0:
726
+ raise ValueError(f"Location with uuid {uuid} not found")
727
+
728
+
191
729
  def update_location(db, uuid, location_data, location_model):
192
730
  return globals()[f"{db.module_name}_update_location"](db, uuid, location_data, location_model)
193
731
 
@@ -220,6 +758,12 @@ def json_db_delete_location(self, uuid):
220
758
  del self.data["data"][idx]
221
759
 
222
760
 
761
+ def mongodb_db_delete_location(self, uuid):
762
+ result = self.db.locations.delete_one({"uuid": uuid})
763
+ if result.deleted_count == 0:
764
+ raise ValueError(f"Location with uuid {uuid} not found")
765
+
766
+
223
767
  def delete_location(db, uuid):
224
768
  return globals()[f"{db.module_name}_delete_location"](db, uuid)
225
769
 
@@ -229,29 +773,17 @@ def delete_location(db, uuid):
229
773
 
230
774
 
231
775
  def json_db_add_suggestion(self, suggestion_data):
232
- suggestions = self.data.setdefault("suggestions", [])
233
- if any(s.get("uuid") == suggestion_data.get("uuid") for s in suggestions):
234
- raise ValueError(f"Suggestion with uuid {suggestion_data['uuid']} already exists")
235
-
236
- record = dict(suggestion_data)
237
- record["status"] = "pending"
238
- suggestions.append(record)
776
+ CRUDHelper.add_item_to_json_db(self.data, "suggestions", suggestion_data, "pending")
239
777
 
240
778
 
241
779
  def json_file_db_add_suggestion(self, suggestion_data):
242
- with open(self.data_file_path, "r") as file:
243
- json_file = json.load(file)
244
-
245
- suggestions = json_file["map"].get("suggestions", [])
246
- if any(s.get("uuid") == suggestion_data.get("uuid") for s in suggestions):
247
- raise ValueError(f"Suggestion with uuid {suggestion_data['uuid']} already exists")
780
+ CRUDHelper.add_item_to_json_file_db(
781
+ self.data_file_path, "suggestions", suggestion_data, "pending"
782
+ )
248
783
 
249
- record = dict(suggestion_data)
250
- record["status"] = "pending"
251
- suggestions.append(record)
252
- json_file["map"]["suggestions"] = suggestions
253
784
 
254
- json_file_atomic_dump(json_file, self.data_file_path)
785
+ def mongodb_db_add_suggestion(self, suggestion_data):
786
+ CRUDHelper.add_item_to_mongodb(self.db.suggestions, suggestion_data, "Suggestion", "pending")
255
787
 
256
788
 
257
789
  def add_suggestion(db, suggestion_data):
@@ -272,6 +804,12 @@ def json_db_get_suggestions(self, query_params):
272
804
  return suggestions
273
805
 
274
806
 
807
+ def json_db_get_suggestions_paginated(self, query):
808
+ """JSON suggestions with improved pagination."""
809
+ suggestions = self.data.get("suggestions", [])
810
+ return PaginationHelper.create_paginated_response(suggestions, query)
811
+
812
+
275
813
  def json_file_db_get_suggestions(self, query_params):
276
814
  with open(self.data_file_path, "r") as file:
277
815
  json_file = json.load(file)
@@ -285,10 +823,67 @@ def json_file_db_get_suggestions(self, query_params):
285
823
  return suggestions
286
824
 
287
825
 
826
+ def json_file_db_get_suggestions_paginated(self, query):
827
+ """JSON file suggestions with improved pagination."""
828
+ with open(self.data_file_path, "r") as file:
829
+ json_file = json.load(file)
830
+
831
+ suggestions = json_file["map"].get("suggestions", [])
832
+ return PaginationHelper.create_paginated_response(suggestions, query)
833
+
834
+
835
+ def mongodb_db_get_suggestions(self, query_params):
836
+ query = {}
837
+ statuses = query_params.get("status")
838
+ if statuses:
839
+ query["status"] = {"$in": statuses}
840
+
841
+ return list(self.db.suggestions.find(query, {"_id": 0}))
842
+
843
+
844
+ def mongodb_db_get_suggestions_paginated(self, query):
845
+ """MongoDB suggestions with improved pagination."""
846
+ page, per_page, sort_by, sort_order = __parse_pagination_params(query)
847
+
848
+ # Build MongoDB query
849
+ mongo_query = {}
850
+ statuses = query.get("status")
851
+ if statuses:
852
+ mongo_query["status"] = {"$in": statuses}
853
+
854
+ # Get total count
855
+ total_count = self.db.suggestions.count_documents(mongo_query)
856
+
857
+ # Build aggregation pipeline
858
+ pipeline = [{"$match": mongo_query}]
859
+
860
+ # Add sorting
861
+ if sort_by:
862
+ sort_direction = -1 if sort_order == "desc" else 1
863
+ pipeline.append({"$sort": {sort_by: sort_direction}})
864
+
865
+ # Add pagination
866
+ if per_page:
867
+ pipeline.extend([{"$skip": (page - 1) * per_page}, {"$limit": per_page}]) # type: ignore
868
+
869
+ # Remove MongoDB _id field
870
+ pipeline.append({"$project": {"_id": 0}})
871
+
872
+ # Execute query
873
+ cursor = self.db.suggestions.aggregate(pipeline)
874
+ items = list(cursor)
875
+
876
+ return __build_pagination_response(items, total_count, page, per_page)
877
+
878
+
288
879
  def get_suggestions(db):
289
880
  return globals()[f"{db.module_name}_get_suggestions"]
290
881
 
291
882
 
883
+ def get_suggestions_paginated(db):
884
+ return globals()[f"{db.module_name}_get_suggestions_paginated"]
885
+
886
+
292
887
  # ------------------------------------------------
293
888
  # get_suggestion
294
889
 
@@ -307,6 +902,10 @@ def json_file_db_get_suggestion(self, suggestion_id):
307
902
  )
308
903
 
309
904
 
905
+ def mongodb_db_get_suggestion(self, suggestion_id):
906
+ return self.db.suggestions.find_one({"uuid": suggestion_id}, {"_id": 0})
907
+
908
+
310
909
  def get_suggestion(db):
311
910
  return globals()[f"{db.module_name}_get_suggestion"]
312
911
 
@@ -341,6 +940,12 @@ def json_file_db_update_suggestion(self, suggestion_id, status):
341
940
  json_file_atomic_dump(json_file, self.data_file_path)
342
941
 
343
942
 
943
+ def mongodb_db_update_suggestion(self, suggestion_id, status):
944
+ result = self.db.suggestions.update_one({"uuid": suggestion_id}, {"$set": {"status": status}})
945
+ if result.matched_count == 0:
946
+ raise ValueError(f"Suggestion with uuid {suggestion_id} not found")
947
+
948
+
344
949
  def update_suggestion(db, suggestion_id, status):
345
950
  return globals()[f"{db.module_name}_update_suggestion"](db, suggestion_id, status)
346
951
 
@@ -373,6 +978,12 @@ def json_file_db_delete_suggestion(self, suggestion_id):
373
978
  json_file_atomic_dump(json_file, self.data_file_path)
374
979
 
375
980
 
981
+ def mongodb_db_delete_suggestion(self, suggestion_id):
982
+ result = self.db.suggestions.delete_one({"uuid": suggestion_id})
983
+ if result.deleted_count == 0:
984
+ raise ValueError(f"Suggestion with uuid {suggestion_id} not found")
985
+
986
+
376
987
  def delete_suggestion(db, suggestion_id):
377
988
  return globals()[f"{db.module_name}_delete_suggestion"](db, suggestion_id)
378
989
 
@@ -403,6 +1014,14 @@ def json_file_db_add_report(self, report_data):
403
1014
  json_file_atomic_dump(json_file, self.data_file_path)
404
1015
 
405
1016
 
1017
+ def mongodb_db_add_report(self, report_data):
1018
+ existing = self.db.reports.find_one({"uuid": report_data.get("uuid")})
1019
+ if existing:
1020
+ raise ValueError(f"Report with uuid {report_data['uuid']} already exists")
1021
+
1022
+ self.db.reports.insert_one(report_data)
1023
+
1024
+
406
1025
  def add_report(db, report_data):
407
1026
  return globals()[f"{db.module_name}_add_report"](db, report_data)
408
1027
 
@@ -425,6 +1044,12 @@ def json_db_get_reports(self, query_params):
425
1044
  return reports
426
1045
 
427
1046
 
1047
+ def json_db_get_reports_paginated(self, query):
1048
+ """JSON reports with improved pagination."""
1049
+ reports = self.data.get("reports", [])
1050
+ return PaginationHelper.create_paginated_response(reports, query)
1051
+
1052
+
428
1053
  def json_file_db_get_reports(self, query_params):
429
1054
  with open(self.data_file_path, "r") as file:
430
1055
  json_file = json.load(file)
@@ -442,10 +1067,75 @@ def json_file_db_get_reports(self, query_params):
442
1067
  return reports
443
1068
 
444
1069
 
1070
+ def json_file_db_get_reports_paginated(self, query):
1071
+ """JSON file reports with improved pagination."""
1072
+ data = FileIOHelper.get_data_from_file(self.data_file_path)
1073
+ reports = data.get("reports", [])
1074
+ return PaginationHelper.create_paginated_response(reports, query)
1075
+
1076
+
1077
+ def mongodb_db_get_reports(self, query_params):
1078
+ query = {}
1079
+
1080
+ statuses = query_params.get("status")
1081
+ if statuses:
1082
+ query["status"] = {"$in": statuses}
1083
+
1084
+ priorities = query_params.get("priority")
1085
+ if priorities:
1086
+ query["priority"] = {"$in": priorities}
1087
+
1088
+ return list(self.db.reports.find(query, {"_id": 0}))
1089
+
1090
+
1091
+ def mongodb_db_get_reports_paginated(self, query):
1092
+ """MongoDB reports with improved pagination."""
1093
+ page, per_page, sort_by, sort_order = __parse_pagination_params(query)
1094
+
1095
+ # Build MongoDB query
1096
+ mongo_query = {}
1097
+
1098
+ statuses = query.get("status")
1099
+ if statuses:
1100
+ mongo_query["status"] = {"$in": statuses}
1101
+
1102
+ priorities = query.get("priority")
1103
+ if priorities:
1104
+ mongo_query["priority"] = {"$in": priorities}
1105
+
1106
+ # Get total count
1107
+ total_count = self.db.reports.count_documents(mongo_query)
1108
+
1109
+ # Build aggregation pipeline
1110
+ pipeline = [{"$match": mongo_query}]
1111
+
1112
+ # Add sorting
1113
+ if sort_by:
1114
+ sort_direction = -1 if sort_order == "desc" else 1
1115
+ pipeline.append({"$sort": {sort_by: sort_direction}})
1116
+
1117
+ # Add pagination
1118
+ if per_page:
1119
+ pipeline.extend([{"$skip": (page - 1) * per_page}, {"$limit": per_page}]) # type: ignore
1120
+
1121
+ # Remove MongoDB _id field
1122
+ pipeline.append({"$project": {"_id": 0}})
1123
+
1124
+ # Execute query
1125
+ cursor = self.db.reports.aggregate(pipeline)
1126
+ items = list(cursor)
1127
+
1128
+ return __build_pagination_response(items, total_count, page, per_page)
1129
+
1130
+
445
1131
  def get_reports(db):
446
1132
  return globals()[f"{db.module_name}_get_reports"]
447
1133
 
448
1134
 
1135
+ def get_reports_paginated(db):
1136
+ return globals()[f"{db.module_name}_get_reports_paginated"]
1137
+
1138
+
449
1139
  # ------------------------------------------------
450
1140
  # get_report
451
1141
 
@@ -463,6 +1153,10 @@ def json_file_db_get_report(self, report_id):
463
1153
  )
464
1154
 
465
1155
 
1156
+ def mongodb_db_get_report(self, report_id):
1157
+ return self.db.reports.find_one({"uuid": report_id}, {"_id": 0})
1158
+
1159
+
466
1160
  def get_report(db):
467
1161
  return globals()[f"{db.module_name}_get_report"]
468
1162
 
@@ -503,6 +1197,19 @@ def json_file_db_update_report(self, report_id, status=None, priority=None):
503
1197
  json_file_atomic_dump(json_file, self.data_file_path)
504
1198
 
505
1199
 
1200
+ def mongodb_db_update_report(self, report_id, status=None, priority=None):
1201
+ update_doc = {}
1202
+ if status:
1203
+ update_doc["status"] = status
1204
+ if priority:
1205
+ update_doc["priority"] = priority
1206
+
1207
+ if update_doc:
1208
+ result = self.db.reports.update_one({"uuid": report_id}, {"$set": update_doc})
1209
+ if result.matched_count == 0:
1210
+ raise ValueError(f"Report with uuid {report_id} not found")
1211
+
1212
+
506
1213
  def update_report(db, report_id, status=None, priority=None):
507
1214
  return globals()[f"{db.module_name}_update_report"](db, report_id, status, priority)
508
1215
 
@@ -534,6 +1241,12 @@ def json_file_db_delete_report(self, report_id):
534
1241
  json_file_atomic_dump(json_file, self.data_file_path)
535
1242
 
536
1243
 
1244
+ def mongodb_db_delete_report(self, report_id):
1245
+ result = self.db.reports.delete_one({"uuid": report_id})
1246
+ if result.deleted_count == 0:
1247
+ raise ValueError(f"Report with uuid {report_id} not found")
1248
+
1249
+
537
1250
  def delete_report(db, report_id):
538
1251
  return globals()[f"{db.module_name}_delete_report"](db, report_id)
539
1252
 
@@ -546,18 +1259,23 @@ def delete_report(db, report_id):
546
1259
  def extend_db_with_goodmap_queries(db, location_model):
547
1260
  db.extend("get_data", get_data(db))
548
1261
  db.extend("get_locations", get_locations(db, location_model))
1262
+ db.extend("get_locations_paginated", get_locations_paginated(db, location_model))
549
1263
  db.extend("get_location", get_location(db, location_model))
550
1264
  db.extend("add_location", partial(add_location, location_model=location_model))
551
1265
  db.extend("update_location", partial(update_location, location_model=location_model))
552
1266
  db.extend("delete_location", delete_location)
553
- if db.module_name in ("json_db", "json_file_db"):
1267
+ db.extend("get_categories", get_categories(db))
1268
+ db.extend("get_category_data", get_category_data(db))
1269
+ if db.module_name in ("json_db", "json_file_db", "mongodb_db"):
554
1270
  db.extend("add_suggestion", add_suggestion)
555
1271
  db.extend("get_suggestions", get_suggestions(db))
1272
+ db.extend("get_suggestions_paginated", get_suggestions_paginated(db))
556
1273
  db.extend("get_suggestion", get_suggestion(db))
557
1274
  db.extend("update_suggestion", update_suggestion)
558
1275
  db.extend("delete_suggestion", delete_suggestion)
559
1276
  db.extend("add_report", add_report)
560
1277
  db.extend("get_reports", get_reports(db))
1278
+ db.extend("get_reports_paginated", get_reports_paginated(db))
561
1279
  db.extend("get_report", get_report(db))
562
1280
  db.extend("update_report", update_report)
563
1281
  db.extend("delete_report", delete_report)
@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: goodmap
3
- Version: 0.5.3
3
+ Version: 1.0.2
4
4
  Summary: Map engine to serve all the people :)
5
+ License-File: LICENSE.md
5
6
  Author: Krzysztof Kolodzinski
6
7
  Author-email: krzysztof.kolodzinski@problematy.pl
7
8
  Requires-Python: >=3.10,<4.0
@@ -10,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.10
10
11
  Classifier: Programming Language :: Python :: 3.11
11
12
  Classifier: Programming Language :: Python :: 3.12
12
13
  Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
13
15
  Requires-Dist: Babel (>=2.10.3,<3.0.0)
14
16
  Requires-Dist: Flask (==3.0.3)
15
17
  Requires-Dist: Flask-Babel (>=4.0.0,<5.0.0)
@@ -22,11 +24,11 @@ Requires-Dist: google-cloud-storage (>=2.7.0,<3.0.0)
22
24
  Requires-Dist: gql (>=3.4.0,<4.0.0)
23
25
  Requires-Dist: gunicorn (>=20.1.0,<21.0.0)
24
26
  Requires-Dist: humanize (>=4.6.0,<5.0.0)
25
- Requires-Dist: platzky (>=0.3.6,<0.4.0)
27
+ Requires-Dist: platzky (>=0.4.0,<0.5.0)
26
28
  Requires-Dist: pydantic (>=2.7.1,<3.0.0)
27
29
  Description-Content-Type: text/markdown
28
30
 
29
- ![Github Actions](https://github.com/problematy/goodmap/actions/workflows/tests.yml/badge.svg?event=push&branch=main)
31
+ ![Github Actions](https://github.com/problematy/goodmap/actions/workflows/release.yml/badge.svg?event=push&branch=main)
30
32
  [![Coverage Status](https://coveralls.io/repos/github/Problematy/goodmap/badge.png)](https://coveralls.io/github/Problematy/goodmap)
31
33
 
32
34
  # Good Map
@@ -1,14 +1,14 @@
1
1
  goodmap/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
2
  goodmap/core.py,sha256=rzMhOIYnR1jxTX6uHQJKIPLYxdUm4_v2d6LrtHtJpHU,1465
3
- goodmap/core_api.py,sha256=8Mhs59c_MnORmHK7aFCNM7eBJBhNQ0MWcdYnmiW2GW4,15515
3
+ goodmap/core_api.py,sha256=MxJ7SGOFBBnV_cBIiZoImHLaY8_FtEW-dl3od5-mIt8,13529
4
4
  goodmap/data_models/location.py,sha256=H3EKozc-WZvrYm6cwajl8_gaw4rQhxdlvxR1mk4mpkA,1104
5
5
  goodmap/data_validator.py,sha256=lBmVAPxvSmEOdUGeVYSjUvVVmKfPyq4CWoHfczTtEMM,4090
6
- goodmap/db.py,sha256=fB9B0nzNuTsMBak7iV4bGxAxGF5NSTGikht9keDOTGA,17518
6
+ goodmap/db.py,sha256=i-fe_f_s9hQxvTqq4alA-RK9j6vQpnaNpXEDPci3s1Y,42049
7
7
  goodmap/formatter.py,sha256=VlUHcK1HtM_IEU0VE3S5TOkZLVheMdakvUeW2tCKdq0,783
8
8
  goodmap/goodmap.py,sha256=OOcuDEY7GZsHh8vjIJ9brd4SuGxNRq_lC_DMmNuome8,2731
9
9
  goodmap/templates/goodmap-admin.html,sha256=zGuau239BXyBerV21mDXcHy34ke8cLmZcMngcX1xhAs,35598
10
10
  goodmap/templates/map.html,sha256=-qbJ313t2EdSyIwULnQy3j81pH4MqI5yjtJ4Xoo7N7M,3979
11
- goodmap-0.5.3.dist-info/LICENSE.md,sha256=nkCQOR7uheLRvHRfXmwx9LhBnMcPeBU9d4ebLojDiQU,1067
12
- goodmap-0.5.3.dist-info/METADATA,sha256=d5Wqic-brdjC_wocR63g1YINlERblKpmkZpTOZ-j5pg,5278
13
- goodmap-0.5.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
14
- goodmap-0.5.3.dist-info/RECORD,,
11
+ goodmap-1.0.2.dist-info/METADATA,sha256=nqqszznBX2xjMT-QiQwWNqUlyqhtjlI4Dsz5yUZtjlg,5356
12
+ goodmap-1.0.2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
13
+ goodmap-1.0.2.dist-info/licenses/LICENSE.md,sha256=nkCQOR7uheLRvHRfXmwx9LhBnMcPeBU9d4ebLojDiQU,1067
14
+ goodmap-1.0.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any