Skip to content

feat(email): add project feedback check-in outreach flow#142

Merged
gregagi merged 2 commits intomainfrom
feat/project-feedback-checkin-email
Feb 24, 2026
Merged

feat(email): add project feedback check-in outreach flow#142
gregagi merged 2 commits intomainfrom
feat/project-feedback-checkin-email

Conversation

@gregagi
Copy link
Copy Markdown
Collaborator

@gregagi gregagi commented Feb 23, 2026

Summary by CodeRabbit

  • New Features

    • Introduced automated project feedback check-in emails targeting users with verified email addresses and existing projects, sent daily to recent registrants.
  • Tests

    • Added comprehensive test coverage for project feedback check-in email workflow, including eligibility verification and email delivery confirmation.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 23, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a new PROJECT_FEEDBACK_CHECKIN email workflow to the system. It adds the email type choice, creates a database migration, implements scheduling logic to identify eligible profiles, adds an email sending function with tracking, and includes comprehensive test coverage for the new feature.

Changes

Cohort / File(s) Summary
Email Type Configuration
core/choices.py, core/migrations/0050_alter_emailsent_email_type.py
Added PROJECT_FEEDBACK_CHECKIN email type to the EmailType enum and updated the EmailSent model's email_type field with new choices via migration.
Email Scheduling & Sending
core/scheduled_tasks.py, core/tasks.py
Introduced schedule_project_feedback_checkin_emails() to identify recently-joined profiles with verified emails and projects, and send_project_feedback_checkin_email(profile_id) to send the feedback check-in email with tracking and deduplication logic.
Tests
core/tests/test_project_feedback_checkin_emails.py
Added tests for email sending (plain-text composition, tracking, deduplication) and scheduling logic (filtering by join date, email verification, and project existence).

Sequence Diagram

sequenceDiagram
    participant Scheduler as Scheduled Task
    participant DB as Database
    participant Queue as Task Queue
    participant Email as Email Service
    participant Tracking as Tracking Task

    Scheduler->>DB: Query eligible profiles (2-day window, verified, has project)
    DB-->>Scheduler: Return eligible profiles
    Scheduler->>Scheduler: Filter out already-sent emails
    loop For each eligible profile
        Scheduler->>Queue: Enqueue send_project_feedback_checkin_email
    end
    Queue->>Email: Dequeue and send plain-text email
    Email-->>Email: Compose email content
    Email->>Email: Send via EmailMessage
    Email->>Queue: Enqueue track_email_sent task
    Queue->>Tracking: Dequeue and record EmailSent entry
    Tracking->>DB: Store email sent record
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PR #120: Modifies the same EmailType TextChoices enum in core/choices.py and updates the corresponding EmailSent model field choices via migration.
  • PR #126: Updates EmailType enum and adds similar lifecycle email scheduling and sending functions (schedule_*_emails() and send_*_email() patterns).

Poem

🐰 A rabbit hops through feedback fields,
Checking on projects that each profile builds,
New emails scheduled, precise and true,
Tracking each message through and through! 📧✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% 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 describes the main change: adding a project feedback check-in outreach email flow, which is reflected across all modified files (choices, migrations, scheduled tasks, tasks, and tests).

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/project-feedback-checkin-email

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

❤️ Share

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

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 23, 2026

Greptile Summary

This PR adds a new automated email outreach flow that sends personalized feedback check-ins to users who have registered within the past 2 days and created at least one project. The email is sent from rasul@tuxseo.com as a plain-text message asking for feedback, suggestions, and offering help.

Key changes:

  • Added PROJECT_FEEDBACK_CHECKIN email type to track these emails
  • Implemented schedule_project_feedback_checkin_emails() scheduled task that filters for verified users registered in last 2 days with projects
  • Implemented send_project_feedback_checkin_email() task that sends the plain-text email
  • Added comprehensive test coverage for both scheduling and sending functions
  • Migration added to update EmailSent model with the new email type

The implementation follows the existing pattern used for CREATE_PROJECT_REMINDER emails, with proper deduplication checks and verification filtering.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation follows established patterns in the codebase, includes comprehensive test coverage, and has proper error handling. All edge cases are covered with validation checks and deduplication logic.
  • No files require special attention

Important Files Changed

Filename Overview
core/scheduled_tasks.py Added schedule_project_feedback_checkin_emails to find and schedule emails for verified users who registered in the last 2 days and have projects
core/tasks.py Added send_project_feedback_checkin_email function that sends plain-text feedback check-in email from rasul@tuxseo.com
core/tests/test_project_feedback_checkin_emails.py Added comprehensive test coverage for both the scheduling function and the email sending function

Last reviewed commit: d343a13

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

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

🧹 Nitpick comments (3)
core/scheduled_tasks.py (1)

380-386: Extra N+1 query in log statement – use the annotated project_count

profile.projects.count() (line 385) issues an additional query per profile. The queryset already annotates project_count (line 354), so profile.project_count can be used directly.

♻️ Proposed fix
         logger.info(
             "[Schedule Project Feedback Check-in] Scheduling email",
             profile_id=profile.id,
             user_email=profile.user.email,
             days_since_registration=(now - profile.user.date_joined).days,
-            project_count=profile.projects.count(),
+            project_count=profile.project_count,
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/scheduled_tasks.py` around lines 380 - 386, The log call using
logger.info currently triggers an extra DB query by calling
profile.projects.count(); change that to use the already-annotated attribute
profile.project_count instead (i.e., replace profile.projects.count() with
profile.project_count in the logger.info call) so no additional query is
executed when logging.
core/tests/test_project_feedback_checkin_emails.py (1)

17-81: Missing test for the "profile has no projects" skip path

send_project_feedback_checkin_email returns early when profile.projects.exists() is False (line 1604–1610 in core/tasks.py), but this branch has no test. As per coding guidelines, write tests for each function and each code path.

✅ Suggested additional test
`@override_settings`(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend")
`@patch`("core.tasks.async_task")
def test_skips_when_profile_has_no_projects(self, mock_async_task):
    user = User.objects.create_user(
        username="no-project-user",
        email="no-project@example.com",
        password="password123",
    )
    profile = Profile.objects.get(user=user)

    result = send_project_feedback_checkin_email(profile.id)

    assert result == f"Profile {profile.id} has no projects, skipping"
    assert len(mail.outbox) == 0
    mock_async_task.assert_not_called()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/tests/test_project_feedback_checkin_emails.py` around lines 17 - 81, Add
a new test to cover the "profile has no projects" early return in
send_project_feedback_checkin_email: create a user and Profile (e.g., username
"no-project-user"), do NOT create any Project for that profile, call
send_project_feedback_checkin_email(profile.id), assert the returned message
equals f"Profile {profile.id} has no projects, skipping", assert mail.outbox is
empty, and assert the patched async_task (patch "core.tasks.async_task") was not
called; place this test alongside the other tests in
TestSendProjectFeedbackCheckinEmail in
core/tests/test_project_feedback_checkin_emails.py.
core/tasks.py (1)

1594-1647: Race condition: dedup check and tracking are not atomic

The EmailSent.objects.filter(...).exists() check (line 1594) and the async track_email_sent task (line 1641) have a TOCTOU window. Two concurrent workers can both pass the exists() check before either tracking record is written, resulting in duplicate emails.

The established pattern in check_and_send_project_setup_complete_email (lines 427–444) avoids this with an atomic get_or_create:

♻️ Proposed fix – atomic idempotency guard
+    from django.db import transaction
+
-    if EmailSent.objects.filter(
-        profile=profile, email_type=EmailType.PROJECT_FEEDBACK_CHECKIN
-    ).exists():
-        logger.info(
-            "[Send Project Feedback Check-in Email] Email already sent to this profile, skipping",
-            profile_id=profile_id,
-            user_email=user.email,
-        )
-        return f"Email already sent to {user.email}, skipping"
+    with transaction.atomic():
+        email_sent_record, created = EmailSent.objects.get_or_create(
+            profile=profile,
+            email_type=EmailType.PROJECT_FEEDBACK_CHECKIN,
+            defaults={"email_address": user.email},
+        )
+        if not created:
+            logger.info(
+                "[Send Project Feedback Check-in Email] Email already sent to this profile, skipping",
+                profile_id=profile_id,
+                user_email=user.email,
+            )
+            return f"Email already sent to {user.email}, skipping"

Then remove the async_task("core.tasks.track_email_sent", ...) call since the record is now created synchronously before sending.

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

In `@core/tasks.py` around lines 1594 - 1647, The exists() race can be fixed by
creating the EmailSent record atomically before sending: replace the
EmailSent.objects.filter(...).exists() check with an
EmailSent.objects.get_or_create(profile=profile,
email_type=EmailType.PROJECT_FEEDBACK_CHECKIN, defaults={...}) pattern (follow
the same approach used in check_and_send_project_setup_complete_email) so only
one worker proceeds to send; if get_or_create returns created=False, log and
return as before. After switching to the synchronous get_or_create guard, remove
the async_task("core.tasks.track_email_sent", ...) call because the EmailSent
row is created synchronously and serves as the idempotency marker.
🤖 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/tasks.py`:
- Line 1636: Replace the hardcoded from_email="rasul@tuxseo.com" with a named
constant (UPPER_CASE) and use that constant in the function that sends the
email; add CHECKIN_EMAIL_FROM = "rasul@tuxseo.com" at the top of tasks.py (or
define it in settings.py and import it) and update the email send call to use
CHECKIN_EMAIL_FROM (or fallback to settings.DEFAULT_FROM_EMAIL if desired) so
the value is not embedded as a bare string.

---

Nitpick comments:
In `@core/scheduled_tasks.py`:
- Around line 380-386: The log call using logger.info currently triggers an
extra DB query by calling profile.projects.count(); change that to use the
already-annotated attribute profile.project_count instead (i.e., replace
profile.projects.count() with profile.project_count in the logger.info call) so
no additional query is executed when logging.

In `@core/tasks.py`:
- Around line 1594-1647: The exists() race can be fixed by creating the
EmailSent record atomically before sending: replace the
EmailSent.objects.filter(...).exists() check with an
EmailSent.objects.get_or_create(profile=profile,
email_type=EmailType.PROJECT_FEEDBACK_CHECKIN, defaults={...}) pattern (follow
the same approach used in check_and_send_project_setup_complete_email) so only
one worker proceeds to send; if get_or_create returns created=False, log and
return as before. After switching to the synchronous get_or_create guard, remove
the async_task("core.tasks.track_email_sent", ...) call because the EmailSent
row is created synchronously and serves as the idempotency marker.

In `@core/tests/test_project_feedback_checkin_emails.py`:
- Around line 17-81: Add a new test to cover the "profile has no projects" early
return in send_project_feedback_checkin_email: create a user and Profile (e.g.,
username "no-project-user"), do NOT create any Project for that profile, call
send_project_feedback_checkin_email(profile.id), assert the returned message
equals f"Profile {profile.id} has no projects, skipping", assert mail.outbox is
empty, and assert the patched async_task (patch "core.tasks.async_task") was not
called; place this test alongside the other tests in
TestSendProjectFeedbackCheckinEmail in
core/tests/test_project_feedback_checkin_emails.py.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e1e270 and 9b6cbad.

📒 Files selected for processing (5)
  • core/choices.py
  • core/migrations/0050_alter_emailsent_email_type.py
  • core/scheduled_tasks.py
  • core/tasks.py
  • core/tests/test_project_feedback_checkin_emails.py

Comment thread core/tasks.py
email = EmailMessage(
subject=subject,
body=plain_text,
from_email="rasul@tuxseo.com",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded from_email – extract to a named constant

from_email="rasul@tuxseo.com" is the only function in this file not using settings.DEFAULT_FROM_EMAIL. Even if the personal sender address is intentional, embedding it as a bare string makes it fragile and inconsistent with the rest of the module.

🛠️ Proposed fix

At the top of tasks.py (or in settings.py):

CHECKIN_EMAIL_FROM = "rasul@tuxseo.com"

Then in the function:

         email = EmailMessage(
             subject=subject,
             body=plain_text,
-            from_email="rasul@tuxseo.com",
+            from_email=settings.CHECKIN_EMAIL_FROM,
             to=[user.email],
         )

As per coding guidelines, extract unchanging values into constants using UPPER_CASE naming.

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

In `@core/tasks.py` at line 1636, Replace the hardcoded
from_email="rasul@tuxseo.com" with a named constant (UPPER_CASE) and use that
constant in the function that sends the email; add CHECKIN_EMAIL_FROM =
"rasul@tuxseo.com" at the top of tasks.py (or define it in settings.py and
import it) and update the email send call to use CHECKIN_EMAIL_FROM (or fallback
to settings.DEFAULT_FROM_EMAIL if desired) so the value is not embedded as a
bare string.

@gregagi gregagi merged commit b59766a into main Feb 24, 2026
4 checks passed
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