Skip to content

feat: implement photo uploads with file validation - #324

Merged
raven-wing merged 11 commits into
Problematy:mainfrom
raven-wing:image
Jan 24, 2026
Merged

raven-wing merged 11 commits into
Problematy:mainfrom
raven-wing:image

Conversation

@raven-wing

@raven-wing raven-wing commented Jan 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • JPEG photo attachments can be uploaded with new location suggestions (multipart/form-data); valid photos are included in notifications.
  • Bug Fixes

    • Richer logging for location validation failures — logs include item identifiers and detailed validation errors; invalid location or photo data return clearer 400 responses.
  • Chores

    • Dependency bumped to support attachments; CI workflow reference updated.
  • Tests

    • Added comprehensive tests covering photo upload validation (format, extension, size, malformed data).

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 23, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds JPEG-only photo attachment support to the suggest endpoint (multipart parsing, validation, and notifier forwarding), enriches LocationValidationError logs with suggestion UUIDs and validation errors, bumps the platzky dependency, and updates CI workflow reference.

Changes

Cohort / File(s) Summary
Admin API Logging Enhancement
goodmap/admin_api.py
LocationValidationError log messages now include the suggestion UUID (e.uuid) and e.validation_errors in the formatted message and extra context; HTTP responses unchanged.
Core API Photo Attachment
goodmap/core_api.py
core_pages signature adds photo_attachment_class: type[AttachmentProtocol] and photo_attachment_config: AttachmentConfig. suggest_new_point handles multipart/form-data: reads request.files["photo"], infers MIME, constructs/validates attachment via injected class, returns 400 on attachment validation errors, and forwards validated attachment(s) to notifier_function. LocationValidationError logging enriched.
App Setup & PhotoAttachment
goodmap/goodmap.py
Initializes module logger, builds photo_attachment_config (JPEG-only, 5MB limit) and PhotoAttachment via create_attachment_class, and passes photo_attachment_class/photo_attachment_config into core_pages.
Dependencies / CI
pyproject.toml, .github/workflows/tests.yml
Bumps platzky from ^1.0.0 to ^1.3.0. CI e2e-tests workflow reference updated to a newer commit hash (no step changes).
Tests
tests/unit_tests/test_core_api.py
Adds extensive tests for multipart photo uploads: valid JPEG, disallowed PNG/gif, oversized files, fake JPEG content, and storage behavior when photos provided.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant CoreAPI as Core API
    participant Validator as PhotoAttachmentClass
    participant Notifier

    Client->>CoreAPI: POST /suggest (multipart/form-data with photo)
    CoreAPI->>CoreAPI: parse multipart, read file bytes, infer MIME
    CoreAPI->>Validator: create/validate attachment (filename, bytes, mime)
    alt valid
        Validator-->>CoreAPI: PhotoAttachment
        CoreAPI->>Notifier: notifier_function(..., attachments=[PhotoAttachment])
        Notifier-->>CoreAPI: ack
        CoreAPI-->>Client: 200/201 Created
    else invalid
        Validator-->>CoreAPI: raises ValueError / returns error
        CoreAPI-->>Client: 400 Bad Request (photo validation error)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I nibble bytes beneath moonlit code,

A JPEG hop validated on the road,
Logs carry UUIDs in a tidy row,
Attachments bound, the notifier goes,
I twitch my whiskers — onward we go.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title 'feat: implement photo uploads with file validation' accurately summarizes the main changes across the PR, covering the introduction of photo upload functionality with validation constraints.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
goodmap/goodmap.py (1)

81-85: MAX_CONTENT_LENGTH may be too restrictive for photo uploads.

The 100KB limit will reject most photo uploads before they reach the application. Typical smartphone photos are 2-5MB. Consider increasing this limit to accommodate the new photo upload feature, or document that photos must be pre-compressed client-side.

🤖 Fix all issues with AI agents
In `@goodmap/core_api.py`:
- Around line 56-105: Run black on the file to fix formatting, then in
compress_photo make filename handling robust and adjust try/except flow: replace
the naive filename.rsplit logic with a safe split (e.g., use os.path.splitext or
check for '.' to produce base + ".jpg") so extensionless names become
"photo.jpg", and refactor the try/except so the function assigns
compressed_content, new_mime_type, new_filename inside the try and only performs
a single return at the end (use the except to set them back to original values
and log the warning) instead of returning from inside the except; this will
satisfy static analysis (TRY300) and keep logging intact.
- Around line 48-51: The compress_photo function is never invoked and the config
values conflict (PHOTO_COMPRESSION_THRESHOLD = 2MB vs MAX_CONTENT_LENGTH =
100KB), causing Flask to reject large uploads before compression; fix by calling
compress_photo in the upload flow before creating the attachment (locate the
upload handler that calls photo_attachment_class and invoke
compress_photo(file_bytes or file_stream) and use its returned bytes when
constructing photo_attachment_class), and reconcile the config by either
lowering PHOTO_COMPRESSION_THRESHOLD to be <= MAX_CONTENT_LENGTH or increasing
MAX_CONTENT_LENGTH so compression can run (adjust the constants
PHOTO_COMPRESSION_THRESHOLD and/or MAX_CONTENT_LENGTH accordingly).
🧹 Nitpick comments (3)
goodmap/core_api.py (1)

213-228: JPEG-only validation is good, but consider applying compression here.

The photo validation flow correctly uses photo_attachment_class to enforce JPEG-only uploads. However, since compress_photo is defined but unused, consider whether compression should be applied to the uploaded photo content before creating the attachment (if the intent was to compress large uploads).

If compression is not needed, consider removing the unused compress_photo function and related constants to reduce dead code.

goodmap/goodmap.py (2)

64-73: Variable shadowing and handler accumulation concerns.

  1. The loop variable logger_name is fine, but note that logging.getLogger(logger_name) in the loop creates/retrieves different loggers - this works correctly.

  2. Handler accumulation: If create_app_from_config is called multiple times (e.g., in tests), handlers will accumulate on the loggers. Consider checking if handlers already exist:

Suggested fix to prevent handler accumulation
     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)
+            log = logging.getLogger(logger_name)
+            log.setLevel(logging.DEBUG)
+            if not log.handlers:
+                log.addHandler(handler)
         logger.info("Debug logging enabled")

125-143: Debug wrapper is functional but could address TRY300.

The wrapper correctly logs notification attempts and errors. The static analysis hint (TRY300) suggests moving return result to an else block for cleaner exception flow, but this is optional given the re-raise pattern.

Optional: Address TRY300
     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
+        else:
+            return result

Comment thread goodmap/core_api.py Outdated
Comment thread goodmap/core_api.py Outdated
@github-actions

Copy link
Copy Markdown

🧪 E2E Test Results

📊 View full workflow run
🔗 Commit: 053b96f

⚠️ E2E Stress Test Results

Performance data not found. See workflow logs for details.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@goodmap/core_api.py`:
- Around line 210-216: The warning currently logs potentially sensitive values
by interpolating e.validation_errors into the message in the suggest endpoint;
change the logger.warning call in the except LocationValidationError block to
avoid including raw validation values in the message string—use either str(e)
(which is implemented to hide details) or a sanitized summary (e.g., list of
field names or a count) for the human-readable message, and keep the full
e.validation_errors only in the extra={} payload for structured logging; ensure
the return behavior (make_response(jsonify({"message":
ERROR_INVALID_LOCATION_DATA}), 400)) remains unchanged.

Comment thread goodmap/core_api.py
@github-actions

Copy link
Copy Markdown

🧪 E2E Test Results

📊 View full workflow run
🔗 Commit: 21f8f44

⚠️ E2E Stress Test Results

Performance data not found. See workflow logs for details.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@goodmap/core_api.py`:
- Around line 167-169: The return statement inside the exception handler (the
call to make_response/jsonify with str(e)) is incorrectly formatted for Black;
run the auto-formatter (black goodmap/core_api.py) or reformat the call to
follow Black's style so the call to make_response(jsonify({"message": str(e)}),
400) is formatted correctly (ensure parentheses and commas follow Black's
line-wrapping rules) so the Black CI check passes.
- Around line 208-209: The call notifier_function(notifier_message,
attachments=attachments) passes an unsupported kwarg and will raise a TypeError;
update the call in goodmap/core_api.py to call notifier_function only with the
message (notifier_function(notifier_message)) or, if you must support custom
notifiers that accept attachments, detect support first (e.g.,
inspect.signature/notifier_function.__code__.co_varnames or hasattr for a custom
wrapper) and only pass attachments when the notifier declares an attachments
parameter; ensure references include notifier_function, notifier_message and the
attachments variable so you modify the correct call site.
🧹 Nitpick comments (1)
goodmap/goodmap.py (1)

22-23: Unused logger initialization.

The logger is initialized but never used in this file. Consider removing it, or if logging is planned for future use, add a TODO comment.

♻️ Suggested removal
-logger = logging.getLogger(__name__)
-

Also remove the import if not needed elsewhere:

-import logging

Comment thread goodmap/core_api.py Outdated
Comment thread goodmap/core_api.py
@github-actions

Copy link
Copy Markdown

🧪 E2E Test Results

📊 View full workflow run
🔗 Commit: 091b584

⚠️ E2E Stress Test Results

Performance data not found. See workflow logs for details.

@github-actions

Copy link
Copy Markdown

🧪 E2E Test Results

📊 View full workflow run
🔗 Commit: c57a7ae

⚠️ E2E Stress Test Results

Performance data not found. See workflow logs for details.

@github-actions

Copy link
Copy Markdown

🧪 E2E Test Results

📊 View full workflow run
🔗 Commit: 3e21f0e

📊 E2E Stress Test Performance

✅ Status: PASSED (13685.54ms max < 25000ms limit)

Metric Value
Average Time 12492.44ms
Minimum Time 11890.67ms
Maximum Time 13685.54ms
Completed Runs 5/5
Avg Markers Loaded 74
📈 Individual Run Times
Run Time (ms) Markers
Run 1 11890.67ms 74
Run 2 12173.26ms 74
Run 3 12012.91ms 74
Run 4 13685.54ms 74
Run 5 12699.8ms 74

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
goodmap/goodmap.py (1)

70-74: Configuration mismatch: MAX_CONTENT_LENGTH vs max_size.

MAX_CONTENT_LENGTH is set to 100KB (line 74), but photo_attachment_config.max_size is 5MB (line 110). Flask will reject any request exceeding 100KB before photo validation logic executes, making the 5MB limit unreachable.

Either increase MAX_CONTENT_LENGTH to accommodate 5MB photos plus overhead, or reduce max_size to fit within the 100KB limit.

Proposed fix
     # SECURITY: Set maximum request body size to 100KB (prevents memory exhaustion)
     # This protects against large file uploads and JSON payloads
-    # Based on calculation: ~6.5KB max legitimate payload + multipart overhead
+    # Based on calculation: 5MB photo + multipart overhead + form fields
     if "MAX_CONTENT_LENGTH" not in app.config:
-        app.config["MAX_CONTENT_LENGTH"] = 100 * 1024  # 100KB
+        app.config["MAX_CONTENT_LENGTH"] = 6 * 1024 * 1024  # 6MB (5MB photo + overhead)

Also applies to: 107-111

tests/unit_tests/test_core_api.py (1)

1-14: Fix Black formatting to pass CI.

The pipeline indicates Black formatting check failed on this file. Run black tests/unit_tests/test_core_api.py to fix.

goodmap/core_api.py (2)

1-11: Fix Black formatting to pass CI.

The pipeline indicates Black formatting check failed. Run black goodmap/core_api.py to fix.


78-87: Mutable default argument feature_flags={}.

Using a mutable default ({}) can cause unexpected behavior if the dict is modified. Replace with None and initialize inside the function.

Proposed fix
 def core_pages(
     database,
     languages: LanguagesMapping,
     notifier_function,
     csrf_generator,
     location_model,
     photo_attachment_class: type[AttachmentProtocol],
     photo_attachment_config: AttachmentConfig,
-    feature_flags={},
+    feature_flags=None,
 ) -> Blueprint:
     core_api_blueprint = Blueprint("api", __name__, url_prefix="/api")
+    if feature_flags is None:
+        feature_flags = {}
🧹 Nitpick comments (3)
goodmap/goodmap.py (1)

22-23: Unused logger initialization.

The logger is initialized at module level but never used in this file. Consider removing it if not needed, or this may be intentional for future use.

tests/unit_tests/test_core_api.py (2)

403-419: Move BytesIO import to module level.

BytesIO is imported inside multiple test functions. Move it to the module-level imports for cleaner code.

Proposed fix at top of file
 import json
+from io import BytesIO
 from unittest import mock

Then remove the from io import BytesIO lines from inside each test function.


505-528: Test assumes ordered suggestion retrieval.

Line 528 accesses suggestions[-1] assuming the newly added suggestion is last. This may be fragile if the database doesn't guarantee insertion order. Consider filtering by name or using a unique identifier instead.

Proposed fix
     # Verify suggestion was stored
     suggestions = db.get_suggestions({})
     assert len(suggestions) == initial_count + 1
-    assert suggestions[-1]["name"] == "Test Location With Photo"
+    # Find the newly added suggestion by name
+    new_suggestion = next(
+        (s for s in suggestions if s["name"] == "Test Location With Photo"), None
+    )
+    assert new_suggestion is not None

@sonarqubecloud

Copy link
Copy Markdown

@raven-wing raven-wing changed the title feat: images support introduced feat: implement photo uploads with file validation Jan 24, 2026
@github-actions

Copy link
Copy Markdown

🧪 E2E Test Results

📊 View full workflow run
🔗 Commit: db3b328

📊 E2E Stress Test Performance

✅ Status: PASSED (11176.24ms max < 25000ms limit)

Metric Value
Average Time 10646.63ms
Minimum Time 9828.07ms
Maximum Time 11176.24ms
Completed Runs 5/5
Avg Markers Loaded 74
📈 Individual Run Times
Run Time (ms) Markers
Run 1 9828.07ms 74
Run 2 10992.69ms 74
Run 3 10114.31ms 74
Run 4 11176.24ms 74
Run 5 11121.84ms 74

@raven-wing
raven-wing merged commit 63c8fe7 into Problematy:main Jan 24, 2026
6 checks passed
@raven-wing
raven-wing deleted the image branch January 24, 2026 00:06
@coderabbitai coderabbitai Bot mentioned this pull request Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant