Skip to content

Story 2428: AI-Assisted Description for Blog and News Posts#2473

Open
javiercoronadonarvaez wants to merge 26 commits into
developfrom
javiercoronarv/2428-ai-assisted-desc-blog-news
Open

Story 2428: AI-Assisted Description for Blog and News Posts#2473
javiercoronadonarvaez wants to merge 26 commits into
developfrom
javiercoronarv/2428-ai-assisted-desc-blog-news

Conversation

@javiercoronadonarvaez

@javiercoronadonarvaez javiercoronadonarvaez commented May 27, 2026

Copy link
Copy Markdown
Collaborator

Issue: #2428

Summary & Context

Adds AI-assisted Description to the v3 create-post page for Blog and News posts.

A 1,000 character Description field with a live counter, an Auto-Generate Description button that calls the existing summarization pipeline synchronously, per-post-type draft persistence in localStorage for both Content and Description, and a Saving / Saved indicator that announces state changes to assistive tech. User-provided descriptions are persisted to the existing Entry.summary field and short-circuit the model's background auto-generation.

⚠️ You'll need to update your local .env so the summarization endpoint can reach OpenRouter:

  • OPENROUTER_API_KEY=<key> (required)
  • SUMMARIZATION_MODEL=<model> (optional, defaults to gpt-oss-120b — see config/settings.py:658)

Changes

Frontend (templates/news/v3/create.html, static/css/v3/create-post-page.css, templates/includes/icon.html)

  • New Description plain-text field for Blog/News with a live "N left" counter (1,000 max), typography matched to Figma: Mona Sans Medium 12px, text/primary, tracking −0.12px, line-height 1.2.
  • New "20,000 left" counter on the Content field with the same typography.
  • Auto-Generate Description button with :disabled bound to generating state.
  • Loading-state placeholder shown in place of the Description textarea while the AI fetch is in flight ("Hold on! We are generating a description for your content, it may take a few seconds" — Figma node 7134-42553).
  • Saving / Saved indicator (pixel-art spinner + floppy icons, new saved icon added to icon.html) under both Content and Description. Spinner uses CSS keyframes and respects prefers-reduced-motion.
  • localStorage-backed draft persistence keyed per post type:
    • boost:createPost:contentDraft:<postType> for Content (markdown).
    • boost:createPost:descriptionDraft:<postType> for Description (plain text).
    • Saves debounced ~800 ms after edit, restores on post-type select (and on error re-render from form.summary.value), and clears both on a validated submit.
  • Bridged the TipTap WYSIWYG editor to Alpine via a bubbling wysiwyg-update custom event (carries plain-text character count, markdown value, and a programmatic flag) and a wysiwyg-set-content window listener so the host page can restore drafts into the editor.
  • Aligned the WYSIWYG resize grip with the Description textarea's grip by moving resize: vertical from .wysiwyg-editor__body onto the inner .wysiwyg-editor__prose (scoped to .create-post-page so other pages keep their existing behavior).
  • Responsive: dropped a faulty mobile media-query that was stacking Cancel/Submit full-width. Both buttons now stay side-by-side from 390 px → 1440 px per Figma. All other layout transitions are handled by the existing flex layout.
  • Dark mode: zero new CSS needed — every color in the new CSS uses design tokens (--color-text-primary/secondary/tertiary, --color-surface-weak, --color-stroke-weak) that are already redefined under html.dark in themes.css.
  • A11y: role="status" aria-live="polite" on both Saving/Saved indicators and on the generating placeholder; <label for> on every field; char counters marked aria-hidden="true"; :aria-invalid / :aria-describedby on Content; live role="alert" error <p>s on each field for client validation.
  • Cleaned up tokens in my new CSS (--space-s instead of a hardcoded 4px, --line-height-relaxed instead of 1.24).

Backend (news/views.py, news/forms.py, news/tasks.py, news/constants.py, config/urls.py)

  • New endpoint POST /v3/news/generate-description/ (name="v3-news-generate-description"): calls summarize_content synchronously, capped at the new DESCRIPTION_SUMMARY_MAX_LENGTH = 970 (under the 1,000-char field cap for model leeway), and returns {"description": "..."}. Decorated with @require_POST. Intentionally not yet @login_required — see Risks.
  • summarize_content now accepts a max_length parameter (default 256 preserved for existing callers).
  • Added summary to BlogPostForm.Meta.fields and NewsForm.Meta.fields.
  • V3AllTypesCreateView.post() remaps the posted description field to summary before instantiating the form, so the frontend wiring stays name="description" while the model field stays summary (no migration needed).

Tooling / CI (.pre-commit-config.yaml)

  • Excluded the generated esbuild bundles static/js/v3/wysiwyg-editor.js and static/js/v3/fuse.min.js from the pre-commit hooks. The trailing-whitespace hook was modifying these and risked corrupting minified string/regex content inside them; bundles are now treated as generated artifacts.

Tests

  • Updated test_blogpost_form and test_news_form in news/tests/test_forms.py to expect the new summary field in each form's Meta.fields.

‼️ Risks & Considerations ‼️

  1. I did not modify the summariser prompt and overall functionality and parameters (temperature, top_k, top_p, etc.) because didn't want to alter the background process which is carried out when the user does not provide a description, but it's worth taking experimentation into account when deciding the token allocation schema and machine learning testing pipeline. As a whole, the prompt works well, but currently there's no way to guarantee it'll always comply with the max_length parameter or if the generated description is actually good. A testing procedure is currently being discussed with @herzog0, to be implemented if deemed appropriate by the team.

  2. generate-description endpoint is not yet login-gated. Left @login_required off for local testing (view docstring flags this). Before deploy: add @login_required and consider rate-limiting — every call hits OpenRouter and costs API budget. The endpoint is an open, unauthenticated AI proxy until then.

  3. Description is posted as description but persisted as summary via a view-level remap. Deliberately unusual: we kept the existing frontend naming (Alpine state, x-model, localStorage key, char counter binding) untouched. The single point of translation is a three-line request.POST.copy() block in V3AllTypesCreateView.post() with a comment explaining why.

  4. Shared WYSIWYG module (frontend/wysiwyg-editor.js) gained an event bridge. Added an editor.on("update") listener that dispatches wysiwyg-update on every change (computes turndown(editor.getHTML()) per dispatch), plus a window-level wysiwyg-set-content listener for restore. These are opt-in (no-ops if no host page listens / dispatches), but they execute for every editor instance globally. Worth a sanity check from someone familiar with the editor.

  5. Resize grip alignment CSS is scoped to .create-post-page because it moves the resize target from .wysiwyg-editor__body onto .wysiwyg-editor__prose. Other pages embedding the editor keep their existing grip placement.

  6. Entry.save() auto-dispatches summary_dispatcher only when summary is empty — unchanged. The new flow means a user-typed (or auto-generated) description now populates summary on submit, which short-circuits the background task. If summary is blank on submit, the background generation runs exactly as before.

  7. Dev-only quirk: with DevTools "Disable cache" on a cold network, the 670 KB editor bundle can be slow enough that clicking Submit before the editor finishes initializing leaves the hidden #field-content textarea empty → client validation rejects with "Content is required." Production caches the bundle normally; not an issue post-deploy.

  8. Pre-existing hardcoded values in create-post-page.css (the title/subtitle line-height: 1 / 1.33 and the card border-radius: 16px) were flagged during the token audit but left untouched — they come from upstream commit 26e8902f (Story Webpage UI: Create a Post #2111) and are out of scope for this PR.

Screenshots

Responsiveness and Layout

responsiveness.mov

Localstorage Persistence

localstorage.mov

Auto Generation Functionality

auto-generated-summary.mov

Self-review Checklist

  • Tag at least one team member from each team to review this PR
  • Link this PR to the related GitHub Project ticket

Frontend

  • UI implementation matches Figma design
  • Tested in light and dark mode
  • Responsive / mobile verified
  • Accessibility checked (keyboard navigation, etc.)
  • Ensure design tokens are used for colors, spacing, typography, etc. – No hardcoded values
  • Test without JavaScript (if applicable)
  • No console errors or warnings

Summary by CodeRabbit

  • New Features

    • One-click “Auto-generate Description” starts an async job, shows a generating placeholder, polls status, and surfaces errors.
    • Autosave drafts for title, content, and description with debounced Saving/Saved indicators and per-post-type restore.
    • Live character counters and improved WYSIWYG host-sync with programmatic content restore.
  • Chores

    • Development tooling config bumped and various non-functional cleanups.

@javiercoronadonarvaez javiercoronadonarvaez changed the title Javiercoronarv/2428 ai assisted desc blog news Task: AI-Assisted Description for Links to External Blog/News Posts May 27, 2026
@javiercoronadonarvaez javiercoronadonarvaez linked an issue May 27, 2026 that may be closed by this pull request
@javiercoronadonarvaez javiercoronadonarvaez changed the title Task: AI-Assisted Description for Links to External Blog/News Posts Story 2451: AI-Assisted Description for Links to External Blog/News Posts May 27, 2026
@javiercoronadonarvaez javiercoronadonarvaez force-pushed the javiercoronarv/2428-ai-assisted-desc-blog-news branch from d4af119 to b6e124d Compare May 28, 2026 00:00
@javiercoronadonarvaez javiercoronadonarvaez marked this pull request as ready for review May 29, 2026 15:29
@javiercoronadonarvaez javiercoronadonarvaez force-pushed the javiercoronarv/2428-ai-assisted-desc-blog-news branch from df4c171 to 6f862f8 Compare May 29, 2026 18:05

@jlchilders11 jlchilders11 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Overall good work, but I have a few comments, and a question over the summation that we are performing.

Also a note, it looks like the description for this PR links itself, rather than the the issue number.

Comment thread templates/includes/icon.html Outdated
<path d="M3 3h2v18H3V3zm16 0H5v2h14v14H5v2h16V3h-2zm-8 6h2V7h-2v2zm2 8h-2v-6h2v6z" />
{% elif icon_name == "check" %}
<path d="M18 6H20V8H18V6ZM16 10V8H18V10H16ZM14 12V10H16V12H14ZM12 14H14V12H12V14ZM10 16H12V14H10V16ZM8 16V18H10V16H8ZM6 14H8V16H6V14ZM6 14H4V12H6V14Z" />
{% elif icon_name == "saving" %}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If these always require a certain viewbox, I'd like to see these broken out into their own if/else check to guarantee that, rather than always having to pass that viewbox.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Great catch! Just addressed.

Comment thread news/views.py Outdated
NOTE: intentionally not login-gated yet (local testing). This endpoint calls
a paid LLM, so add @login_required (and rate limiting) before it ships.
"""
content = (request.POST.get("content") or "").strip()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
content = (request.POST.get("content") or "").strip()
content = (request.POST.get("content") or "").strip()
content = (request.POST.get("content", "")).strip()

Rather than using or here, our default is to provide the default as part of the get call.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Got it! Updated title to follow the same convention.

Comment thread news/views.py
@javiercoronadonarvaez javiercoronadonarvaez changed the title Story 2451: AI-Assisted Description for Links to External Blog/News Posts Story 2451: AI-Assisted Description for Content from Blog/News Posts Jun 1, 2026
@javiercoronadonarvaez javiercoronadonarvaez changed the title Story 2451: AI-Assisted Description for Content from Blog/News Posts Story 2451: AI-Assisted Description for Links to External Blog/News Posts Jun 1, 2026
@javiercoronadonarvaez javiercoronadonarvaez changed the title Story 2451: AI-Assisted Description for Links to External Blog/News Posts Story 2451: AI-Assisted Description for Blog and News Posts Jun 1, 2026
@javiercoronadonarvaez javiercoronadonarvaez linked an issue Jun 1, 2026 that may be closed by this pull request
@javiercoronadonarvaez javiercoronadonarvaez changed the title Story 2451: AI-Assisted Description for Blog and News Posts Story 2428: AI-Assisted Description for Blog and News Posts Jun 1, 2026

@herzog0 herzog0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Currently looking good to me, just finishing some tests before approval!

Comment thread news/views.py
Comment on lines +591 to +592
NOTE: intentionally not login-gated yet (local testing). This endpoint calls
a paid LLM, so add @login_required (and rate limiting) before it ships.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we address this before merging?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I was thinking of leaving this as part of a separate rate limiting PR first of all because seems more natural and second to expedite the auto generation process for Links, since this principle applies to both this PR and that of Links.

Let me know what you think about this.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@javiercoronadonarvaez If possible, I think including adding @login_required in this PR and leave the rate limiting to a separate PR would be great, especially when we don't have a ticket for rate limiting prioritized just yet 😄

Comment thread news/views.py Outdated
@ycanales

ycanales commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Looks good! Besides the existing feedback there's just this thing that I believe I also addressed in the What's New PR.

What happens is generate_description calls summarize_content(...) as a plain function, which quietly disables its retries, autoretry_for and max_retries only fire under .apply_async in a worker. On a direct call, self.retry() re-raises instead of retrying, so a transient OpenRouter blip becomes an immediate 502. Running inline also means the OpenAI call has no timeout=, so a hung upstream can tie up a web worker.

Could we extract the core into a plain helper and keep the task as a thin wrapper, so the view calls the helper directly (with its own timeout) and the background path keeps its retries? Not a blocker, just a cleanup.

It would be something like this:

def generate_summary(content, title, model, max_length=256) -> str:
    # build prompts, call OpenRouter (with timeout=), return text

@app.task(bind=True, max_retries=3, autoretry_for=(OpenAIError,))
def summarize_content(self, content, title, model, max_length=256):
    return generate_summary(content, title, model, max_length)
    ```

@ycanales ycanales self-requested a review June 2, 2026 17:35
@julhoang julhoang removed request for gregjkal and julhoang June 2, 2026 21:22

@jlchilders11 jlchilders11 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, Pending the other feedback

@javiercoronadonarvaez

Copy link
Copy Markdown
Collaborator Author

Looks good! Besides the existing feedback there's just this thing that I believe I also addressed in the What's New PR.

What happens is generate_description calls summarize_content(...) as a plain function, which quietly disables its retries, autoretry_for and max_retries only fire under .apply_async in a worker. On a direct call, self.retry() re-raises instead of retrying, so a transient OpenRouter blip becomes an immediate 502. Running inline also means the OpenAI call has no timeout=, so a hung upstream can tie up a web worker.

Could we extract the core into a plain helper and keep the task as a thin wrapper, so the view calls the helper directly (with its own timeout) and the background path keeps its retries? Not a blocker, just a cleanup.

It would be something like this:

def generate_summary(content, title, model, max_length=256) -> str:
    # build prompts, call OpenRouter (with timeout=), return text

@app.task(bind=True, max_retries=3, autoretry_for=(OpenAIError,))
def summarize_content(self, content, title, model, max_length=256):
    return generate_summary(content, title, model, max_length)
    ```

@ycanales great insight! Just addressed.

@javiercoronadonarvaez javiercoronadonarvaez force-pushed the javiercoronarv/2428-ai-assisted-desc-blog-news branch from 8c72043 to 8a4e778 Compare June 4, 2026 19:36
@javiercoronadonarvaez javiercoronadonarvaez force-pushed the javiercoronarv/2428-ai-assisted-desc-blog-news branch from 8a4e778 to 51d552c Compare June 4, 2026 21:27
Comment thread news/forms.py Outdated
Comment on lines +25 to +46
@@ -43,7 +43,7 @@ class Meta:
class NewsForm(EntryForm):
class Meta:
model = News
fields = ["title", "publish_at", "content", "image"]
fields = ["title", "publish_at", "content", "summary", "image"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi @javiercoronadonarvaez ! Can we gate the addition of the summary field to just V3? Currently this change is leaking to the legacy view as well, which is something we want to make sure not to do, as this feature is meant for V3 only

Develop Branch This Branch
Image Image

javiercoronadonarvaez and others added 22 commits June 11, 2026 07:43
- Does not display anything if the user hasn't started typing
- Display Saving text + Icon while user types and display Saved text + Icon once he's finished typing
- Corroborate auto generate description works with wysiwyg component
- Fix in accordance to GitHub Actions failure
…ated

- Add 'Hold on! We are generating a description for your content, it may take a few seconds' as placeholder in description box
…ry.summary

- Persist user-typed summary
- Skip background regeneration when set
- Truncation to comply with 1000 character limit from Design templates
- Optimize prompt
- Update pre commit config file to match black's version in requirements (26.1.0)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@javiercoronadonarvaez javiercoronadonarvaez force-pushed the javiercoronarv/2428-ai-assisted-desc-blog-news branch from 1bbccec to 0f47b2d Compare June 11, 2026 13:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

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

1952-1962: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the leading newline from the markdown demo literal.

Line 1952 currently starts dedent(""" on its own line, which injects an initial blank line into rendered markdown and can cause unintended top spacing.

Suggested fix
-            "markdown": dedent("""
-
-            ######Insert anything Required
+            "markdown": dedent("""\
+            ######Insert anything Required
...
-            """),
+            """),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/views.py` around lines 1952 - 1962, The markdown demo literal under the
"markdown" key uses dedent(""" on its own line which injects a leading blank
line; update the dedent call so the opening triple-quote immediately precedes
the content (e.g., dedent("""######Insert anything Required...) to remove the
initial newline and avoid extra top spacing when rendered, leaving the rest of
the content and indentation handling in place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@core/views.py`:
- Around line 1952-1962: The markdown demo literal under the "markdown" key uses
dedent(""" on its own line which injects a leading blank line; update the dedent
call so the opening triple-quote immediately precedes the content (e.g.,
dedent("""######Insert anything Required...) to remove the initial newline and
avoid extra top spacing when rendered, leaving the rest of the content and
indentation handling in place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 36ee5165-f1d0-4cfb-8889-0d6aaf6b5017

📥 Commits

Reviewing files that changed from the base of the PR and between 1bbccec and 0f47b2d.

📒 Files selected for processing (41)
  • .pre-commit-config.yaml
  • ak/views.py
  • config/settings.py
  • config/urls.py
  • core/context_processors.py
  • core/tests/test_managers.py
  • core/tests/test_tasks.py
  • core/views.py
  • frontend/wysiwyg-editor.js
  • gunicorn.conf.py
  • libraries/admin.py
  • libraries/forms.py
  • libraries/tests/fixtures.py
  • libraries/tests/test_commands.py
  • libraries/tests/test_tasks.py
  • mailing_list/management/commands/sync_mailinglist_stats.py
  • mailing_list/tasks.py
  • marketing/tests.py
  • news/acl.py
  • news/constants.py
  • news/forms.py
  • news/tasks.py
  • news/tests/test_forms.py
  • news/urls.py
  • news/views.py
  • slack/management/commands/clear_slack_activity.py
  • slack/management/commands/fetch_slack_activity.py
  • static/css/v3/create-post-page.css
  • static/js/v3/wysiwyg-editor.js
  • templates/includes/icon.html
  • templates/news/v3/create.html
  • users/forms.py
  • users/tests/test_urls.py
  • users/tests/test_views.py
  • users/urls.py
  • users/views.py
  • versions/releases.py
  • versions/tasks.py
  • versions/tests/test_ai_tasks.py
  • versions/tests/test_api.py
  • versions/views.py
💤 Files with no reviewable changes (17)
  • versions/tests/test_api.py
  • news/urls.py
  • versions/tests/test_ai_tasks.py
  • ak/views.py
  • versions/releases.py
  • users/tests/test_views.py
  • slack/management/commands/clear_slack_activity.py
  • slack/management/commands/fetch_slack_activity.py
  • libraries/tests/test_commands.py
  • users/urls.py
  • users/forms.py
  • gunicorn.conf.py
  • users/tests/test_urls.py
  • core/tests/test_managers.py
  • news/acl.py
  • mailing_list/tasks.py
  • core/tests/test_tasks.py
✅ Files skipped from review due to trivial changes (8)
  • news/constants.py
  • marketing/tests.py
  • versions/views.py
  • libraries/tests/test_tasks.py
  • mailing_list/management/commands/sync_mailinglist_stats.py
  • users/views.py
  • core/context_processors.py
  • libraries/tests/fixtures.py
🚧 Files skipped from review as they are similar to previous changes (8)
  • config/settings.py
  • frontend/wysiwyg-editor.js
  • news/tests/test_forms.py
  • news/tasks.py
  • templates/news/v3/create.html
  • versions/tasks.py
  • static/css/v3/create-post-page.css
  • config/urls.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@news/forms.py`:
- Line 25: Replace the mutable list literals used for Meta.fields with immutable
tuples to satisfy linting and avoid mutable class attributes: locate each Meta
class in news/forms.py where you see Meta.fields = ["title", "publish_at",
"content", "image"] (and the two other Meta.fields occurrences around lines 46
and 52) and change the RHS from a list literal to a tuple literal (use
parentheses with the same string items). Ensure you update all three Meta.fields
occurrences consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 58611065-5015-41bf-bdad-69b006511be0

📥 Commits

Reviewing files that changed from the base of the PR and between 0f47b2d and 7d0631b.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • news/forms.py
  • news/tests/test_forms.py
  • news/views.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • news/views.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@news/forms.py`:
- Line 25: Replace the mutable list literals used for Meta.fields with immutable
tuples to satisfy linting and avoid mutable class attributes: locate each Meta
class in news/forms.py where you see Meta.fields = ["title", "publish_at",
"content", "image"] (and the two other Meta.fields occurrences around lines 46
and 52) and change the RHS from a list literal to a tuple literal (use
parentheses with the same string items). Ensure you update all three Meta.fields
occurrences consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 58611065-5015-41bf-bdad-69b006511be0

📥 Commits

Reviewing files that changed from the base of the PR and between 0f47b2d and 7d0631b.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • news/forms.py
  • news/tests/test_forms.py
  • news/views.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • news/views.py
🛑 Comments failed to post (1)
news/forms.py (1)

25-25: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use tuples for Meta.fields to avoid mutable class attributes.

Line 25, Line 46, and Line 52 currently use list literals for Meta.fields, which triggers Ruff RUF012. Switching to tuples removes the mutable class-attribute risk and aligns with lint expectations.

Proposed fix
 class BlogPostForm(EntryForm):
     class Meta:
         model = BlogPost
-        fields = ["title", "publish_at", "content", "image"]
+        fields = ("title", "publish_at", "content", "image")
@@
 class NewsForm(EntryForm):
     class Meta:
         model = News
-        fields = ["title", "publish_at", "content", "image"]
+        fields = ("title", "publish_at", "content", "image")
@@
 class V3BlogPostForm(BlogPostForm):
     class Meta(BlogPostForm.Meta):
-        fields = ["title", "publish_at", "content", "summary", "image"]
+        fields = ("title", "publish_at", "content", "summary", "image")
@@
 class V3NewsForm(NewsForm):
     class Meta(NewsForm.Meta):
-        fields = ["title", "publish_at", "content", "summary", "image"]
+        fields = ("title", "publish_at", "content", "summary", "image")

Also applies to: 46-46, 52-52

🧰 Tools
🪛 Ruff (0.15.15)

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

(RUF012)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@news/forms.py` at line 25, Replace the mutable list literals used for
Meta.fields with immutable tuples to satisfy linting and avoid mutable class
attributes: locate each Meta class in news/forms.py where you see Meta.fields =
["title", "publish_at", "content", "image"] (and the two other Meta.fields
occurrences around lines 46 and 52) and change the RHS from a list literal to a
tuple literal (use parentheses with the same string items). Ensure you update
all three Meta.fields occurrences consistently.

Source: Linters/SAST tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

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

528-533: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Extract the profile-completeness guard instead of calling dispatch() directly.

Line 530 already runs the full handler chain on self. Any 200 response then falls through to Line 533 and dispatches again, so GETs render twice and 200 POST paths like invalid post types / invalid forms execute twice as well. That can duplicate messages and doubles the work on those requests.

Proposed fix
 class AllTypesCreateView(LoginRequiredMixin, TemplateView):
     template_name = "news/create.html"
     http_method_names = ["get"]  # This is a "create news" multiplexer (by news type)

+    def _profile_completeness_response(self, request):
+        """User must have a profile photo and a name to post an entry."""
+        if request.user.is_authenticated:
+            missing_data = []
+
+            if not request.user.display_name:
+                missing_data.append("your name")
+
+            if missing_data:
+                messages.warning(
+                    request, f"Please add {' and '.join(missing_data)} first."
+                )
+                return redirect("profile-account")
+        return None
+
     def dispatch(self, request, *args, **kwargs):
-        """User must have a profile photo and a name to post an entry."""
-        if request.user.is_authenticated:
-            missing_data = []
-
-            if not request.user.display_name:
-                missing_data.append("your name")
-
-            if missing_data:
-                messages.warning(
-                    request, f"Please add {' and '.join(missing_data)} first."
-                )
-                return redirect("profile-account")
-
+        if response := self._profile_completeness_response(request):
+            return response
         return super().dispatch(request, *args, **kwargs)

 class V3AllTypesCreateView(V3Mixin, AllTypesCreateView):
     v3_template_name = "news/v3/create.html"
     http_method_names = ["get", "post"]

     def dispatch(self, request, *args, **kwargs):
-        # Run AllTypesCreateView's profile-completeness guard before V3Mixin takes over.
-        response = AllTypesCreateView.dispatch(self, request, *args, **kwargs)
-        if response.status_code != 200:
-            return response
+        if response := self._profile_completeness_response(request):
+            return response
         return super().dispatch(request, *args, **kwargs)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@news/views.py` around lines 528 - 533, The current override calls
AllTypesCreateView.dispatch(self, request, ...) which runs the full handler
chain on self and then calls super().dispatch again, causing handlers to run
twice; instead extract the profile-completeness guard logic out of
AllTypesCreateView.dispatch into a dedicated helper (e.g.,
AllTypesCreateView.profile_completeness_guard or ensure_profile_complete) and
call that helper from this dispatch in news.views (returning its HttpResponse if
present), then proceed to return super().dispatch(...) only when the guard
returns None/OK; update AllTypesCreateView to expose the guard method and
replace its internal usage to avoid duplicating dispatch calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@news/views.py`:
- Around line 528-533: The current override calls
AllTypesCreateView.dispatch(self, request, ...) which runs the full handler
chain on self and then calls super().dispatch again, causing handlers to run
twice; instead extract the profile-completeness guard logic out of
AllTypesCreateView.dispatch into a dedicated helper (e.g.,
AllTypesCreateView.profile_completeness_guard or ensure_profile_complete) and
call that helper from this dispatch in news.views (returning its HttpResponse if
present), then proceed to return super().dispatch(...) only when the guard
returns None/OK; update AllTypesCreateView to expose the guard method and
replace its internal usage to avoid duplicating dispatch calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 285def1b-7fb0-496e-abf8-390af2af366f

📥 Commits

Reviewing files that changed from the base of the PR and between 92026ae and a31064b.

📒 Files selected for processing (2)
  • news/tasks.py
  • news/views.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • news/tasks.py

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.

Task: AI-Assisted Description for Blog and News Posts

5 participants