Skip to content

[Forge][P1] Add logo selection to custom Post Type creation flow#245

Merged
gregagi merged 7 commits intomainfrom
forge/p1-custom-post-type-logo-create-flow
Mar 19, 2026
Merged

[Forge][P1] Add logo selection to custom Post Type creation flow#245
gregagi merged 7 commits intomainfrom
forge/p1-custom-post-type-logo-create-flow

Conversation

@gregagi
Copy link
Copy Markdown
Collaborator

@gregagi gregagi commented Mar 19, 2026

Summary

  • add optional logo image upload on custom post type create/edit forms
  • persist logo reference on ProjectCustomPostType with migration
  • validate uploaded logo type (PNG/JPG/WEBP/GIF) and size (<=2MB)
  • render logo across custom post type UI surfaces (management cards, nav rows, post type page header) with fallback icon when absent
  • add regression tests for create flow with logo, without logo, and invalid logo uploads
  • update changelog

Testing

  • ./.venv/bin/python -m compileall core/models.py core/forms.py core/views.py core/tests/test_custom_post_types.py
  • ./.venv/bin/pytest core/tests/test_custom_post_types.py -q (fails in current sandbox due unavailable Postgres host db; expected to run in CI environment)

Summary by CodeRabbit

  • New Features

    • Custom post types now support optional logo uploads (PNG, JPG, WEBP, GIF; up to 2MB).
    • Uploaded logos display in navigation menus and post-type headers; default icons appear when no logo is set.
    • Added ability to remove existing logos via update form.
  • Tests

    • Added comprehensive test coverage for logo upload, validation, and clearing functionality.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 19, 2026

Warning

Rate limit exceeded

@gregagi has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 13 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ebc93c7a-1cb6-4f70-a033-0f520068ac08

📥 Commits

Reviewing files that changed from the base of the PR and between 88ebecf and d785d98.

📒 Files selected for processing (3)
  • core/forms.py
  • core/tests/test_custom_post_types.py
  • frontend/templates/project/project_custom_post_types.html
📝 Walkthrough

Walkthrough

This pull request adds logo upload functionality to the custom post types feature. Users can upload PNG/JPG/WEBP/GIF logos (up to 2MB) when creating or editing custom post types. The logos persist on the post-type records and display across UI surfaces with fallback to default icons.

Changes

Cohort / File(s) Summary
Model and Database
core/models.py, core/migrations/0065_projectcustomposttype_logo.py
Added logo ImageField to ProjectCustomPostType with file extension validation (png/jpg/jpeg/webp/gif) and class-level constraints: logo_max_file_size_bytes (2MB) and logo_allowed_content_types. Extended model's clean() method to enforce file size limits.
Form Handling
core/forms.py
Extended ProjectCustomPostTypeForm with optional logo file upload field and clear_logo boolean control. Added clean_logo() validation enforcing content type and file size constraints. Overrode save() method to handle logo clearing when clear_logo is set without a new upload.
View Layer
core/views.py
Updated ProjectCustomPostTypesView.post and ProjectCustomPostTypeUpdateView.post to accept request.FILES alongside request.POST. Enhanced error handling in update view to emit per-field error messages with field-specific labels.
UI Templates
frontend/templates/project/project_custom_post_types.html, frontend/templates/project/project_custom_post_type_posts.html, frontend/templates/components/project_navigation.html
Added logo upload UI to create/edit forms with multipart/form-data encoding and validation error display. Updated post-type header and navigation to render logo images with fallback to placeholder SVG icons.
Testing
core/tests/test_custom_post_types.py
Added comprehensive test coverage for logo upload flows: successful uploads, missing logo field, unsupported formats, oversized files, and logo clearing on update.
Documentation
CHANGELOG.md
Documented new logo upload capability with file format support and fallback behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A logo hops into view,
Uploading dreams in every hue,
Two megabytes of pride so fine,
No format fails—PNG to WEBP align,
Custom posts now wear their crown! 👑

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: adding logo selection to the custom post type creation flow, which is the core feature described across all modified files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch forge/p1-custom-post-type-logo-create-flow
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR adds optional logo upload support to the custom post type create/edit flows — persisting the logo on ProjectCustomPostType, validating format (PNG/JPG/WEBP/GIF) and size (≤2 MB) at both the form and model layers, and rendering the logo across three UI surfaces (management cards, nav rows, post-type page header) with an SVG fallback. The feature integrates cleanly with the existing form/view/model pattern.

Key findings:

  • Broken testtest_create_custom_post_type_rejects_oversized_logo supplies raw non-image bytes (b"0" * 2097153) as the file payload. Django's forms.ImageField runs PIL validation in to_python() before clean_logo is called, so PIL rejects the file with an "invalid image" error and the expected "Logo must be 2MB or smaller" message never appears — the test will fail.
  • No logo removal path — once a user sets a logo on an existing post type they cannot clear it. The edit form renders a plain <input type="file"> with no "Remove logo" affordance, and Django silently preserves the existing file if nothing is uploaded.
  • Validation errors silently dropped in update flow — the update view always redirects regardless of form validity, so if a user uploads an oversized or wrong-format logo during an edit, they receive only the generic "Could not update custom post type. Please check the form." flash message with no field-level detail.
  • FileExtensionValidator weaker than form-level check — the model-level validator only inspects the file extension (trivially spoofable), while clean_logo correctly checks the MIME content_type. The extension validator adds no meaningful security over what forms.ImageField + PIL already enforce.

Confidence Score: 2/5

  • Not safe to merge as-is — one regression test is likely broken and users who upload a logo have no way to remove it.
  • The oversized-logo test uses structurally invalid PNG bytes that PIL rejects before the size check fires, so the test will fail in CI. The missing logo-removal path is a user-facing functional gap that would require a follow-up PR to fix. These two issues, one blocking CI and one a product gap, lower confidence significantly.
  • core/tests/test_custom_post_types.py (broken size test) and frontend/templates/project/project_custom_post_types.html (no logo removal/error display in edit form).

Important Files Changed

Filename Overview
core/tests/test_custom_post_types.py Adds four new test cases for logo upload; the oversized-logo test uses invalid PNG bytes that PIL rejects before the size check runs, making the test likely to fail with the wrong error message.
frontend/templates/project/project_custom_post_types.html Adds logo preview and file inputs to create/edit forms; the edit form uses raw HTML for the file input, lacks a logo-clearing affordance, and silently drops validation errors due to the always-redirect update view.
core/models.py Adds logo ImageField with FileExtensionValidator and size check in clean(); validator is weaker than the form-level content-type check, but model otherwise looks correct.
core/forms.py Adds logo to form fields with content-type and size validation in clean_logo; works correctly for the create flow.
core/views.py Correctly passes request.FILES to both create and update forms; update view always redirects so logo validation errors are surfaced only as generic flash messages.
core/migrations/0065_projectcustomposttype_logo.py Clean migration adding nullable logo ImageField to ProjectCustomPostType.
frontend/templates/components/project_navigation.html Replaces the SVG placeholder with the post type logo when one is set; fallback SVG is preserved.
frontend/templates/project/project_custom_post_type_posts.html Adds logo or SVG fallback to the post type page header; straightforward and correct.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant CreateView as ProjectCustomPostTypesView
    participant UpdateView as ProjectCustomPostTypeUpdateView
    participant Form as ProjectCustomPostTypeForm
    participant Model as ProjectCustomPostType
    participant Storage

    Note over Browser,Storage: Create flow
    Browser->>CreateView: POST /project/:pk/custom-post-types (multipart)
    CreateView->>Form: ProjectCustomPostTypeForm(POST, FILES)
    Form->>Form: ImageField.to_python() → PIL validates image
    Form->>Form: clean_logo() → checks content_type + size
    alt Form invalid
        CreateView-->>Browser: 200 re-render with inline errors
    else Form valid
        Form->>Model: save(commit=False) → project assigned
        Model->>Model: full_clean() → clean() size check
        Model->>Storage: upload to custom_post_type_logos/
        CreateView-->>Browser: 302 redirect
    end

    Note over Browser,Storage: Edit/Update flow
    Browser->>UpdateView: POST /project/:pk/post-type/:id (multipart)
    UpdateView->>Form: ProjectCustomPostTypeForm(POST, FILES, instance=…)
    Form->>Form: ImageField.to_python() → PIL validates image
    Form->>Form: clean_logo() → checks content_type + size
    alt Form invalid
        UpdateView-->>Browser: 302 redirect + generic flash error ⚠️
    else Form valid
        Form->>Model: form.save()
        Model->>Model: full_clean()
        Model->>Storage: upload / keep existing logo
        UpdateView-->>Browser: 302 redirect + success flash
    end
Loading

Last reviewed commit: "Add custom post type..."

Comment on lines +313 to +326
oversized = b"0" * (ProjectCustomPostType.logo_max_file_size_bytes + 1)

client.force_login(user)
response = client.post(
reverse("project_custom_post_types", kwargs={"pk": project.id}),
{
"name": "News",
"prompt_guidance": "Fast-paced updates.",
"logo": SimpleUploadedFile("logo.png", oversized, content_type="image/png"),
},
)

assert response.status_code == 200
assert "Logo must be 2MB or smaller" in response.content.decode("utf-8")
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.

P1 Oversized logo test will fail — PIL rejects the payload before size check runs

The oversized data (b"0" * 2097153) is not a valid PNG file. Django's forms.ImageField (the form field type produced from a model ImageField) calls PIL's Image.open() + image.verify() in to_python() before the form's clean_logo method executes. PIL will reject the raw bytes with a "Upload a valid image…" error, so the "Logo must be 2MB or smaller" assertion will never be true and the test will fail.

To properly test the size path you need a structurally valid image whose byte-length exceeds 2 MB. One common approach is to embed the 1×1 PNG bytes inside a large-enough payload by stuffing a PNG comment chunk, or to mock logo.size at the form layer. For example:

# Build a >2 MB payload that PIL still accepts by wrapping TINY_PNG_BYTES in
# an in-memory file but overriding the reported size.
from unittest.mock import patch
from django.core.files.uploadedfile import SimpleUploadedFile

oversized_file = SimpleUploadedFile("logo.png", TINY_PNG_BYTES, content_type="image/png")
oversized_file._size = ProjectCustomPostType.logo_max_file_size_bytes + 1

client.force_login(user)
response = client.post(
    reverse("project_custom_post_types", kwargs={"pk": project.id}),
    {
        "name": "News",
        "prompt_guidance": "Fast-paced updates.",
        "logo": oversized_file,
    },
)

SimpleUploadedFile exposes a writable _size attribute that the form reads via logo.size, letting you simulate an oversized file without actually allocating 2 MB or crafting a real large image.

Comment on lines +65 to +69
<div>
<label class="block mb-1 text-sm font-medium text-gray-700">Logo (optional)</label>
<input type="file" name="logo" accept="image/png,image/jpeg,image/webp,image/gif" class="block w-full text-sm text-gray-900 rounded-md border border-gray-300 cursor-pointer bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-gray-500" />
<p class="mt-1 text-xs text-gray-500">Accepted: PNG, JPG, WEBP, GIF · max 2MB.</p>
</div>
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.

P1 No way to remove an existing logo once set

The edit form only renders a blank <input type="file">. When the form is submitted without selecting a new file, Django's FileField preserves the existing value from the instance — which is correct — but it also means there is no mechanism for a user to clear a logo they previously uploaded. They can replace it but never remove it.

A common Django pattern is to add a ClearableFileInput widget or a companion boolean checkbox clear_logo that, when checked, sets logo = None in the view before form.save(). Even a simple "Remove logo" button that POSTs to a dedicated endpoint would unblock users.

Additionally, the edit form renders the file input as raw HTML rather than through the form widget ({{ edit_form.logo }}), which means any error messages from clean_logo (wrong format, oversized) are silently dropped when the update view redirects — the user only sees the generic "Could not update custom post type. Please check the form." flash message with no indication of which field failed or why.

Comment on lines +783 to +787
logo = models.ImageField(
upload_to="custom_post_type_logos/",
blank=True,
validators=[FileExtensionValidator(allowed_extensions=["png", "jpg", "jpeg", "webp", "gif"])],
)
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.

P2 FileExtensionValidator is weaker than the form's content-type check and could be misleading

FileExtensionValidator only inspects the file's extension (e.g., ".png"), which an attacker can trivially spoof. The form's clean_logo already enforces a stricter check via the MIME content_type attribute. Having the extension validator on the model could give a false sense of security for code paths that bypass the form (e.g., tests or management commands that create records directly), while still allowing a file named evil.png with non-image content to pass through the model-level guard.

Consider replacing it with a custom validator that also checks the declared MIME type, or relying solely on the forms.ImageField PIL validation (which is already the strongest layer). If the intent is defence-in-depth at the model layer, the validator should at least mirror what the form checks.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
core/views.py (1)

1567-1574: Add a structured log before redirecting invalid updates.

These failures currently only hit flash messages, so repeated rejected logo uploads leave no server-side trace. Logging project_id, post_type_id, and form.errors.get_json_data() here would make support/debugging much easier.

As per coding guidelines, "Use Python's logging module (structlog) extensively" and "Include context in log messages with relevant fields (error, exc_info, resource IDs, etc.) to aid debugging and traceability."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/views.py` around lines 1567 - 1574, Add a structured server-side log
before emitting flash messages for invalid form updates: call the module's
structlog logger (e.g., logger = structlog.get_logger() or existing logger) and
log at error level including fields project_id, post_type_id and the form errors
via form.errors.get_json_data(), plus any contextual info like request.user.id;
do this right before the existing messages.error loop so invalid update attempts
are recorded with these identifiers and the error payload for
debugging/traceability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/forms.py`:
- Around line 340-353: Replace the explicit logo = forms.FileField override with
Django's forms.ImageField so Pillow-based image validation is preserved (keep
the existing widget attrs and optional required=False setting), and retain any
size checks in clean_logo() but stop relying solely on client-supplied
content_type; ensure the form continues to use the ProjectCustomPostType model
and fields in Meta and leave clear_logo untouched—update references to logo and
clean_logo accordingly so ImageField.clean handles actual image verification
while you still enforce max-size server-side.

In `@frontend/templates/project/project_custom_post_types.html`:
- Around line 65-68: The label for the logo file input is not bound to the
input, so add an id to the input (e.g., id="logo") and set the label's for
attribute to that id to associate them (look for the <label> and <input
name="logo"> elements in project_custom_post_types.html); optionally ensure the
helper paragraph is referenced via aria-describedby on the input if you want
screen readers to read the "Accepted: ..." text (use the helper's id and set
input aria-describedby to that id).

---

Nitpick comments:
In `@core/views.py`:
- Around line 1567-1574: Add a structured server-side log before emitting flash
messages for invalid form updates: call the module's structlog logger (e.g.,
logger = structlog.get_logger() or existing logger) and log at error level
including fields project_id, post_type_id and the form errors via
form.errors.get_json_data(), plus any contextual info like request.user.id; do
this right before the existing messages.error loop so invalid update attempts
are recorded with these identifiers and the error payload for
debugging/traceability.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8bdc99c2-bb34-499f-a674-ecb07e6ed2b5

📥 Commits

Reviewing files that changed from the base of the PR and between d7d3ec4 and 88ebecf.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • core/forms.py
  • core/migrations/0065_projectcustomposttype_logo.py
  • core/models.py
  • core/tests/test_custom_post_types.py
  • core/views.py
  • frontend/templates/components/project_navigation.html
  • frontend/templates/project/project_custom_post_type_posts.html
  • frontend/templates/project/project_custom_post_types.html

Comment on lines +340 to +353
logo = forms.FileField(
required=False,
widget=forms.FileInput(
attrs={
"class": "block w-full text-sm text-gray-900 rounded-md border border-gray-300 cursor-pointer bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-gray-500",
"accept": "image/png,image/jpeg,image/webp,image/gif",
}
),
)
clear_logo = forms.BooleanField(required=False)

class Meta:
model = ProjectCustomPostType
fields = ["name", "prompt_guidance"]
fields = ["name", "prompt_guidance", "logo"]
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "models.py" | head -20

Repository: rasulkireev/TuxSEO

Length of output: 181


🏁 Script executed:

rg "class ProjectCustomPostType" -A 30 --type py

Repository: rasulkireev/TuxSEO

Length of output: 10648


🏁 Script executed:

rg -B 5 -A 15 "logo.*=.*ImageField\|logo.*=.*FileField" core/forms.py

Repository: rasulkireev/TuxSEO

Length of output: 44


🏁 Script executed:

# Get the full ProjectCustomPostTypeForm to understand the structure
sed -n '320,410p' core/forms.py

Repository: rasulkireev/TuxSEO

Length of output: 3023


🏁 Script executed:

# Check if Pillow is in dependencies and if ImageField validation actually happens
rg -i "pillow|image" pyproject.toml requirements.txt setup.py setup.cfg 2>/dev/null | head -20

Repository: rasulkireev/TuxSEO

Length of output: 181


🏁 Script executed:

# Verify the model field definition and understand the validators
rg -B 5 -A 5 "class ProjectCustomPostType" core/models.py

Repository: rasulkireev/TuxSEO

Length of output: 336


Don't downgrade the model ImageField to a plain FileField.

The explicit logo = forms.FileField override removes Django's built-in image validation. The form's clean_logo() method only checks the content_type header attribute (client-supplied, spoofable) and file size. A crafted request can submit arbitrary bytes with a PNG/JPEG extension and matching Content-Type header, bypassing validation and breaking the security guarantee provided by Pillow's ImageField validation.

🖼️ Proposed fix
 class ProjectCustomPostTypeForm(forms.ModelForm):
-    logo = forms.FileField(
-        required=False,
-        widget=forms.FileInput(
-            attrs={
-                "class": "block w-full text-sm text-gray-900 rounded-md border border-gray-300 cursor-pointer bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-gray-500",
-                "accept": "image/png,image/jpeg,image/webp,image/gif",
-            }
-        ),
-    )
     clear_logo = forms.BooleanField(required=False)
 
     class Meta:
         model = ProjectCustomPostType
         fields = ["name", "prompt_guidance", "logo"]
         widgets = {
+            "logo": forms.FileInput(
+                attrs={
+                    "class": "block w-full text-sm text-gray-900 rounded-md border border-gray-300 cursor-pointer bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-gray-500",
+                    "accept": "image/png,image/jpeg,image/webp,image/gif",
+                }
+            ),
             "name": forms.TextInput(
🧰 Tools
🪛 Ruff (0.15.6)

[warning] 353-353: Mutable default value for class attribute

(RUF012)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/forms.py` around lines 340 - 353, Replace the explicit logo =
forms.FileField override with Django's forms.ImageField so Pillow-based image
validation is preserved (keep the existing widget attrs and optional
required=False setting), and retain any size checks in clean_logo() but stop
relying solely on client-supplied content_type; ensure the form continues to use
the ProjectCustomPostType model and fields in Meta and leave clear_logo
untouched—update references to logo and clean_logo accordingly so
ImageField.clean handles actual image verification while you still enforce
max-size server-side.

Comment on lines +65 to +68
<div>
<label class="block mb-1 text-sm font-medium text-gray-700">Logo (optional)</label>
<input type="file" name="logo" accept="image/png,image/jpeg,image/webp,image/gif" class="block w-full text-sm text-gray-900 rounded-md border border-gray-300 cursor-pointer bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-gray-500" />
<p class="mt-1 text-xs text-gray-500">Accepted: PNG, JPG, WEBP, GIF · max 2MB.</p>
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.

⚠️ Potential issue | 🟡 Minor

Bind the update-form label to its file input.

The new logo label is not associated with the file input, so screen readers will not announce the control name correctly and clicking the label will not focus the picker.

♿ Proposed fix
-                <label class="block mb-1 text-sm font-medium text-gray-700">Logo (optional)</label>
-                <input type="file" name="logo" accept="image/png,image/jpeg,image/webp,image/gif" class="block w-full text-sm text-gray-900 rounded-md border border-gray-300 cursor-pointer bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-gray-500" />
+                <label class="block mb-1 text-sm font-medium text-gray-700" for="id_logo_{{ post_type.id }}">Logo (optional)</label>
+                <input id="id_logo_{{ post_type.id }}" type="file" name="logo" accept="image/png,image/jpeg,image/webp,image/gif" class="block w-full text-sm text-gray-900 rounded-md border border-gray-300 cursor-pointer bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-gray-500" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div>
<label class="block mb-1 text-sm font-medium text-gray-700">Logo (optional)</label>
<input type="file" name="logo" accept="image/png,image/jpeg,image/webp,image/gif" class="block w-full text-sm text-gray-900 rounded-md border border-gray-300 cursor-pointer bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-gray-500" />
<p class="mt-1 text-xs text-gray-500">Accepted: PNG, JPG, WEBP, GIF · max 2MB.</p>
<div>
<label class="block mb-1 text-sm font-medium text-gray-700" for="id_logo_{{ post_type.id }}">Logo (optional)</label>
<input id="id_logo_{{ post_type.id }}" type="file" name="logo" accept="image/png,image/jpeg,image/webp,image/gif" class="block w-full text-sm text-gray-900 rounded-md border border-gray-300 cursor-pointer bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-gray-500" />
<p class="mt-1 text-xs text-gray-500">Accepted: PNG, JPG, WEBP, GIF · max 2MB.</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/templates/project/project_custom_post_types.html` around lines 65 -
68, The label for the logo file input is not bound to the input, so add an id to
the input (e.g., id="logo") and set the label's for attribute to that id to
associate them (look for the <label> and <input name="logo"> elements in
project_custom_post_types.html); optionally ensure the helper paragraph is
referenced via aria-describedby on the input if you want screen readers to read
the "Accepted: ..." text (use the helper's id and set input aria-describedby to
that id).

@gregagi gregagi merged commit 39c8edb into main Mar 19, 2026
4 checks passed
@gregagi gregagi deleted the forge/p1-custom-post-type-logo-create-flow branch March 19, 2026 05:18
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