Skip to content

Fixed #339 -- Added mass mailing for User model in admin panel#370

Open
sathwikshetty33 wants to merge 1 commit intodjangoindia:mainfrom
sathwikshetty33:issue-339
Open

Fixed #339 -- Added mass mailing for User model in admin panel#370
sathwikshetty33 wants to merge 1 commit intodjangoindia:mainfrom
sathwikshetty33:issue-339

Conversation

@sathwikshetty33
Copy link
Copy Markdown

@sathwikshetty33 sathwikshetty33 commented Jun 18, 2025

Closes #339

Add functionality to send bulk emails to users from Django admin panel with background task processing

Changes

  • Activated the UserAdmin class with enhanced functionality
  • Added send_email_to_selected_users action to User admin for bulk email operations
  • Enhanced list_filter with is_password_expired and is_onboarded fields for better user management
  • Implemented send_email_view method with custom URL routing for email form handling
  • Created send_mass_mail_task_users shared task for background email processing with batch support
  • Added email validation logic to skip users without valid email addresses
  • Integrated session-based user ID management for selected users
  • Added proper error handling and user feedback messages

Type of change

  • Feature update
  • Bug fix
  • Breaking change
  • Documentation update

Flags

  • Background task requires Celery worker to be running for email processing
  • Email functionality uses DEFAULT_FROM_EMAIL setting from Django configuration
  • Batch processing implemented to handle large user lists (100 emails per batch)
  • Email validation prevents sending to users with empty/null email addresses

Demo

  • Admin action "Send email to selected users" appears in User admin dropdown
  • Email form renders at /admin/db/user/send_email/ with subject and message fields
  • Success/warning messages display based on email processing results
  • Background task processes emails asynchronously without blocking admin interface

How has this been tested?

  • Verified email action appears in User admin interface
  • Tested email form submission with valid user selections
  • Confirmed background task processes emails successfully with Celery worker
  • Tested error handling for users without email addresses
  • Verified new filter options (is_password_expired, is_onboarded) work correctly
  • Confirmed session handling maintains selected user IDs properly

Author Checklist

  • Code has been commented, particularly in hard-to-understand areas
  • Changes generate no new warnings
  • Vital changes have been captured in unit and/or integration tests
  • New and existing unit tests pass locally with my changes
  • Documentation has been extended, if necessary
  • Merging to main from fork:issue-339

Additional context

  • Implementation follows the same pattern as existing EventUserRegistrationAdmin email functionality for consistency
  • Background task send_mass_mail_task_users mirrors the existing send_mass_mail_task but specifically handles User model email sending
  • Enhanced admin filters improve user management capabilities for administrators
  • Email validation prevents common errors and provides better user experience with appropriate feedback messages

Summary by CodeRabbit

  • New Features

    • Send bulk emails to selected users from the Admin via a compose page with validation, background queuing, and success/error feedback.
  • Improvements

    • Added “Password expired” and “Onboarded” indicators to Admin filters and Permissions to help segment users for communication.

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Welcome to Django India! 🎉🇮🇳

We're so happy to see you contribute. Your efforts mean a lot to us and we're excited to have you on this journey.

Before we proceed, please take a moment to review our contribution guide. It's packed with all the information you need to make your contribution seamless.

If you're fixing an issue from the Django India issue tracker, remember to get it assigned to you before you acutally start working on it.

If you ever need help or just want to chat, the Django India discord server is always open. We're here for you!

And while you're here, don't forget to ⭐ star our repo to show your support! It helps the community grow.

Thank you for being a part of Django India. Together, let's make an amazing community! 🚀

@DevilsAutumn
Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 28, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 28, 2025

Walkthrough

Adds a Celery task to send mass emails to arbitrary user email lists and integrates an admin action, custom view and route in UserAdmin to compose and queue emails to selected users. Also exposes is_password_expired and is_onboarded in UserAdmin filters and Permissions fieldset.

Changes

Cohort / File(s) Summary
Background tasks: user mass mailer
backend/djangoindia/bg_tasks/event_tasks.py
Adds send_mass_mail_task_users(emails, **kwargs): validates iterable emails, batches into 100-item chunks, calls send_mass_mail, logs errors and re-raises; does not update EventCommunication or persistent records.
Admin: user email action, view, and filters
backend/djangoindia/db/admin.py
Imports send_mass_mail_task_users, adds send_email_to_selected_users to actions, implements get_urls() to expose send_email/ and send_email_view to render/process an email form, queue emails via the task, handle empty/invalid selections and messages; exposes is_password_expired and is_onboarded in list_filter and Permissions fieldset.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Admin as Admin User
    participant AdminSite as Django Admin (UserAdmin)
    participant SendView as send_email_view
    participant Celery as Celery Queue
    participant Task as send_mass_mail_task_users
    participant Mailer as Email Backend

    Admin->>AdminSite: select users + action "Send email"
    AdminSite->>SendView: show compose form (/send_email/)
    Admin->>SendView: POST subject/body
    SendView->>Celery: queue Task.delay(emails, kwargs)
    Celery->>Task: execute task
    Task->>Mailer: send_mass_mail(batched emails)
    Mailer-->>Task: results (success/errors)
    Task-->>Celery: log outcome / raise on error
    SendView-->>Admin: display success or error message
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I twitch my ears at queues that hum,
I batch the notes and send them some,
From admin form to celery drum,
I thump, “Queued!” — the emails come. 🥕✉️

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The pull request introduces two new list filters and fieldset attributes (is_password_expired and is_onboarded) in the UserAdmin class that are unrelated to the linked issue’s objective of adding bulk email functionality, representing features outside the scope of sending mail to users from the admin panel. Remove the unrelated filters and fieldset modifications for password expiration and onboarding status or extract them into a separate feature pull request to keep this change focused on the email functionality.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly captures the primary change—adding mass mailing support for the User model in the admin panel—and directly aligns with the pull request’s objectives while remaining concise and clear.
Linked Issues Check ✅ Passed The changes implement the linked issue’s objectives by adding a Django admin action and custom view for sending emails to selected users, leveraging a new Celery task (send_mass_mail_task_users) that processes emails asynchronously in batches, thereby mirroring the existing event user registration email workflow without side effects on communication records.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d052166 and 44c51f9.

📒 Files selected for processing (2)
  • backend/djangoindia/bg_tasks/event_tasks.py (1 hunks)
  • backend/djangoindia/db/admin.py (4 hunks)

Comment on lines +362 to +363
actions = [send_email_to_selected_users]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore default admin actions.
Setting actions to just the custom handler removes Django’s built-in delete_selected action, so staff can no longer bulk-delete users from the changelist. Please append the new action instead of replacing the defaults.

-    actions = [send_email_to_selected_users]
+    actions = ["delete_selected", send_email_to_selected_users]
📝 Committable suggestion

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

Suggested change
actions = [send_email_to_selected_users]
++ b/backend/djangoindia/db/admin.py
@@ -360,7 +360,7 @@ class UserAdmin(admin.ModelAdmin):
"""
Custom actions for UserAdmin.
"""
actions = ["delete_selected", send_email_to_selected_users]
def send_email_to_selected_users(self, request, queryset):
# ... implementation ...
🤖 Prompt for AI Agents
In backend/djangoindia/db/admin.py around lines 362-363, the current assignment
replaces Django’s default admin actions (removing delete_selected); change it so
the custom send_email_to_selected_users is appended to the ModelAdmin defaults
instead of replacing them. Retrieve the default actions from
django.contrib.admin.ModelAdmin (handling the case they may be None), convert to
a mutable list if necessary, append send_email_to_selected_users, and assign
that combined list to actions.

Comment on lines +460 to +463
"form": form,
"opts": self.model._meta,
"queryset": json.loads(request.session.get("selected_email_ids", [])),
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid crashing when no selection is stored.
json.loads cannot handle the list default ([]), so visiting /send_email/ without a session value raises a TypeError. Use a string fallback (or conditional parse) to keep the view resilient.

-            "queryset": json.loads(request.session.get("selected_email_ids", [])),
+            "queryset": json.loads(
+                request.session.get("selected_email_ids", "[]")
+            ),
📝 Committable suggestion

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

Suggested change
"form": form,
"opts": self.model._meta,
"queryset": json.loads(request.session.get("selected_email_ids", [])),
}
"form": form,
"opts": self.model._meta,
"queryset": json.loads(
request.session.get("selected_email_ids", "[]")
),
}
🤖 Prompt for AI Agents
In backend/djangoindia/db/admin.py around lines 460 to 463, the code calls
json.loads(request.session.get("selected_email_ids", [])) which crashes when the
session key is missing because json.loads expects a string, not a list; change
to either provide a JSON string default (e.g.
request.session.get("selected_email_ids", "[]")) or guard the value (val =
request.session.get("selected_email_ids"); parsed = json.loads(val) if
isinstance(val, str) else val or []) so the view safely handles missing or
already-parsed selections.

@yanurag-dev
Copy link
Copy Markdown
Contributor

@CodeRabbit fullreview

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 28, 2025

@Yadavanurag13 I'll perform a full review of PR #370 which adds mass mailing functionality for the User model in the Django admin panel.

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@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: 3

Caution

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

⚠️ Outside diff range comments (2)
backend/djangoindia/db/admin.py (2)

189-189: Incorrect Celery task signature usage (missing comm_id).

send_mass_mail_task requires (emails, comm_id, **kwargs), but this call only passes emails. It will raise TypeError at runtime.

Fix one of:

  • If you don’t track an EventCommunication here, call the users task:
-                    send_mass_mail_task.delay(emails, fail_silently=False)
+                    send_mass_mail_task_users.delay(emails, fail_silently=False)
  • Or create an EventCommunication record (as in EventUserRegistrationAdmin) and pass its id.

726-728: Don’t mark communication SENT before the async task completes.

You set status=SENT and sent_at immediately after queuing. The Celery task also updates status and may fail, causing incorrect state.

Apply this diff:

-                    communication.status = EventCommunication.Status.SENT
-                    communication.sent_at = timezone.now()
-                    communication.save()
+                    # Keep as PENDING; task will set SENT/FAILED and sent_at.

Optional: show a “Queued N emails” message instead of “sent”.

🧹 Nitpick comments (3)
backend/djangoindia/bg_tasks/event_tasks.py (1)

126-127: Consider adding Celery auto‑retry/backoff.

Transient SMTP/network errors are common. Add autoretry_for + limited retries and a delay.

Example:

-@shared_task
+@shared_task(bind=True, autoretry_for=(Exception,), retry_backoff=True, retry_jitter=True, max_retries=3)
 def send_mass_mail_task_users(emails, **kwargs):

If project Celery version doesn’t support retry_backoff/retry_jitter, use default_retry_delay=60 with autoretry_for.

backend/djangoindia/db/admin.py (2)

419-456: Harden session parsing and clear selection after send.

  • Session value may be missing or non‑string; parsing defensively avoids surprises.
  • Clear selected_email_ids after success to prevent accidental re‑use on the next visit.

Apply this diff:

-                    user_ids = request.session.get("selected_email_ids", [])
+                    raw = request.session.get("selected_email_ids")
+                    user_ids = json.loads(raw) if isinstance(raw, str) else (raw or [])
                     if not user_ids:
                         messages.error(request, "No user IDs provided.")
                         return redirect("../")
-
-                    if user_ids:
-                        user_ids = json.loads(user_ids)
+                    # user_ids is now a list of IDs
@@
-                    if emails:
-                        send_mass_mail_task_users.delay(emails, fail_silently=False)
+                    if emails:
+                        send_mass_mail_task_users.delay(emails, fail_silently=False)
                         messages.success(
                             request, f"{len(emails)} emails sent successfully."
                         )
+                        request.session.pop("selected_email_ids", None)
                     else:

Optionally validate addresses with EmailValidator to catch malformed inputs early.


549-549: Use Celery asynchronously for confirmation emails.

These are Celery tasks but are called synchronously; use .delay(...) to avoid blocking admin requests.

-                rsvp_confirmation_email_task(registration.user.email, event_id)
+                rsvp_confirmation_email_task.delay(registration.user.email, event_id)

and

-                        rsvp_confirmation_email_task(registration.user.email, event.id)
+                        rsvp_confirmation_email_task.delay(registration.user.email, event.id)

Also applies to: 667-667

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d052166 and 44c51f9.

📒 Files selected for processing (2)
  • backend/djangoindia/bg_tasks/event_tasks.py (1 hunks)
  • backend/djangoindia/db/admin.py (4 hunks)
🔇 Additional comments (4)
backend/djangoindia/db/admin.py (4)

21-22: Import looks correct.


362-363: Don’t replace default admin actions. Append instead.

Overriding actions removes Django’s built‑in delete_selected. Prefer extending via get_actions:

-    actions = [send_email_to_selected_users]
+    # Keep default actions (incl. delete_selected) and add our custom action
+    def get_actions(self, request):
+        actions = super().get_actions(request)
+        actions["send_email_to_selected_users"] = (
+            send_email_to_selected_users,
+            "send_email_to_selected_users",
+            send_email_to_selected_users.short_description,
+        )
+        return actions

459-464: Avoid json.loads with a list default.

json.loads expects a string; using [] as the default raises TypeError when the session key is missing. Use a JSON string fallback or guard the type.

Apply this diff:

-            "queryset": json.loads(request.session.get("selected_email_ids", [])),
+            "queryset": json.loads(request.session.get("selected_email_ids", "[]")),

Do the same adjustment in other admin views using this pattern.


352-355: No issues with list_filter fields
Both is_password_expired and is_onboarded are defined on the User model, so including them in list_filter will not trigger a FieldError.

Comment on lines +138 to +142
if not isinstance(emails, (list, tuple)):
logger.exception(
"Invalid input: Emails must be a list or tuple of email data tuples."
)

Copy link
Copy Markdown

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 just log invalid input — fail fast.

You log an invalid emails type but continue execution, which can cascade into harder-to-debug failures. Raise a TypeError (or return 0) immediately.

Apply this diff:

-    if not isinstance(emails, (list, tuple)):
-        logger.exception(
-            "Invalid input: Emails must be a list or tuple of email data tuples."
-        )
+    if not isinstance(emails, (list, tuple)):
+        raise TypeError(
+            "emails must be a list/tuple of (subject, message, from_email, recipient_list) tuples"
+        )
📝 Committable suggestion

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

Suggested change
if not isinstance(emails, (list, tuple)):
logger.exception(
"Invalid input: Emails must be a list or tuple of email data tuples."
)
if not isinstance(emails, (list, tuple)):
raise TypeError(
"emails must be a list/tuple of (subject, message, from_email, recipient_list) tuples"
)
🤖 Prompt for AI Agents
In backend/djangoindia/bg_tasks/event_tasks.py around lines 138 to 142, the code
only logs when `emails` is not a list/tuple and then continues; change this to
fail fast by raising a TypeError (or returning 0 if calling convention expects
that) immediately after detecting the invalid type. Replace the logger.exception
call with a raised TypeError containing a clear message like "emails must be a
list or tuple of email data tuples" (or return 0 instead), so execution stops
and callers can handle the error.

Comment on lines +144 to +155
if len(emails) > 100:
loops = len(emails) // 100
else:
loops = 1
for i in range(loops):
send_mass_mail(emails[i * 100 : (i + 1) * 100], **kwargs)

if len(emails) % 100 != 0:
send_mass_mail(emails[loops * 100 :], **kwargs)
except Exception as e:
logger.exception("Failed to send mass emails.")
raise e
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix return value and simplify batching; currently always returns None.

Docstring promises “same as send_mass_mail” but the function returns None. Also the loop/remainder logic is overcomplicated and can make a redundant call for <100 items.

Apply this diff:

-    try:
-        if len(emails) > 100:
-            loops = len(emails) // 100
-        else:
-            loops = 1
-        for i in range(loops):
-            send_mass_mail(emails[i * 100 : (i + 1) * 100], **kwargs)
-
-        if len(emails) % 100 != 0:
-            send_mass_mail(emails[loops * 100 :], **kwargs)
-    except Exception as e:
-        logger.exception("Failed to send mass emails.")
-        raise e
+    try:
+        if not emails:
+            return 0
+        total_sent = 0
+        for start in range(0, len(emails), 100):
+            batch = emails[start : start + 100]
+            total_sent += send_mass_mail(batch, **kwargs)
+        return total_sent
+    except Exception as e:
+        logger.exception("Failed to send mass emails.")
+        raise
📝 Committable suggestion

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

Suggested change
if len(emails) > 100:
loops = len(emails) // 100
else:
loops = 1
for i in range(loops):
send_mass_mail(emails[i * 100 : (i + 1) * 100], **kwargs)
if len(emails) % 100 != 0:
send_mass_mail(emails[loops * 100 :], **kwargs)
except Exception as e:
logger.exception("Failed to send mass emails.")
raise e
try:
if not emails:
return 0
total_sent = 0
for start in range(0, len(emails), 100):
batch = emails[start : start + 100]
total_sent += send_mass_mail(batch, **kwargs)
return total_sent
except Exception as e:
logger.exception("Failed to send mass emails.")
raise
🤖 Prompt for AI Agents
In backend/djangoindia/bg_tasks/event_tasks.py around lines 144-155, the
batching logic overcomplicates slicing and can call send_mass_mail redundantly
and the function always returns None; change the loop to iterate in fixed-size
chunks (for start in range(0, len(emails), 100)) and call send_mass_mail once
per chunk, accumulate the returned counts into a total variable, and at the end
return that total so the function behaves "same as send_mass_mail"; keep the
existing exception handling (log and re-raise) unchanged.

Comment on lines +408 to +417
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path(
"send_email/",
self.admin_site.admin_view(self.send_email_view),
name="send_email",
),
]
return custom_urls + urls
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

URL name collision risk across multiple admins. Use unique names per model.

Several admins define name="send_email". Named URLs must be unique within the admin namespace; collisions break reversing and make logs ambiguous.

Apply this diff to make names unique and model-scoped:

-            path(
-                "send_email/",
-                self.admin_site.admin_view(self.send_email_view),
-                name="send_email",
-            ),
+            path(
+                "send_email/",
+                self.admin_site.admin_view(self.send_email_view),
+                name=f"{self.model._meta.app_label}_{self.model._meta.model_name}_send_email",
+            ),

Consider updating any reverse(...) calls accordingly (you mostly use relative redirects, which are unaffected).

📝 Committable suggestion

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

Suggested change
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path(
"send_email/",
self.admin_site.admin_view(self.send_email_view),
name="send_email",
),
]
return custom_urls + urls
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path(
"send_email/",
self.admin_site.admin_view(self.send_email_view),
name=f"{self.model._meta.app_label}_{self.model._meta.model_name}_send_email",
),
]
return custom_urls + urls
🤖 Prompt for AI Agents
In backend/djangoindia/db/admin.py around lines 408 to 417, the custom URL name
"send_email" collides across multiple admin classes; change the URL name to a
model-scoped unique name (e.g., f"{self.model._meta.model_name}_send_email" or
"{app_label}_{model_name}_send_email") when calling path(..., name=...), and
update any reverse(...) or admin_site.reverse calls that reference "send_email"
to use the new unique name (or construct it dynamically from the model/app meta)
so names are globally unique within the admin namespace.

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.

[FEATURE] Add functionality to send mail to users

3 participants