My implementation uses a CustomUser model. I'm creating a Django admin action that allows me to compose and send emails to multiple users. Everything on the website behaves as though it's working. After I hit the Send Email button, I'm redirected back to the Admin but I see no console output indicating an email was sent and self.message_user is never called and sent to the Admin.
Email Backend
Password Reset works with reset link sent to the console, so email backend is working.
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
admin.py
if "apply" in request.POST: <-- This line never fires!
class CustomUserAdmin(ExportMixin, UserAdmin):
@admin.action(description="Email selected users")
def send_email_action(self, request, queryset):
print(request.POST)
print(request.POST.getlist("apply"))
input("HALT")
if "apply" in request.POST:
print("apply")
form = EmailForm(request.POST)
if form.is_valid():
print("form valid")
subject = form.cleaned_data["subject"]
message = form.cleaned_data["message"]
recipients = [user.email for user in queryset]
send_mail(subject, message, "your_email@example.com", recipients)
self.message_user(request, f"{len(recipients)} emails were sent.")
return HttpResponseRedirect(request.get_full_path())
else:
print("no apply")
form = EmailForm()
return render(request, "email/admin_send.html", {"form": form})
forms.py
Works as expected.
class EmailForm(forms.Form):
subject = forms.CharField(max_length=256)
message = forms.CharField(widget=forms.Textarea)
admin_send.html
Either:
Q1: Do I need to define the form action="" which is currently blank? I ask because it sort of feels like when I click the Send Email button, it may not be calling the send_email_action function again. If I need to define this, I don't know what to put here because normally it would be a URL but I need it to be an admin action function. I believe this is the primary problem because if the function were getting called, I should see the results of print(request.POST) printed to the console again, but I don't (ie: QueryDict).
Q2: Notice the input name="apply". The send_email_action function is obviously looking for this data, but it doesn't come back from the form or if it does, I cannot find it in the request object. I believe this may also be a problem, but we can only solve it after we solve the first problem above.
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block title %}
Email Users
{% endblock %}
{% block content %}
<br>
<br>
<br>
<h2>Email Users</h2>
<form action="" method="post">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" name="apply" value="Send Email">
</form>
{% endblock content %}
Step1) In the Admin I've selected multiple users in my CustomUser table, I've selected "Email selected users" from the action dropdown menu, I've clicked GO:
console
<QueryDict: {'csrfmiddlewaretoken': ['zHlbmtRaNIFwrdNmCFxwyiVLi1IaCFKHoJAmZWKttwTMN97gC1JhWSryO9POuaDK'], 'action': ['send_email_action'], 'select_across': ['0'], 'index': ['0'], '_selected_action': ['6', '1', '7', '8']}>
[]
HALT
Step2) I hit enter in the console to continue, the form displays. I enter a subject and a message.
console
no apply
[23/Nov/2024 23:58:25] "POST /admin/app_accounts/customuser/?company=Cisco HTTP/1.1" 200 10317
Step3) I click the "Send Email" button.
console
[24/Nov/2024 00:02:07] "POST /admin/app_accounts/customuser/?company=Cisco HTTP/1.1" 200 19098
[24/Nov/2024 00:02:07] "GET /admin/jsi18n/ HTTP/1.1" 200 3342
OK, I figured out how to make this work. The key for me was simply realizing that form action="" did in fact need to be populated. With what? I didn't know how to populate it with something that would allow me to call back to my admin action function <-- this was my mental block. DUH! I needed to populate it with a normal view, not an admin action function! I needed to split up these pieces of my code. So I created a URL where form POST data could be sent, figured out how to transparently pass data from my admin action function through the intermediate form to my normal view for post processing (a bit of data conversion), then I built my view using portions of the code from my original admin.py file under if "apply" in request.POST:. Then I simplified my admin action function.
The only thing I couldn't get working is the confirmation message back to the Admin interface... see the # comments near the end of my views.py file. This errored out with: ModelAdmin.message_user() missing 1 required positional argument: 'message', even though it's clear I provided the message. I can't explain why this is happening. But as a workaround, I commented out the return HttpResponseRedirect and rendered the information I needed in a new template, which still allows me to navigate back to the place I was working from.
This solution feels a little dirty to me, mostly because the transparent pass through of data gets passed as parameters in the URL, but nothing sensitive is being passed so I guess it's not that big of a deal, and this is never revealed to the end user as long as the code is working properly (although it is revealed in the console logs).
If anyone reads this, I hope this helps. Also, if you can think of a better, simpler, more efficient way to handle this, let me know! I'm pretty new to Django so I feel good that I got this working at all, but I'm wondering if there isn't a better way.
admin.py
class CustomUserAdmin(ExportMixin, UserAdmin):
model = CustomUser
actions = ["send_email_action"]
@admin.action(description="Email selected users")
def send_email_action(self, request, queryset):
form = EmailForm()
recipients = "|".join([item.email for item in queryset])
return render(
request,
"email/admin_send.html",
{"form": form, "recipients": recipients},
forms.py
class EmailForm(forms.Form):
subject = forms.CharField(max_length=256)
message = forms.CharField(widget=forms.Textarea)
admin_send.html
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block title %}
Email Users
{% endblock %}
{% block content %}
<br>
<br>
<br>
<h2>Email Users</h2>
<form action="{% url 'process_email' recipients %}" method="post">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" value="Send Email">
</form>
{% endblock content %}
urls.py
from django.urls import path
from .views import process_email_view
urlpatterns = [
path(
"process_email/<str:recipients>/",
process_email_view,
name="process_email",
),
]
views.py
def process_email_view(request, recipients):
form = EmailForm(request.POST)
if form.is_valid():
subject = form.cleaned_data["subject"]
message = form.cleaned_data["message"]
recipients = recipients.split("|")
for recipient in recipients:
send_mail(subject, message, "your_email@example.com", [recipient])
# CustomUserAdmin.message_user(request, f"{len(recipients)} emails were sent.")
# return HttpResponseRedirect("/admin/app_accounts/customuser/")
context = {
"count": len(recipients),
"return_url": "/admin/app_accounts/customuser/",
}
return render(request, "email/admin_send_success.html", {"context": context})
admin_send_success.html
{% extends "base.html" %}
{% block title %}Admin Emails Sent{% endblock title %}
{% block content %}
<br>
<br>
<br>
<h2>Admin Emails Sent</h2>
<p>{{ context.count }} emails were sent successfully.</p>
<p>Please <a href="{{ context.return_url }}">CLICK HERE</a> to return to the Admin page you were working from.
{% endblock content %}