Skip to content

[Forge][P1] Allow editing custom Post Types after creation#246

Merged
gregagi merged 4 commits intomainfrom
forge/p1-edit-custom-post-types
Mar 19, 2026
Merged

[Forge][P1] Allow editing custom Post Types after creation#246
gregagi merged 4 commits intomainfrom
forge/p1-edit-custom-post-types

Conversation

@gregagi
Copy link
Collaborator

@gregagi gregagi commented Mar 19, 2026

Summary\n- add a dedicated edit route/view for existing custom post types\n- add an edit page for updating name, prompt guidance, and logo with explicit logo removal support\n- simplify custom post types list UI to use an Edit action instead of inline edit forms\n- extend regression tests for edit validation, persistence, and logo updates/removal\n- update CHANGELOG with shipped user-facing behavior\n\n## Testing\n- \n- Local pytest/docker run is not available in this runtime (), so CI is the gate for full test execution.\n

Summary by CodeRabbit

Release Notes

  • New Features

    • Dedicated edit page for custom post types with support for updating name, prompt guidance, and logo
    • Explicit logo removal capability for custom post types
  • Tests

    • Added regression tests covering edit persistence and validation failure scenarios

@coderabbitai
Copy link
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 6 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: 9abd7ec1-78ab-4ac3-a80f-0613feb4bb19

📥 Commits

Reviewing files that changed from the base of the PR and between e84af3c and 204f977.

📒 Files selected for processing (3)
  • core/forms.py
  • core/tests/test_custom_post_types.py
  • core/views.py
📝 Walkthrough

Walkthrough

Added a dedicated custom post type edit page with a form supporting name, prompt guidance, and logo updates. Includes explicit logo removal functionality and form-level validation to prevent simultaneous uploads and removals.

Changes

Cohort / File(s) Summary
Form & Validation
core/forms.py
Added optional remove_logo boolean field to ProjectCustomPostTypeForm. Implemented clean() validation to reject simultaneous logo upload and removal. Modified save() to delete existing logo file when remove_logo is true.
View & Routing
core/views.py, core/urls.py
Introduced ProjectCustomPostTypeEditView with dispatch, GET (form rendering), and POST (validation/save) handlers. Modified ProjectCustomPostTypeUpdateView.post() to forward requests to the new edit view. Added URL route project/<int:pk>/posts/custom/<int:post_type_pk>/edit/.
Templates
frontend/templates/project/project_custom_post_type_edit.html, frontend/templates/project/project_custom_post_types.html
Created new edit template with form fields for name, prompt guidance, logo upload, and conditional remove logo checkbox. Refactored list template to replace inline forms with separate edit and open links.
Tests
core/tests/test_custom_post_types.py
Updated existing test to target project_custom_post_type_edit URL. Added test_edit_custom_post_type_updates_name_prompt_and_logo verifying persistence and logo path. Added test_edit_custom_post_type_can_remove_existing_logo verifying logo deletion.
Documentation
CHANGELOG.md
Added unreleased changelog entry documenting the new edit page with validated updates, explicit logo removal, and regression test coverage.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PR #245: Modifies ProjectCustomPostTypeForm, view, template, and tests for logo upload and removal handling in custom post type management.
  • PR #220: Introduces the foundational custom post types CRUD workflow that this PR extends with a dedicated edit page and logo removal feature.

Poem

🐰 A form to edit, a logo to remove,
With validation so clean, we're in the groove,
No upload and delete in a single stride,
Just graceful edits, side by side! ✨

🚥 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 clearly and concisely describes the main change: enabling post-creation editing of custom post types, which is the primary feature addition 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-edit-custom-post-types
📝 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
Contributor

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR replaces the previous inline-edit forms on the custom post types list page with a dedicated /edit/ page (ProjectCustomPostTypeEditView), adds explicit logo removal support via a remove_logo form field, and extends the test suite with coverage for edit persistence and logo operations.

Key changes and findings:

  • core/views.pyProjectCustomPostTypeEditView overrides dispatch() and accesses request.user.profile before calling super().dispatch() (where LoginRequiredMixin performs its authentication check). An unauthenticated request will raise AttributeError and produce a 500 response rather than a login redirect. All other views in the codebase avoid this by accessing the profile only inside get_queryset() or get_context_data(), which run after the mixin check.
  • core/views.pyProjectCustomPostTypeUpdateView (the old /update/ endpoint) is kept as a delegation shim. Its POST failure path now returns full HTML (the edit page) instead of the previous redirect-with-flash pattern, which is a silent contract change for any caller that expected a 302.
  • core/forms.pyremove_logo field and save() override are well-implemented; the mutual-exclusion validation with a new logo upload is correct.
  • frontend/templates/project/project_custom_post_type_edit.html — Minor UX edge case: the header logo preview and remove-checkbox are driven by the DB instance, so they can appear stale when the form re-renders after an error where remove_logo=true was submitted.
  • Tests — Good coverage for the new edit flow, logo upload, and logo removal scenarios.

Confidence Score: 2/5

  • Not safe to merge until the authentication ordering bug in dispatch() is fixed — unauthenticated requests to the edit URL produce a 500 instead of a login redirect.
  • The core feature logic (form, save, logo removal, templates, tests) is solid, but there is a genuine P1 bug: ProjectCustomPostTypeEditView.dispatch() accesses request.user.profile before LoginRequiredMixin has verified the user is authenticated. This causes a 500 for any unauthenticated access rather than the expected redirect to login. Fixing this brings the score to a comfortable merge range.
  • core/views.py — specifically the dispatch() method of ProjectCustomPostTypeEditView and the now-redundant ProjectCustomPostTypeUpdateView.

Important Files Changed

Filename Overview
core/views.py Introduces ProjectCustomPostTypeEditView with a dispatch() override that accesses request.user.profile before LoginRequiredMixin checks authentication — unauthenticated requests raise AttributeError (500) instead of redirecting. Also keeps a now-redundant ProjectCustomPostTypeUpdateView that proxies to the new edit view with changed failure-response semantics.
core/forms.py Adds remove_logo BooleanField and cross-field clean() validation; overrides save() to handle file deletion. Logic is correct — conflict between remove_logo and a new upload is properly rejected, and file deletion via FieldFile.delete(save=False) is standard Django practice.
core/tests/test_custom_post_types.py Good regression coverage added: tests for name/prompt/logo update, logo removal, and duplicate-name rejection with the new 200+form-error response contract. Existing test correctly updated to point to the new project_custom_post_type_edit URL.
core/urls.py New /edit/ route added cleanly. The legacy /update/ route is retained, now backed by a delegation shim — both routes coexist, which may cause maintenance confusion.
frontend/templates/project/project_custom_post_type_edit.html Well-structured edit page with per-field error rendering and conditional logo removal UI. Minor UX issue: the header logo preview and remove-checkbox visibility use the DB instance state rather than the form's current submission state, which can look stale on re-render after errors.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant ListPage as project_custom_post_types (GET/POST)
    participant EditView as ProjectCustomPostTypeEditView /edit/
    participant UpdateView as ProjectCustomPostTypeUpdateView /update/ (legacy)
    participant Form as ProjectCustomPostTypeForm
    participant DB as Database / Storage

    Browser->>ListPage: GET /project/:pk/posts/custom/
    ListPage-->>Browser: list + create form

    Browser->>EditView: GET /project/:pk/posts/custom/:id/edit/
    Note over EditView: dispatch() — get_object_or_404<br/>(runs BEFORE LoginRequiredMixin auth check ⚠️)
    EditView->>Form: ProjectCustomPostTypeForm(instance=cpt)
    EditView-->>Browser: edit page with pre-filled form

    Browser->>EditView: POST /edit/ (name, prompt, logo?, remove_logo?)
    EditView->>Form: form.is_valid()
    alt valid
        Form->>DB: form.save() — update fields, delete logo if remove_logo
        EditView-->>Browser: 302 → project_custom_post_types
    else invalid
        EditView-->>Browser: 200 edit page with inline errors
    end

    Browser->>UpdateView: POST /update/ (legacy path)
    UpdateView->>EditView: ProjectCustomPostTypeEditView.as_view()(request)
    EditView-->>Browser: same response as /edit/ POST path
Loading

Last reviewed commit: "feat(posts): add ded..."

core/views.py Outdated
Comment on lines +1550 to +1557
def dispatch(self, request, *args, **kwargs):
self.project = get_object_or_404(Project, pk=kwargs["pk"], profile=request.user.profile)
self.custom_post_type = get_object_or_404(
ProjectCustomPostType,
pk=kwargs["post_type_pk"],
project=self.project,
)
return super().dispatch(request, *args, **kwargs)
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 dispatch() accesses request.user.profile before LoginRequiredMixin runs

The overridden dispatch() accesses request.user.profile on lines 1551–1556 before calling super().dispatch(), which is where LoginRequiredMixin performs the authentication check. For an unauthenticated request, request.user is AnonymousUser, which has no profile attribute — this raises AttributeError (a 500) instead of redirecting to the login page.

Compare this with the safe pattern used by ProjectCustomPostTypePostsView and ProjectCustomPostTypesView, where request.user.profile is only accessed inside get_queryset() / get_context_data(), both of which are called after LoginRequiredMixin has already verified the user is authenticated.

Fix: guard profile access or restructure to let super().dispatch() run first:

def dispatch(self, request, *args, **kwargs):
    # Let LoginRequiredMixin redirect unauthenticated users first.
    response = super().dispatch(request, *args, **kwargs)
    if not request.user.is_authenticated:
        return response
    self.project = get_object_or_404(Project, pk=kwargs["pk"], profile=request.user.profile)
    self.custom_post_type = get_object_or_404(
        ProjectCustomPostType,
        pk=kwargs["post_type_pk"],
        project=self.project,
    )
    return self.get(request, *args, **kwargs) if request.method == "GET" else self.post(request, *args, **kwargs)

Or, more cleanly, move the lookups into a helper that both get() and post() call after the mixin has checked auth.

core/views.py Outdated
Comment on lines +1596 to +1598
class ProjectCustomPostTypeUpdateView(LoginRequiredMixin, View):
def post(self, request, pk, post_type_pk):
return ProjectCustomPostTypeEditView.as_view()(request, pk=pk, post_type_pk=post_type_pk)
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Redundant /update/ endpoint now delegates to edit view

ProjectCustomPostTypeUpdateView now just proxies every POST to ProjectCustomPostTypeEditView. The list template was updated to link directly to the /edit/ route, so /update/ is no longer the canonical path for any UI action.

This leaves a duplicate endpoint that subtly changes the old behaviour: a failed form submission on /update/ now returns a full 200 HTML page (the edit template) instead of the previous redirect-with-messages to the list page. Any client (e.g. integration tests, external consumers) that relied on the old redirect-on-failure contract will silently get HTML instead of a 302.

Consider either removing the /update/ URL entry (and this class) entirely — or, if backward compat is needed, explicitly documenting it as a deprecated alias with a note that it may be removed later.

Comment on lines +47 to +53
{% if custom_post_type.logo %}
<div class="flex gap-2 items-center">
<input id="id_remove_logo" name="remove_logo" type="checkbox" value="true" {% if form.remove_logo.value %}checked{% endif %} class="w-4 h-4 text-gray-900 rounded border-gray-300 focus:ring-gray-500" />
<label for="id_remove_logo" class="text-sm text-gray-700">Remove current logo</label>
</div>
{% for error in form.remove_logo.errors %}<p class="mt-1 text-sm text-red-600">{{ error }}</p>{% endfor %}
{% endif %}
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Stale header logo when form re-renders after a remove_logo + error

Both the header preview image (line 12) and the "Remove current logo" checkbox visibility (line 47) are driven by custom_post_type.logo, which is the database state — not the form submission state. If a user checks "Remove current logo", also triggers another validation error (unlikely but possible), and the form re-renders, they will see the old logo in the header and the checkbox still available, even though their intent was to remove it. The checkbox will be pre-checked (because form.remove_logo.value is True) so re-submitting works correctly — but the stale logo preview can look confusing.

This is a minor UX polish item: consider updating the header preview to use the form's current state rather than the DB instance logo, e.g. hiding it when form.remove_logo.value is truthy.

Copy link
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: 1

🧹 Nitpick comments (1)
core/tests/test_custom_post_types.py (1)

194-247: Add one regression for the new remove-vs-upload validation branch.

The new coverage exercises successful edits and duplicate-name failures, but it never submits both remove_logo and logo, so the new edit-specific validation path is still unpinned.

🧪 Suggested test
+@pytest.mark.django_db
+@override_settings(MEDIA_ROOT="/tmp/tuxseo-test-media")
+def test_edit_custom_post_type_rejects_remove_logo_and_new_logo_together(client):
+    user = User.objects.create_user(
+        "owner-conflict-logo",
+        "owner-conflict-logo@example.com",
+        "secret",
+    )
+    project = Project.objects.create(profile=user.profile, name="Site", url="https://site.test")
+    post_type = ProjectCustomPostType.objects.create(
+        project=project,
+        name="Tutorial",
+        prompt_guidance="Step-by-step instructional style.",
+        logo=SimpleUploadedFile("existing.png", TINY_PNG_BYTES, content_type="image/png"),
+    )
+
+    client.force_login(user)
+    response = client.post(
+        reverse("project_custom_post_type_edit", kwargs={"pk": project.id, "post_type_pk": post_type.id}),
+        {
+            "name": "Tutorial",
+            "prompt_guidance": "Updated instructional style.",
+            "remove_logo": "true",
+            "logo": SimpleUploadedFile("logo.png", TINY_PNG_BYTES, content_type="image/png"),
+        },
+    )
+
+    assert response.status_code == 200
+    assert "Choose either remove logo or upload a new logo, not both." in response.content.decode("utf-8")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/tests/test_custom_post_types.py` around lines 194 - 247, Add a
regression test that posts both "remove_logo" and a new "logo" together to
exercise the remove-vs-upload validation branch: create a user/project/PostType
with an existing logo, log in, POST to the same "project_custom_post_type_edit"
endpoint with form data including "remove_logo": "true" and a
SimpleUploadedFile(...) under "logo", then assert the response indicates a
validation error (no 302 redirect), the form contains the expected error
message, and the post_type.logo remains unchanged; name the test something like
test_edit_custom_post_type_remove_and_upload_logo_conflict to mirror the
existing tests.
🤖 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 400-408: The save method currently deletes instance.logo
immediately when self.cleaned_data.get("remove_logo") is true; instead, capture
the existing file object (old_logo = instance.logo), set instance.logo = None so
the DB will be updated, then perform instance.save() and, only if commit is
True, schedule the file deletion inside transaction.on_commit(lambda:
old_logo.delete(save=False)); do not delete the file when commit is False.
Reference: save(), self.cleaned_data.get("remove_logo"), instance.logo, and
transaction.on_commit.

---

Nitpick comments:
In `@core/tests/test_custom_post_types.py`:
- Around line 194-247: Add a regression test that posts both "remove_logo" and a
new "logo" together to exercise the remove-vs-upload validation branch: create a
user/project/PostType with an existing logo, log in, POST to the same
"project_custom_post_type_edit" endpoint with form data including "remove_logo":
"true" and a SimpleUploadedFile(...) under "logo", then assert the response
indicates a validation error (no 302 redirect), the form contains the expected
error message, and the post_type.logo remains unchanged; name the test something
like test_edit_custom_post_type_remove_and_upload_logo_conflict to mirror the
existing tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ead4034-2bbc-44c8-91b6-941768b0a7bb

📥 Commits

Reviewing files that changed from the base of the PR and between 39c8edb and e84af3c.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • core/forms.py
  • core/tests/test_custom_post_types.py
  • core/urls.py
  • core/views.py
  • frontend/templates/project/project_custom_post_type_edit.html
  • frontend/templates/project/project_custom_post_types.html

@gregagi gregagi merged commit 4b3fc07 into main Mar 19, 2026
4 checks passed
@gregagi gregagi deleted the forge/p1-edit-custom-post-types branch March 19, 2026 05:47
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