Skip to content

Issue/3350/feat/follow reorganization#4363

Open
lsabor wants to merge 10 commits intomainfrom
issue/3350/feat/follow-reorganization
Open

Issue/3350/feat/follow reorganization#4363
lsabor wants to merge 10 commits intomainfrom
issue/3350/feat/follow-reorganization

Conversation

@lsabor
Copy link
Copy Markdown
Contributor

@lsabor lsabor commented Feb 15, 2026

closes #3350

Summary by CodeRabbit

  • New Features

    • Metaculus News newsletter toggle; "Auto-follow on predict" option; weekly top-comments email toggle.
    • Localized CP-change size labels (small/medium/large) used in follow controls.
  • Improvements

    • Redesigned notification settings UI with sectioned switches, per-item save feedback, and a clearer "Site News / Keeping Up" grouping.
    • Granular default follow controls for CP-change threshold, comment frequency, milestone step, and status-change.
  • Removed

    • Legacy email/settings labels and the prior per-question notification list.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6489175b-1241-4a1e-a47b-842c7ee8f25d

📥 Commits

Reviewing files that changed from the base of the PR and between 86ed862 and d261a03.

📒 Files selected for processing (2)
  • front_end/messages/es.json
  • front_end/messages/pt.json
✅ Files skipped from review due to trivial changes (1)
  • front_end/messages/pt.json

📝 Walkthrough

Walkthrough

Reorganizes notification settings: removes the question-subscription UI, adds "Site News" and "Keeping up" follow controls, introduces auto-follow-on-predict and per-user default follow preferences, extends user/profile types and migration, updates serializers/views/API, and adds i18n keys across multiple locales.

Changes

Cohort / File(s) Summary
Localization files
front_end/messages/cs.json, front_end/messages/en.json, front_end/messages/es.json, front_end/messages/pt.json, front_end/messages/zh-TW.json, front_end/messages/zh.json
Removed legacy notification keys and added new "Site News"/"Keeping up"/follow-default translation keys plus `cpChangeSmall
Email notifications UI
front_end/src/app/(main)/accounts/settings/notifications/components/email_notifications.tsx
Replaced checkbox list with multi-section Switch layout, added SwitchRow, per-field pending/save UX, newsletter/Metaculus News toggles, auto-follow toggle, and default-follow controls persisted via profile update actions.
Question notifications removal
front_end/src/app/(main)/accounts/settings/notifications/components/question_notifications.tsx, front_end/src/app/(main)/accounts/settings/notifications/page.tsx
Deleted QuestionNotifications component and related post-subscriptions fetching; page now only renders EmailNotifications.
Frontend types & API
front_end/src/types/users.ts, front_end/src/types/notifications.ts, front_end/src/app/(main)/accounts/profile/actions.tsx, front_end/src/services/api/profile/profile.server.ts
Added new CurrentUser fields for news/auto-follow/follow-notify settings; expanded UpdateProfileRequest and updateProfileAction to accept them; reordered SubscriptionEmailType members.
Post-subscribe UI tweaks
front_end/src/components/post_subscribe/subscription_types_customisation/subscription_cp_change.tsx, front_end/src/components/ui/button_group.tsx
Localized CP-change labels to use new i18n keys and added ButtonGroup.disabled prop to propagate disabled state.
Backend user model & migration
users/models.py, users/migrations/0019_user_automatically_follow_on_predict_and_more.py
Added automatically_follow_on_predict and follow-notify default fields to User and created migration adding those fields (migration defines but does not wire a helper that would mass-update users).
Serializers & profile view
users/serializers.py, users/views.py
Added metaculus_news_subscription computed field, made follow fields writable in update serializer, and view now creates/deletes "platform" project subscription when metaculus_news_subscription provided.
Auto-follow behaviour
questions/services/forecasts.py
On forecast creation with created=True and automatically_follow_on_predict enabled, create per-post (non-global) subscriptions for CP changes, comments, milestones, and status changes if they don't already exist.
Mailing tags & tests
notifications/constants.py, tests/unit/test_questions/test_views.py
Reordered MailingTags entries (no value changes); test updated to set scheduled_resolve_time and refresh pseudo-materialized post fields before assertions.

Sequence Diagram

sequenceDiagram
    actor User
    participant Frontend as Frontend
    participant API as Backend API
    participant Service as Subscription Service
    participant DB as Database

    User->>Frontend: Submit prediction (created=true)
    Frontend->>API: POST /forecasts (prediction)
    API->>DB: Load user (includes automatically_follow_on_predict)
    DB-->>API: Return user
    alt automatically_follow_on_predict = true
        API->>Service: Query existing non-global subscriptions for post
        Service->>DB: Fetch subscriptions
        DB-->>Service: Return existing subscriptions
        alt missing subscription types
            API->>Service: Create CP-change, comment, milestone, status-change subscriptions
            Service->>DB: Insert subscriptions
            DB-->>Service: Confirm creation
        end
    end
    API-->>Frontend: Return forecast created (and subscriptions)
    Frontend-->>User: Show success and auto-follow result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • elisescu
  • hlbmtc

Poem

🐰 I hopped through toggles, switches bright,

Auto-follow woke to greet the light,
News and defaults stitched with care,
Translations scattered everywhere,
A rabbit cheers — settings take flight!

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Issue/3350/feat/follow reorganization' directly references the linked issue and clearly describes the main feature being implemented regarding follow settings and notification reorganization.
Linked Issues check ✅ Passed The PR successfully implements all primary coding objectives from issue #3350: removes question notification table [3350], adds newsletter option and Metaculus News subscription [3350], implements auto-follow-on-predict with correct defaults [3350], adds default follow notification section with CP/comments/milestone/status-change options [3350], and replaces checkboxes with switches [3350].
Out of Scope Changes check ✅ Passed All changes directly support the stated objectives: translation keys added for new UI sections, database migrations for new user preferences, serializer updates for API consistency, and UI component refactoring for the notifications settings page redesign—no extraneous modifications detected.

✏️ 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 issue/3350/feat/follow-reorganization

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.

@lsabor lsabor force-pushed the issue/3350/feat/follow-reorganization branch from 87f684b to 86bc72b Compare February 16, 2026 18:00
@github-actions
Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had recent activity. It will not be closed automatically, but please consider updating it or closing it if it is no longer relevant.

@github-actions github-actions bot added the Stale label Mar 27, 2026
@cemreinanc cemreinanc marked this pull request as ready for review March 31, 2026 22:00
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 31, 2026

🚀 Preview Environment

Your preview environment is ready!

Resource Details
🌐 Preview URL https://metaculus-pr-4363-issue-3350-feat-follow-reorgan-preview.mtcl.cc
📦 Docker Image ghcr.io/metaculus/metaculus:issue-3350-feat-follow-reorganization-d261a03
🗄️ PostgreSQL NeonDB branch preview/pr-4363-issue-3350-feat-follow-reorgan
Redis Fly Redis mtc-redis-pr-4363-issue-3350-feat-follow-reorgan

Details

  • Commit: 6789a3f027b276a683116d5f72a1f6572d606785
  • Branch: issue/3350/feat/follow-reorganization
  • Fly App: metaculus-pr-4363-issue-3350-feat-follow-reorgan

ℹ️ Preview Environment Info

Isolation:

  • PostgreSQL and Redis are fully isolated from production
  • Each PR gets its own database branch and Redis instance
  • Changes pushed to this PR will trigger a new deployment

Limitations:

  • Background workers and cron jobs are not deployed in preview environments
  • If you need to test background jobs, use Heroku staging environments

Cleanup:

  • This preview will be automatically destroyed when the PR is closed

@cemreinanc cemreinanc removed the Stale label Mar 31, 2026
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: 6

Caution

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

⚠️ Outside diff range comments (1)
front_end/src/app/(main)/accounts/settings/notifications/components/email_notifications.tsx (1)

97-123: ⚠️ Potential issue | 🟠 Major

Don’t build mailing-tag updates from the original user prop.

These switches still render from, and diff against, user.unsubscribed_mailing_tags. After the first toggle, a second change is computed from stale data and can silently undo the first choice. Keep a local unsubscribedMailingTags state here (or send per-tag deltas) and render/update from that instead.

Also applies to: 231-247, 281-290

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

In
`@front_end/src/app/`(main)/accounts/settings/notifications/components/email_notifications.tsx
around lines 97 - 123, The toggle handler and render currently read and diff
against the stale user.unsubscribed_mailing_tags prop (in
handleEmailSubscriptionChange), causing racey undos; create a local state like
unsubscribedMailingTags initialized from user.unsubscribed_mailing_tags, render
switches from that state, and in handleEmailSubscriptionChange (and the places
referenced at 231-247, 281-290) compute the new set from
unsubscribedMailingTags, immediately update unsubscribedMailingTags
optimistically and setLoadingTag, then call updateProfileAction (via
updateMailingTag / useServerAction) and reconcile on success/failure — keep
references to handleEmailSubscriptionChange, updateProfileAction,
updateMailingTag, isMailingTagPending so the UI uses the local state and not the
original user prop.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@front_end/src/app/`(main)/accounts/settings/notifications/components/email_notifications.tsx:
- Around line 163-169: updateDefaultFollow currently fire-and-forgets
runDefaultFollowUpdate which allows overlapping writes; change it to serialize
the save by awaiting the server action and managing the pending state: call
setSavingDefaultField(field) before the await, await
runDefaultFollowUpdate(updates) inside a try/catch, clear
setSavingDefaultField(null) in finally and handle/log errors in catch; also
ensure the button groups use the same saving state (setSavingDefaultField) to
disable themselves while pending (same pattern applies to the similar handlers
runDefaultMentionUpdate/runDefaultMuteUpdate and their wrappers at the other
ranges).
- Around line 46-50: CP_BUTTONS currently uses hardcoded English labels
("small","medium","large") which prevents localization; replace the static
labels with translated strings obtained via useTranslations() (or the project's
i18n helper) and construct CP_BUTTONS using those translated values so
ButtonGroup renders localized labels; update the code where CP_BUTTONS is
defined (the GroupButton<CpKey> array) to call useTranslations() inside the
component or pass translated labels in from the parent and ensure keys (value:
"small"/"medium"/"large") remain unchanged while label uses t('...') lookups.

In `@questions/services/forecasts.py`:
- Around line 292-343: The auto-follow subscription creation guarded by created
and user.automatically_follow_on_predict (the block using post = question.post,
existing_subscription, PostSubscription.SubscriptionType.* and
create_subscription) is currently placed after early returns that prevent it
from running for normal forecasts; extract these checks into a helper (e.g.,
ensure_auto_follow_subscriptions(created, user, question)) or move the entire
block to execute immediately when created is True (before the early returns),
and use the same existing_subscription and create_subscription calls to add
CP_CHANGE, NEW_COMMENTS, MILESTONE, and STATUS_CHANGE subscriptions only if they
don't already exist.
- Around line 294-343: The subscription creation is racy because you check
existence then call create_subscription which uses direct .create(), risking
IntegrityError under concurrent requests; update the logic so
create_subscription (or the underlying factory) uses get_or_create (or
update_or_create) keyed on (type, user, post, is_global) or wraps the create in
a try/except that handles IntegrityError by returning the existing
PostSubscription; specifically modify create_subscription (and any factory it
calls) to call PostSubscription.objects.get_or_create(...) for
PostSubscription.SubscriptionType values (CP_CHANGE, NEW_COMMENTS, MILESTONE,
STATUS_CHANGE) instead of relying on the external exists() checks referenced by
existing_subscription.

In `@users/migrations/0019_user_automatically_follow_on_predict_and_more.py`:
- Around line 6-23: The migrate() function that sets existing users'
automatically_follow_on_predict=False is defined but not executed; add a
migrations.RunPython step to operations so the backfill runs before/after the
AddField. Update the Migration.operations list to include
migrations.RunPython(migrate, migrations.RunPython.noop) (referencing the
migrate function name) alongside the existing migrations.AddField for
model_name="user" name="automatically_follow_on_predict" to ensure existing rows
are set to False instead of being backfilled to True.

In `@users/serializers.py`:
- Around line 191-196: The serializer in users/serializers.py that exposes the
follow-default fields ("automatically_follow_on_predict",
"follow_notify_cp_change_threshold", "follow_notify_comments_frequency",
"follow_notify_milestone_step", "follow_notify_on_status_change",
"metaculus_news_subscription") must validate incoming values before save:
replace or augment the current writable field definitions with explicit DRF
validators (e.g. serializers.IntegerField with min_value and max_value or
serializers.ChoiceField for preset enums) or add model validators on the
corresponding model fields; ensure numeric fields reject 0 and non-integers
(e.g. use IntegerField to prevent 1.5), allow null only when intended, and
include clear error messages so invalid values are rejected by the
ProfileSerializer (or the serializer class in users/serializers.py) before
persisting.

---

Outside diff comments:
In
`@front_end/src/app/`(main)/accounts/settings/notifications/components/email_notifications.tsx:
- Around line 97-123: The toggle handler and render currently read and diff
against the stale user.unsubscribed_mailing_tags prop (in
handleEmailSubscriptionChange), causing racey undos; create a local state like
unsubscribedMailingTags initialized from user.unsubscribed_mailing_tags, render
switches from that state, and in handleEmailSubscriptionChange (and the places
referenced at 231-247, 281-290) compute the new set from
unsubscribedMailingTags, immediately update unsubscribedMailingTags
optimistically and setLoadingTag, then call updateProfileAction (via
updateMailingTag / useServerAction) and reconcile on success/failure — keep
references to handleEmailSubscriptionChange, updateProfileAction,
updateMailingTag, isMailingTagPending so the UI uses the local state and not the
original user prop.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5333c05b-70d1-4882-97c0-154dfd62a0cd

📥 Commits

Reviewing files that changed from the base of the PR and between 12b0564 and 7928048.

📒 Files selected for processing (20)
  • front_end/messages/cs.json
  • front_end/messages/en.json
  • front_end/messages/es.json
  • front_end/messages/pt.json
  • front_end/messages/zh-TW.json
  • front_end/messages/zh.json
  • front_end/src/app/(main)/accounts/profile/actions.tsx
  • front_end/src/app/(main)/accounts/settings/notifications/components/email_notifications.tsx
  • front_end/src/app/(main)/accounts/settings/notifications/components/question_notifications.tsx
  • front_end/src/app/(main)/accounts/settings/notifications/page.tsx
  • front_end/src/services/api/profile/profile.server.ts
  • front_end/src/types/notifications.ts
  • front_end/src/types/users.ts
  • notifications/constants.py
  • questions/services/forecasts.py
  • tests/unit/test_questions/test_views.py
  • users/migrations/0019_user_automatically_follow_on_predict_and_more.py
  • users/models.py
  • users/serializers.py
  • users/views.py
💤 Files with no reviewable changes (1)
  • front_end/src/app/(main)/accounts/settings/notifications/components/question_notifications.tsx

Comment on lines +292 to +343
if created and user.automatically_follow_on_predict:
post = question.post
existing_subscription = post.subscriptions.filter(user=user).exclude(
is_global=True
)
if (
user.follow_notify_cp_change_threshold
and not existing_subscription.filter(
type=PostSubscription.SubscriptionType.CP_CHANGE
).exists()
):
create_subscription(
subscription_type=PostSubscription.SubscriptionType.CP_CHANGE,
user=user,
post=post,
cp_change_threshold=user.follow_notify_cp_change_threshold,
)
if (
user.follow_notify_comments_frequency
and not existing_subscription.filter(
type=PostSubscription.SubscriptionType.NEW_COMMENTS
).exists()
):
create_subscription(
subscription_type=PostSubscription.SubscriptionType.NEW_COMMENTS,
user=user,
post=post,
comments_frequency=user.follow_notify_comments_frequency,
)
if (
user.follow_notify_milestone_step
and not existing_subscription.filter(
type=PostSubscription.SubscriptionType.MILESTONE
).exists()
):
create_subscription(
subscription_type=PostSubscription.SubscriptionType.MILESTONE,
user=user,
post=post,
milestone_step=user.follow_notify_milestone_step,
)
if (
user.follow_notify_on_status_change
and not existing_subscription.filter(
type=PostSubscription.SubscriptionType.STATUS_CHANGE
).exists()
):
create_subscription(
subscription_type=PostSubscription.SubscriptionType.STATUS_CHANGE,
user=user,
post=post,
)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 31, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Don’t gate auto-follow on withdrawal-reminder eligibility.

This block is unreachable for the common “normal forecast” path. Because end_time falls back to start_time above, Line 273 returns for every forecast without an explicit end_time; Line 269 also exits when the forecast ends at or after scheduled_close_time. That means automatically_follow_on_predict only runs for a narrow subset of expiring forecasts instead of all new predictions. Please move the follow-subscription creation before those returns, or split it into a helper that runs unconditionally on created=True.

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

In `@questions/services/forecasts.py` around lines 292 - 343, The auto-follow
subscription creation guarded by created and
user.automatically_follow_on_predict (the block using post = question.post,
existing_subscription, PostSubscription.SubscriptionType.* and
create_subscription) is currently placed after early returns that prevent it
from running for normal forecasts; extract these checks into a helper (e.g.,
ensure_auto_follow_subscriptions(created, user, question)) or move the entire
block to execute immediately when created is True (before the early returns),
and use the same existing_subscription and create_subscription calls to add
CP_CHANGE, NEW_COMMENTS, MILESTONE, and STATUS_CHANGE subscriptions only if they
don't already exist.

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.

@lsabor can you please check that? It might not be for every forecast but still looked like it can block subscriptions for some cases.

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +294 to +343
existing_subscription = post.subscriptions.filter(user=user).exclude(
is_global=True
)
if (
user.follow_notify_cp_change_threshold
and not existing_subscription.filter(
type=PostSubscription.SubscriptionType.CP_CHANGE
).exists()
):
create_subscription(
subscription_type=PostSubscription.SubscriptionType.CP_CHANGE,
user=user,
post=post,
cp_change_threshold=user.follow_notify_cp_change_threshold,
)
if (
user.follow_notify_comments_frequency
and not existing_subscription.filter(
type=PostSubscription.SubscriptionType.NEW_COMMENTS
).exists()
):
create_subscription(
subscription_type=PostSubscription.SubscriptionType.NEW_COMMENTS,
user=user,
post=post,
comments_frequency=user.follow_notify_comments_frequency,
)
if (
user.follow_notify_milestone_step
and not existing_subscription.filter(
type=PostSubscription.SubscriptionType.MILESTONE
).exists()
):
create_subscription(
subscription_type=PostSubscription.SubscriptionType.MILESTONE,
user=user,
post=post,
milestone_step=user.follow_notify_milestone_step,
)
if (
user.follow_notify_on_status_change
and not existing_subscription.filter(
type=PostSubscription.SubscriptionType.STATUS_CHANGE
).exists()
):
create_subscription(
subscription_type=PostSubscription.SubscriptionType.STATUS_CHANGE,
user=user,
post=post,
)
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find the PostSubscription model definition
find . -type f -name "*.py" | xargs grep -l "class PostSubscription" | head -5

Repository: Metaculus/metaculus

Length of output: 80


🏁 Script executed:

# Find the posts/services/subscriptions.py file mentioned in the review
find . -type f -path "*/posts/services/subscriptions.py"

Repository: Metaculus/metaculus

Length of output: 96


🏁 Script executed:

# Also check the questions/services/forecasts.py file structure
find . -type f -path "*/questions/services/forecasts.py" | head -1

Repository: Metaculus/metaculus

Length of output: 96


🏁 Script executed:

# Check the PostSubscription model for unique constraints
cat -n ./posts/models.py | grep -A 30 "class PostSubscription"

Repository: Metaculus/metaculus

Length of output: 1581


🏁 Script executed:

# Check the full create_subscription function in posts/services/subscriptions.py
cat -n ./posts/services/subscriptions.py | head -100

Repository: Metaculus/metaculus

Length of output: 4471


🏁 Script executed:

# Get more of the PostSubscription model to see constraints
cat -n ./posts/models.py | sed -n '960,1010p'

Repository: Metaculus/metaculus

Length of output: 2406


🏁 Script executed:

# Find the create_subscription function definition
grep -n "def create_subscription" ./posts/services/subscriptions.py

Repository: Metaculus/metaculus

Length of output: 321


🏁 Script executed:

# Check the main create_subscription function
cat -n ./posts/services/subscriptions.py | sed -n '539,600p'

Repository: Metaculus/metaculus

Length of output: 1900


🏁 Script executed:

# Check the factory functions to see if they use create() or get_or_create()
cat -n ./posts/services/subscriptions.py | sed -n '468,490p'

Repository: Metaculus/metaculus

Length of output: 704


🏁 Script executed:

# Also check create_subscription_new_comments
cat -n ./posts/services/subscriptions.py | sed -n '452,467p'

Repository: Metaculus/metaculus

Length of output: 558


🏁 Script executed:

# And create_subscription_status_change
cat -n ./posts/services/subscriptions.py | sed -n '508,518p'

Repository: Metaculus/metaculus

Length of output: 389


🏁 Script executed:

# Look at the context around lines 294-343 in questions/services/forecasts.py
cat -n ./questions/services/forecasts.py | sed -n '280,350p'

Repository: Metaculus/metaculus

Length of output: 3097


🏁 Script executed:

# Find what function contains this subscription creation code
grep -B 30 "user.follow_notify_cp_change_threshold" ./questions/services/forecasts.py | head -50

Repository: Metaculus/metaculus

Length of output: 1516


🏁 Script executed:

# Find the function containing this subscription creation code
grep -B 50 "if created and user.automatically_follow_on_predict" ./questions/services/forecasts.py | grep "^def " | tail -1

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

# Get the full function signature and check for transaction handling
cat -n ./questions/services/forecasts.py | sed -n '200,295p' | grep -A 1 "^.*def "</parameter>

Repository: Metaculus/metaculus

Length of output: 250


🏁 Script executed:

# More precise: look for def around line 250-280
cat -n ./questions/services/forecasts.py | sed -n '200,300p'

Repository: Metaculus/metaculus

Length of output: 4553


🏁 Script executed:

# Check if update_forecast_notification has transaction decorator
cat -n ./questions/services/forecasts.py | sed -n '230,245p'

Repository: Metaculus/metaculus

Length of output: 838


🏁 Script executed:

# Find where update_forecast_notification is called from
grep -n "update_forecast_notification" ./questions/services/forecasts.py

Repository: Metaculus/metaculus

Length of output: 262


🏁 Script executed:

# Check the create_forecast function for transaction handling
grep -B 10 "update_forecast_notification" ./questions/services/forecasts.py | grep -E "(def |@transaction|atomic)" | head -20

Repository: Metaculus/metaculus

Length of output: 174


🏁 Script executed:

# Check the create_forecast function around line 172 for transaction handling
cat -n ./questions/services/forecasts.py | sed -n '100,180p'

Repository: Metaculus/metaculus

Length of output: 3409


🏁 Script executed:

# Search for transaction decorators in the file
grep -n "@transaction\|@atomic\|atomic(" ./questions/services/forecasts.py

Repository: Metaculus/metaculus

Length of output: 97


🏁 Script executed:

# Check what transaction.atomic() is being used for at line 494
cat -n ./questions/services/forecasts.py | sed -n '485,510p'

Repository: Metaculus/metaculus

Length of output: 1275


🏁 Script executed:

# Check if create_forecast_bulk has any transaction handling
cat -n ./questions/services/forecasts.py | sed -n '160,175p'

Repository: Metaculus/metaculus

Length of output: 676


🏁 Script executed:

# Look for imports of transaction at the top of the file
head -50 ./questions/services/forecasts.py | grep -i transaction

Repository: Metaculus/metaculus

Length of output: 96


Use get_or_create to make subscription creation idempotent.

The code checks for existing subscriptions with .exists() then calls create_subscription() which invokes .objects.create() directly. PostSubscription has a unique constraint on (type, user_id, post_id, is_global). Two concurrent forecast submissions can both pass the existence check and race to insert, causing one to fail with IntegrityError after the forecast row has already been persisted.

Replace the existence checks and create_subscription() calls with get_or_create() logic in the subscription factory functions, or use ignore_conflicts=True in a bulk operation, to eliminate the race.

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

In `@questions/services/forecasts.py` around lines 294 - 343, The subscription
creation is racy because you check existence then call create_subscription which
uses direct .create(), risking IntegrityError under concurrent requests; update
the logic so create_subscription (or the underlying factory) uses get_or_create
(or update_or_create) keyed on (type, user, post, is_global) or wraps the create
in a try/except that handles IntegrityError by returning the existing
PostSubscription; specifically modify create_subscription (and any factory it
calls) to call PostSubscription.objects.get_or_create(...) for
PostSubscription.SubscriptionType values (CP_CHANGE, NEW_COMMENTS, MILESTONE,
STATUS_CHANGE) instead of relying on the external exists() checks referenced by
existing_subscription.

Comment on lines +6 to +23
def migrate(apps, schema_editor):
# automatically follow on predict should be set to false for existing users
User = apps.get_model("users", "User")
User.objects.all().update(automatically_follow_on_predict=False)


class Migration(migrations.Migration):

dependencies = [
("users", "0018_add_auth_revoked_at"),
]

operations = [
migrations.AddField(
model_name="user",
name="automatically_follow_on_predict",
field=models.BooleanField(default=True),
),
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 31, 2026

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:

# First, locate and examine the migration file
find . -name "0019_user_automatically_follow_on_predict_and_more.py" -type f

Repository: Metaculus/metaculus

Length of output: 135


🏁 Script executed:

# Also search for the users migrations directory to understand context
fd "users.*migrations" -t d | head -5

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

# Look at the users migrations directory
ls -la users/migrations/ | grep -E "^-.*\.py$" | tail -20

Repository: Metaculus/metaculus

Length of output: 1715


🏁 Script executed:

cat -n ./users/migrations/0019_user_automatically_follow_on_predict_and_more.py

Repository: Metaculus/metaculus

Length of output: 2255


🏁 Script executed:

# Search for related PRs or issues mentioning this migration or field
rg "automatically_follow_on_predict" --type py -l | head -20

Repository: Metaculus/metaculus

Length of output: 202


🏁 Script executed:

# Check the User model to see the field definition
rg "automatically_follow_on_predict" -A 3 -B 1 users/models.py

Repository: Metaculus/metaculus

Length of output: 303


🏁 Script executed:

# Search for any follow-up PR numbers or related context
rg "4507|4499" --type py,md | head -20

Repository: Metaculus/metaculus

Length of output: 92


🏁 Script executed:

# Search for any follow-up PR references or TODO comments related to this field
rg "automatically_follow_on_predict" -B 5 -A 5 --type py | grep -E "(TODO|FIXME|follow-up|PR|4[0-9]{3})"

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

# Check git log for this migration to see if there's a follow-up commit
cd . && git log --oneline --all -- users/migrations/0019_user_automatically_follow_on_predict_and_more.py | head -10

Repository: Metaculus/metaculus

Length of output: 77


🏁 Script executed:

# Search for any references to the migration or field in PR/issue context
rg "RunPython" users/migrations/0019_user_automatically_follow_on_predict_and_more.py

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

# Check how the field is used in the forecasts service
rg "automatically_follow_on_predict" -B 3 -A 3 questions/services/forecasts.py

Repository: Metaculus/metaculus

Length of output: 279


🏁 Script executed:

# Check the commit that touches this migration
git log --pretty=format:"%h %s" --all -- users/migrations/0019_user_automatically_follow_on_predict_and_more.py | head -5

Repository: Metaculus/metaculus

Length of output: 77


🏁 Script executed:

# Check if there's a subsequent migration that might handle this
ls -la users/migrations/ | grep "^-" | tail -5

Repository: Metaculus/metaculus

Length of output: 478


🏁 Script executed:

# Search for any RunPython usage in user migrations
rg "RunPython" users/migrations/ | head -10

Repository: Metaculus/metaculus

Length of output: 577


🏁 Script executed:

# Check the git diff or any follow-up changes to understand intent
git log -p --all -S "automatically_follow_on_predict" -- users/migrations/ | head -100

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

# Check the commit details to see if there's any mention of follow-up
git show 7928048d --quiet || git show 7928048d | head -50

Repository: Metaculus/metaculus

Length of output: 202


🏁 Script executed:

# Search for any TODO, FIXME, or comments about deferred backfill in this migration
cat -n ./users/migrations/0019_user_automatically_follow_on_predict_and_more.py | grep -E "(TODO|FIXME|defer|follow-up|PR)"

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

# Check if there's a follow-up migration file or any mention in recent PRs
fd "0020" users/migrations/

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

# Let me check one of the other migrations to see the exact pattern
cat -n users/migrations/0002_user_is_onboarding_complete.py

Repository: Metaculus/metaculus

Length of output: 1011


🏁 Script executed:

# Also check 0017 to see another example
cat -n users/migrations/0017_user_is_primary_bot_user_owner_and_more.py | head -50

Repository: Metaculus/metaculus

Length of output: 2223


🏁 Script executed:

# Verify the RunPython pattern in 0017 fully
cat -n users/migrations/0017_user_is_primary_bot_user_owner_and_more.py | tail -30

Repository: Metaculus/metaculus

Length of output: 1545


Add the missing RunPython step to wire the migration helper into operations.

The migrate() function is defined but never executed because it is not registered in operations. Without migrations.RunPython(migrate, migrations.RunPython.noop), the AddField(default=True) will backfill automatically_follow_on_predict=True for all existing users, inverting the intent stated in the helper comment ("automatically follow on predict should be set to false for existing users").

This pattern is established throughout the user migrations: 0002_user_is_onboarding_complete.py and 0017_user_is_primary_bot_user_owner_and_more.py both follow the same structure—define a migration function, then wire it with migrations.RunPython() in operations.

Suggested fix
     operations = [
         migrations.AddField(
             model_name="user",
             name="automatically_follow_on_predict",
             field=models.BooleanField(default=True),
         ),
+        migrations.RunPython(migrate, migrations.RunPython.noop),
         migrations.AddField(
             model_name="user",
             name="follow_notify_comments_frequency",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@users/migrations/0019_user_automatically_follow_on_predict_and_more.py`
around lines 6 - 23, The migrate() function that sets existing users'
automatically_follow_on_predict=False is defined but not executed; add a
migrations.RunPython step to operations so the backfill runs before/after the
AddField. Update the Migration.operations list to include
migrations.RunPython(migrate, migrations.RunPython.noop) (referencing the
migrate function name) alongside the existing migrations.AddField for
model_name="user" name="automatically_follow_on_predict" to ensure existing rows
are set to False instead of being backfilled to True.

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.

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +191 to +196
"automatically_follow_on_predict",
"follow_notify_cp_change_threshold",
"follow_notify_comments_frequency",
"follow_notify_milestone_step",
"follow_notify_on_status_change",
"metaculus_news_subscription",
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

Validate the new follow-default fields before saving them.

These settings are now writable through the profile API, but the serializer accepts any integer/float/null for them. That means clients can persist invalid defaults like follow_notify_comments_frequency=0 or follow_notify_milestone_step=1.5, and those bad values will later be copied into new subscriptions. Please add explicit serializer validation or model validators for the allowed ranges/presets.

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

In `@users/serializers.py` around lines 191 - 196, The serializer in
users/serializers.py that exposes the follow-default fields
("automatically_follow_on_predict", "follow_notify_cp_change_threshold",
"follow_notify_comments_frequency", "follow_notify_milestone_step",
"follow_notify_on_status_change", "metaculus_news_subscription") must validate
incoming values before save: replace or augment the current writable field
definitions with explicit DRF validators (e.g. serializers.IntegerField with
min_value and max_value or serializers.ChoiceField for preset enums) or add
model validators on the corresponding model fields; ensure numeric fields reject
0 and non-integers (e.g. use IntegerField to prevent 1.5), allow null only when
intended, and include clear error messages so invalid values are rejected by the
ProfileSerializer (or the serializer class in users/serializers.py) before
persisting.

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

♻️ Duplicate comments (1)
front_end/src/app/(main)/accounts/settings/notifications/components/email_notifications.tsx (1)

173-179: ⚠️ Potential issue | 🟠 Major

Race condition: savingDefaultField is never cleared after save completes.

updateDefaultFollow sets savingDefaultField but never resets it to null after runDefaultFollowUpdate finishes. This means:

  1. The pending indicator may stay visible indefinitely after isDefaultFollowPending becomes false
  2. Rapid clicks on different fields can issue overlapping writes where an earlier slow response could overwrite a later user selection

Consider either:

  • Awaiting the action and clearing state in a finally block, or
  • Disabling the controls while any default-follow save is pending
🔧 Suggested fix pattern
 const updateDefaultFollow = useCallback(
-  (field: string, updates: Parameters<typeof updateProfileAction>[0]) => {
+  async (field: string, updates: Parameters<typeof updateProfileAction>[0]) => {
     setSavingDefaultField(field);
-    runDefaultFollowUpdate(updates);
+    try {
+      await runDefaultFollowUpdate(updates);
+    } finally {
+      setSavingDefaultField(null);
+    }
   },
   [runDefaultFollowUpdate]
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/app/`(main)/accounts/settings/notifications/components/email_notifications.tsx
around lines 173 - 179, The callback updateDefaultFollow sets savingDefaultField
but never clears it, causing a stuck pending indicator and possible race
overwrites; change updateDefaultFollow (and/or runDefaultFollowUpdate usage) to
await the save action and clear setSavingDefaultField(null) in a finally block
(or alternatively disable controls when isDefaultFollowPending is true) so
savingDefaultField is reset after the save completes; reference the
savingDefaultField state, updateDefaultFollow function, and
runDefaultFollowUpdate action when making this change.
🧹 Nitpick comments (1)
front_end/src/app/(main)/accounts/settings/notifications/components/email_notifications.tsx (1)

101-104: Consider reverting optimistic state on error.

handleNewsChange updates local state optimistically before the server action completes. If updateProfileAction fails, the UI will show the new state but the server will retain the old value.

💡 Pattern for reverting on error
 const handleNewsChange = useCallback(async (checked: boolean) => {
+  const previousValue = newsSubscribed;
   setNewsSubscribed(checked);
-  await updateProfileAction({ metaculus_news_subscription: checked });
+  try {
+    await updateProfileAction({ metaculus_news_subscription: checked });
+  } catch {
+    setNewsSubscribed(previousValue);
+  }
 }, []);

Note: A similar pattern could apply to handleAutoFollowChange at lines 139-142.

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

In
`@front_end/src/app/`(main)/accounts/settings/notifications/components/email_notifications.tsx
around lines 101 - 104, handleNewsChange currently sets local state
optimistically via setNewsSubscribed(checked) before awaiting
updateProfileAction; change it to save the previous value, call
updateProfileAction first (or await it) and only setNewsSubscribed(checked) on
success, and if the action throws revert to the saved previous value (or set it
in a catch block); apply the same pattern to handleAutoFollowChange (preserve
previous auto-follow state, await updateProfileAction, then update local state
or revert on error) so the UI reflects server state on failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@front_end/messages/pt.json`:
- Around line 678-680: The CP-change size labels use masculine singular
Portuguese forms but must be feminine plural to agree with surrounding words
like "alterações"/"mudanças"; update the translation values for the keys
cpChangeSmall, cpChangeMedium, and cpChangeLarge to the feminine plural forms
("pequenas", "médias", "grandes") so the CP-change selector and sentence
templates render correctly.

---

Duplicate comments:
In
`@front_end/src/app/`(main)/accounts/settings/notifications/components/email_notifications.tsx:
- Around line 173-179: The callback updateDefaultFollow sets savingDefaultField
but never clears it, causing a stuck pending indicator and possible race
overwrites; change updateDefaultFollow (and/or runDefaultFollowUpdate usage) to
await the save action and clear setSavingDefaultField(null) in a finally block
(or alternatively disable controls when isDefaultFollowPending is true) so
savingDefaultField is reset after the save completes; reference the
savingDefaultField state, updateDefaultFollow function, and
runDefaultFollowUpdate action when making this change.

---

Nitpick comments:
In
`@front_end/src/app/`(main)/accounts/settings/notifications/components/email_notifications.tsx:
- Around line 101-104: handleNewsChange currently sets local state
optimistically via setNewsSubscribed(checked) before awaiting
updateProfileAction; change it to save the previous value, call
updateProfileAction first (or await it) and only setNewsSubscribed(checked) on
success, and if the action throws revert to the saved previous value (or set it
in a catch block); apply the same pattern to handleAutoFollowChange (preserve
previous auto-follow state, await updateProfileAction, then update local state
or revert on error) so the UI reflects server state on failure.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9b25e10d-2703-4272-b680-d9800ef53b03

📥 Commits

Reviewing files that changed from the base of the PR and between 7928048 and 86ed862.

📒 Files selected for processing (9)
  • front_end/messages/cs.json
  • front_end/messages/en.json
  • front_end/messages/es.json
  • front_end/messages/pt.json
  • front_end/messages/zh-TW.json
  • front_end/messages/zh.json
  • front_end/src/app/(main)/accounts/settings/notifications/components/email_notifications.tsx
  • front_end/src/components/post_subscribe/subscription_types_customisation/subscription_cp_change.tsx
  • front_end/src/components/ui/button_group.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • front_end/messages/es.json
  • front_end/messages/zh-TW.json
  • front_end/messages/zh.json

Copy link
Copy Markdown
Contributor

@ncarazon ncarazon left a comment

Choose a reason for hiding this comment

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

LGTM with suggestions

Comment on lines +101 to +104
const handleNewsChange = useCallback(async (checked: boolean) => {
setNewsSubscribed(checked);
await updateProfileAction({ metaculus_news_subscription: checked });
}, []);
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.

State is set optimistically before the server action completes, but there's no rollback on failure. If updateProfileAction throws, the switch shows the wrong state. Same applies to the default follow toggles. Either wrap in try/catch with rollback, or set state only after success.

Comment on lines +164 to +169
const handleDefaultFollowUpdate = useCallback(
async (updates: Parameters<typeof updateProfileAction>[0]) => {
await updateProfileAction(updates);
},
[]
);
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.

handleDefaultFollowUpdate is just async (updates) => { await updateProfileAction(updates); } - it's an unnecessary wrapper. You could pass updateProfileAction directly to useServerAction.

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.

Default question follow settings and notification settings reorganisation

3 participants