Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions goodmap/admin_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ def _clean_model_name(model: Type[Any]) -> str:
def _handle_location_validation_error(e: LocationValidationError):
"""Handle LocationValidationError and return appropriate response."""
logger.warning(
"Location validation failed",
"Location validation failed - uuid: %s, errors: %s",
e.uuid,
e.validation_errors,
extra={"uuid": e.uuid, "errors": e.validation_errors},
)
return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
Expand Down Expand Up @@ -126,7 +128,9 @@ def _update_suggestion_handler(database, suggestion_id):
database.update_suggestion(suggestion_id, status)
except LocationValidationError as e:
logger.warning(
"Location validation failed in suggestion",
"Location validation failed in suggestion - uuid: %s, errors: %s",
e.uuid,
e.validation_errors,
extra={"uuid": e.uuid, "errors": e.validation_errors},
)
return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
Expand Down
29 changes: 25 additions & 4 deletions goodmap/core_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pysupercluster
from flask import Blueprint, jsonify, make_response, request
from flask_babel import gettext
from platzky.attachment import AttachmentProtocol
from platzky.config import LanguagesMapping
from spectree import Response, SpecTree

Expand Down Expand Up @@ -80,6 +81,7 @@ def core_pages(
notifier_function,
csrf_generator,
location_model,
photo_attachment_class: type[AttachmentProtocol],
feature_flags={},
) -> Blueprint:
core_api_blueprint = Blueprint("api", __name__, url_prefix="/api")
Expand Down Expand Up @@ -111,6 +113,9 @@ def suggest_new_point():
import json as json_lib

try:
# Initialize photo attachment (only populated for multipart/form-data)
photo_attachment = None

# Handle both multipart/form-data (with file uploads) and JSON
if request.content_type and request.content_type.startswith("multipart/form-data"):
# Parse form data dynamically
Expand Down Expand Up @@ -146,8 +151,22 @@ def suggest_new_point():
# If not JSON, use as-is (simple string values)
suggested_location[key] = value

# TODO: Handle photo file upload from request.files['photo']
# For now, we just ignore it as the backend doesn't store photos yet
# Extract and validate photo attachment if present
photo_file = request.files.get("photo")
if photo_file and photo_file.filename:
photo_content = photo_file.read()
photo_mime = photo_file.content_type or "application/octet-stream"

# Validate using JPEG-only Attachment class
try:
photo_attachment = photo_attachment_class(
photo_file.filename, photo_content, photo_mime
)
except ValueError as e:
logger.warning("Rejected photo: %s", e)
return make_response(
jsonify({"message": "Only JPEG images are allowed"}), 400
)
else:
# Parse JSON data with security checks (depth/size protection)
raw_data = request.get_data(as_text=True)
Expand Down Expand Up @@ -186,10 +205,12 @@ def suggest_new_point():
database.add_suggestion(location.model_dump())
message = gettext("A new location has been suggested with details")
notifier_message = f"{message}: {json_lib.dumps(suggested_location, indent=2)}"
notifier_function(notifier_message)
attachments = [photo_attachment] if photo_attachment else None
notifier_function(notifier_message, attachments=attachments)
Comment thread
raven-wing marked this conversation as resolved.
except LocationValidationError as e:
logger.warning(
"Location validation failed in suggest endpoint",
"Location validation failed in suggest endpoint: %s",
e.validation_errors,
extra={"errors": e.validation_errors},
)
return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
Comment thread
raven-wing marked this conversation as resolved.
Expand Down
50 changes: 48 additions & 2 deletions goodmap/goodmap.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""Goodmap engine with location management and admin interface."""

import logging
import os

from flask import Blueprint, redirect, render_template, session
from flask_wtf.csrf import CSRFProtect, generate_csrf
from platzky import platzky
from platzky.config import languages_dict
from platzky.attachment import AttachmentProtocol, create_attachment_class
from platzky.config import AttachmentConfig, languages_dict
from platzky.models import CmsModule

from goodmap.admin_api import admin_pages
Expand All @@ -17,6 +19,8 @@
get_location_obligatory_fields,
)

logger = logging.getLogger(__name__)


def create_app(config_path: str) -> platzky.Engine:
"""Create Goodmap application from YAML configuration file.
Expand Down Expand Up @@ -57,6 +61,17 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
Returns:
platzky.Engine: Fully configured Flask application with Goodmap features
"""
# Configure debug logging (FLASK_DEBUG is set by `flask --debug` before factory runs)
# Must be done before platzky.create_app_from_config to capture plugin init logs
if os.environ.get("FLASK_DEBUG") == "1":
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
handler.setFormatter(logging.Formatter("%(name)s - %(levelname)s - %(message)s"))
for logger_name in ("goodmap", "platzky"):
logging.getLogger(logger_name).setLevel(logging.DEBUG)
logging.getLogger(logger_name).addHandler(handler)
logger.info("Debug logging enabled")

directory = os.path.dirname(os.path.realpath(__file__))

locale_dir = os.path.join(directory, "locale")
Expand Down Expand Up @@ -97,12 +112,43 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:

CSRFProtect(app)

# Create JPEG-only Attachment class for photo uploads
photo_attachment_config = AttachmentConfig(
allowed_mime_types=frozenset({"image/jpeg"}),
allowed_extensions=frozenset({"jpg", "jpeg"}),
)
PhotoAttachment = create_attachment_class(photo_attachment_config)

# Wrap notifier with detailed logging for debugging
original_notify = app.notify

def debug_notify(message: str, attachments: list[AttachmentProtocol] | None = None):
logger.debug("Notifier called with message: %s", message)
if attachments:
logger.debug(
"Attachments: %s",
[(a.filename, len(a.content), a.mime_type) for a in attachments],
)
try:
result = original_notify(message, attachments=attachments)
logger.debug("Notifier succeeded, result type: %s", type(result).__name__)
return result
except Exception as e:
logger.error(
"Notifier failed: %s: %s",
type(e).__name__,
str(e),
exc_info=True,
)
raise

cp = core_pages(
app.db,
languages_dict(config.languages),
app.notify,
debug_notify,
generate_csrf,
location_model,
photo_attachment_class=PhotoAttachment,
feature_flags=config.feature_flags,
)
app.register_blueprint(cp)
Expand Down
Loading
Loading