goodmap 1.3.0__tar.gz → 1.4.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: goodmap
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: Map engine to serve all the people :)
5
5
  Author: Krzysztof Kolodzinski
6
6
  Author-email: krzysztof.kolodzinski@problematy.pl
@@ -24,7 +24,7 @@ Requires-Dist: gunicorn (>=20.1,<24.0)
24
24
  Requires-Dist: humanize (>=4.6.0,<5.0.0)
25
25
  Requires-Dist: myst-parser (>=4.0.0,<5.0.0) ; extra == "docs"
26
26
  Requires-Dist: numpy (>=2.2.0,<3.0.0)
27
- Requires-Dist: platzky (>=1.0.0,<2.0.0)
27
+ Requires-Dist: platzky (>=1.3.0,<2.0.0)
28
28
  Requires-Dist: pydantic (>=2.7.1,<3.0.0)
29
29
  Requires-Dist: pysupercluster-problematy (>=0.7.8,<0.8.0)
30
30
  Requires-Dist: scipy (>=1.15.1,<2.0.0)
@@ -34,7 +34,9 @@ def _clean_model_name(model: Type[Any]) -> str:
34
34
  def _handle_location_validation_error(e: LocationValidationError):
35
35
  """Handle LocationValidationError and return appropriate response."""
36
36
  logger.warning(
37
- "Location validation failed",
37
+ "Location validation failed - uuid: %s, errors: %s",
38
+ e.uuid,
39
+ e.validation_errors,
38
40
  extra={"uuid": e.uuid, "errors": e.validation_errors},
39
41
  )
40
42
  return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
@@ -126,7 +128,9 @@ def _update_suggestion_handler(database, suggestion_id):
126
128
  database.update_suggestion(suggestion_id, status)
127
129
  except LocationValidationError as e:
128
130
  logger.warning(
129
- "Location validation failed in suggestion",
131
+ "Location validation failed in suggestion - uuid: %s, errors: %s",
132
+ e.uuid,
133
+ e.validation_errors,
130
134
  extra={"uuid": e.uuid, "errors": e.validation_errors},
131
135
  )
132
136
  return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
@@ -7,7 +7,8 @@ import numpy
7
7
  import pysupercluster
8
8
  from flask import Blueprint, jsonify, make_response, request
9
9
  from flask_babel import gettext
10
- from platzky.config import LanguagesMapping
10
+ from platzky.attachment import AttachmentProtocol
11
+ from platzky.config import AttachmentConfig, LanguagesMapping
11
12
  from spectree import Response, SpecTree
12
13
 
13
14
  from goodmap.api_models import (
@@ -80,15 +81,21 @@ def core_pages(
80
81
  notifier_function,
81
82
  csrf_generator,
82
83
  location_model,
84
+ photo_attachment_class: type[AttachmentProtocol],
85
+ photo_attachment_config: AttachmentConfig,
83
86
  feature_flags={},
84
87
  ) -> Blueprint:
85
88
  core_api_blueprint = Blueprint("api", __name__, url_prefix="/api")
86
89
 
87
- # Initialize Spectree for API documentation and validation
88
- # Use simple naming strategy without hashes for cleaner schema names
89
- from typing import Any, Type
90
+ # Build photo error message from config
91
+ allowed_ext = ", ".join(sorted(photo_attachment_config.allowed_extensions or []))
92
+ max_size_mb = photo_attachment_config.max_size / (1024 * 1024)
93
+ error_invalid_photo = (
94
+ f"Invalid photo. Allowed formats: {allowed_ext}. Max size: {max_size_mb:.0f}MB."
95
+ )
90
96
 
91
- def _clean_model_name(model: Type[Any]) -> str:
97
+ # Initialize Spectree for API documentation and validation
98
+ def _clean_model_name(model: type) -> str:
92
99
  return model.__name__
93
100
 
94
101
  spec = SpecTree(
@@ -111,6 +118,9 @@ def core_pages(
111
118
  import json as json_lib
112
119
 
113
120
  try:
121
+ # Initialize photo attachment (only populated for multipart/form-data)
122
+ photo_attachment = None
123
+
114
124
  # Handle both multipart/form-data (with file uploads) and JSON
115
125
  if request.content_type and request.content_type.startswith("multipart/form-data"):
116
126
  # Parse form data dynamically
@@ -146,8 +156,24 @@ def core_pages(
146
156
  # If not JSON, use as-is (simple string values)
147
157
  suggested_location[key] = value
148
158
 
149
- # TODO: Handle photo file upload from request.files['photo']
150
- # For now, we just ignore it as the backend doesn't store photos yet
159
+ # Extract and validate photo attachment if present
160
+ photo_file = request.files.get("photo")
161
+ if photo_file and photo_file.filename:
162
+ photo_content = photo_file.read()
163
+ photo_mime = photo_file.content_type or "application/octet-stream"
164
+
165
+ # Validate using configured Attachment class
166
+ try:
167
+ photo_attachment = photo_attachment_class(
168
+ photo_file.filename, photo_content, photo_mime
169
+ )
170
+ except ValueError as e:
171
+ logger.warning(
172
+ "Rejected photo: %s",
173
+ e,
174
+ extra={"photo_filename": photo_file.filename},
175
+ )
176
+ return make_response(jsonify({"message": error_invalid_photo}), 400)
151
177
  else:
152
178
  # Parse JSON data with security checks (depth/size protection)
153
179
  raw_data = request.get_data(as_text=True)
@@ -186,10 +212,18 @@ def core_pages(
186
212
  database.add_suggestion(location.model_dump())
187
213
  message = gettext("A new location has been suggested with details")
188
214
  notifier_message = f"{message}: {json_lib.dumps(suggested_location, indent=2)}"
189
- notifier_function(notifier_message)
215
+ attachments = [photo_attachment] if photo_attachment else None
216
+ notifier_function(notifier_message, attachments=attachments)
190
217
  except LocationValidationError as e:
218
+ # NOTE: validation_errors includes input values from the location model fields:
219
+ # - Core fields: position (lat/long), uuid, remark
220
+ # - Dynamic fields: categories and obligatory_fields configured per deployment
221
+ # These are geographic/categorical data, NOT PII (no email, phone, names of people).
222
+ # Safe to log for debugging. If PII fields are ever added to the location model,
223
+ # strip 'input' from validation_errors before logging.
191
224
  logger.warning(
192
- "Location validation failed in suggest endpoint",
225
+ "Location validation failed in suggest endpoint: %s",
226
+ e.validation_errors,
193
227
  extra={"errors": e.validation_errors},
194
228
  )
195
229
  return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
@@ -371,11 +405,37 @@ def core_pages(
371
405
  """
372
406
  categories_data = database.get_category_data()
373
407
  result = []
408
+
409
+ categories_options_help = categories_data.get("categories_options_help", {})
410
+
374
411
  for key, options in categories_data["categories"].items():
375
- result.append(
376
- {"key": key, "name": gettext(key), "options": make_tuple_translation(options)}
377
- )
378
- return jsonify({"categories": result})
412
+ category_entry = {
413
+ "key": key,
414
+ "name": gettext(key),
415
+ "options": make_tuple_translation(options),
416
+ }
417
+
418
+ if feature_flags.get("CATEGORIES_HELP", False):
419
+ option_help_list = categories_options_help.get(key, [])
420
+ proper_options_help = []
421
+ for option in option_help_list:
422
+ proper_options_help.append(
423
+ {option: gettext(f"categories_options_help_{option}")}
424
+ )
425
+ category_entry["options_help"] = proper_options_help
426
+
427
+ result.append(category_entry)
428
+
429
+ response = {"categories": result}
430
+
431
+ if feature_flags.get("CATEGORIES_HELP", False):
432
+ categories_help = categories_data.get("categories_help", [])
433
+ proper_categories_help = []
434
+ for option in categories_help:
435
+ proper_categories_help.append({option: gettext(f"categories_help_{option}")})
436
+ response["categories_help"] = proper_categories_help
437
+
438
+ return jsonify(response)
379
439
 
380
440
  @core_api_blueprint.route("/languages", methods=["GET"])
381
441
  @spec.validate()
@@ -1,11 +1,13 @@
1
1
  """Goodmap engine with location management and admin interface."""
2
2
 
3
+ import logging
3
4
  import os
4
5
 
5
6
  from flask import Blueprint, redirect, render_template, session
6
7
  from flask_wtf.csrf import CSRFProtect, generate_csrf
7
8
  from platzky import platzky
8
- from platzky.config import languages_dict
9
+ from platzky.attachment import create_attachment_class
10
+ from platzky.config import AttachmentConfig, languages_dict
9
11
  from platzky.models import CmsModule
10
12
 
11
13
  from goodmap.admin_api import admin_pages
@@ -17,6 +19,8 @@ from goodmap.db import (
17
19
  get_location_obligatory_fields,
18
20
  )
19
21
 
22
+ logger = logging.getLogger(__name__)
23
+
20
24
 
21
25
  def create_app(config_path: str) -> platzky.Engine:
22
26
  """Create Goodmap application from YAML configuration file.
@@ -97,12 +101,24 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
97
101
 
98
102
  CSRFProtect(app)
99
103
 
104
+ # Create Attachment class for photo uploads
105
+ # JPEG-only: universal browser/device support, good compression for location photos,
106
+ # no transparency needed. PNG/WebP can be added if user demand warrants it.
107
+ photo_attachment_config = AttachmentConfig(
108
+ allowed_mime_types=frozenset({"image/jpeg"}),
109
+ allowed_extensions=frozenset({"jpg", "jpeg"}),
110
+ max_size=5 * 1024 * 1024, # 5MB - reasonable for location photos
111
+ )
112
+ PhotoAttachment = create_attachment_class(photo_attachment_config)
113
+
100
114
  cp = core_pages(
101
115
  app.db,
102
116
  languages_dict(config.languages),
103
117
  app.notify,
104
118
  generate_csrf,
105
119
  location_model,
120
+ photo_attachment_class=PhotoAttachment,
121
+ photo_attachment_config=photo_attachment_config,
106
122
  feature_flags=config.feature_flags,
107
123
  )
108
124
  app.register_blueprint(cp)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "goodmap"
3
- version = "1.3.0"
3
+ version = "1.4.0"
4
4
  description = "Map engine to serve all the people :)"
5
5
  authors = ["Krzysztof Kolodzinski <krzysztof.kolodzinski@problematy.pl>"]
6
6
  readme = "README.md"
@@ -20,7 +20,7 @@ Flask-WTF = "^1.2.1"
20
20
  gql = "^3.4.0"
21
21
  aiohttp = "^3.8.4"
22
22
  pydantic = "^2.7.1"
23
- platzky = "^1.0.0"
23
+ platzky = "^1.3.0"
24
24
  deprecation = "^2.1.0"
25
25
  numpy = "^2.2.0"
26
26
  # Using fork because official PyPI version (0.7.7) has outdated numpy setup hack
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes