djangodjango-viewsdjango-formsdjango-admin-filtersdjango-admin-actions

Django custom admin actions with an intermediate page submit not triggered


Writing an admin action so an administrator can select a template they can use to send a message to subscribers by inputting only the subject and text message. Using a filtered list from the admin panel an action called broadcast is triggered on this queryset (the default filter list). The admin action 'broadcast' is a function of a sub-classed UserAdmin class. The intermediate page is displayed that shows a dropdown selector for the emailtype, the queryset items (which will be email addresses, input fields for the subject and message text (message is required field) a button for optional file attachment followed by send or cancel buttons. Problem 1) after hitting the send button the app reverts to the admin change list page. In the broadcast function, the conditional if 'send' in request.POST: is never called.

forms.py

mail_types=(('1','Newsletter Link'),('2','Update Alert'))

class SendEmailForm(forms.Form):
    _selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
    #Initialized 'accounts' from Account:admin.py Actions: 'send_email' using>> form = SendEmailForm(initial={'accounts': queryset})
    my_mail_type=forms.ChoiceField(label='Mail Type',choices=mail_types,required=False)
    subject = forms.CharField(widget=forms.TextInput(attrs={'placeholder': ('Subject')}),required=False)
    message = forms.CharField(widget=forms.Textarea(attrs={'placeholder': ('Teaser')}),required=True,min_length=5,max_length=1000)
    attachment = forms.FileField(widget=forms.ClearableFileInput(),required=False)
    accounts = forms.ModelChoiceField(label="To:",
                                           queryset=Account.objects.all(),
                                           widget=forms.SelectMultiple(attrs={'placeholder': ('user_email@somewhere.com')}),
                                           empty_label='user_email@somewhere.com',
                                           required=False,

admin.py

from .forms import SendEmailForm
from django.http import HttpResponseRedirect,HttpResponse
from django.shortcuts import render, redirect
from django.template.response import TemplateResponse

def broadcast(self, request, queryset):
         form=None      
       if 'send' in request.POST:
                print('DEBUGGING: send found in post request')
                form = SendEmailForm(request.POST, request.FILES,initial={'accounts': queryset,})
                if form.is_valid():
                    #do email sending stuff here
                    print('DEBUGGING form.valid ====>>> BROADCASTING TO:',queryset)
                    #num_sent=send_mail('test subject2', 'test message2','From Team',['dummy@hotmail.com'],fail_silently=False, html_message='email_simple_nb_template.html',)
                    self.message_user(request, "Broadcasting of %s messages has been started" % len(queryset))
                    print('DEBUGGING: returning to success page')
                    return HttpResponseRedirect(request, 'success.html', {})
        if not form:    
            # intermediate page right here 
            print('DEBUGGING: broadcast ELSE called')
            form = SendEmailForm(request.POST, request.FILES, initial={'accounts': queryset,})
        return TemplateResponse(request, "send_email.html",context={'accounts': queryset, 'form': form},)

send_email.html

{% extends "admin/base_site.html" %}
{% load i18n admin_urls static %}
{% load crispy_forms_tags %}


{% block content %}
<form method="POST" enctype="multipart/form-data" action=""  >
    {% csrf_token %} 
    <div>   
        <div>
            
            <p>{{ form.my_mail_type.label_tag }}</p>
            <p>{{ form.my_mail_type }}</p>
        </div>
        <div>        
           <p>{{ form.accounts.label_tag }}</p>
            <p>
                {% for account in form.accounts.queryset %} 
                    {{ account.email }}{% if not forloop.last %},&nbsp;{% endif %}
                {% endfor %}
            </p>
            <p><select name="accounts" multiple style="display: form.accounts.email">
                {% for account in form.accounts.initial %}  
                    <option value="{{ account.email }}" selected>{{ account }}</option>

                {% endfor %}
            </p></select>
        </div>
        <div>
            
            <p>{{ form.subject.label_tag }}</p>
            <p>{{ form.subject }}</p>
        </div>
        <div>

            <p>{{ form.message.label_tag }}</p>
            <p>{{ form.message }}</p>
        </div>  
        <div>
            
            <p>{{ form.attachment.label_tag }}</p>
            <p>{{ form.attachment.errors }}</p>
            <p>{{ form.attachment }}</p>
        </div> 
    
                <input type="hidden" name="action" value="send_email" />
                <input type="submit" name="send" id="send" value="{% trans 'Send messages' %}"/> 
                <a href="{% url 'admin:account_account_changelist' %}" name="cancel" class="button cancel-link">{% trans "Cancel this Message" %}</a>
       

    </div>
</form>
{% endblock %}

Inspecting the browser at the POST call seems to show all the data was bound. Another poster here suggested the admin action buttons divert requests to an internal 'view' and you should redirect to a new view to handle the POST request. I can't get that to work because I can't get a redirect to 'forward' the queryset. The form used in the suggested fix was simpler and did not use the queryset the same way. I have tried writing some FBVs in Forms.py and Views.py and also tried CBVs in views.py but had issues having a required field (message) causing non-field errors and resulting in an invalid form. I tried overriding these by writing def \_clean_form(self): that would ignore this error, which did what it was told to do but resulted in the form essentially being bound and validated without any inputs so the intermediate page didn't appear. Which means the rabbit hole returned to the same place. The send button gets ignored in either case of FBVs or CBVs, which comes back to the admin action buttons Post requests revert to the admin channels! Any ideas on how to work around this? Key requirements: From the admin changelist action buttons:

  1. the Form on an intermediate page must appear with the queryset passed from the admin changelist filter.

  2. The message input field on the form is a required field.

  3. the send button on the HTML form view needs to trigger further action.

NOTES: My custom Admin User is a subclass of AbstractBaseUser called Account, where I chose not to have a username and am using USERNAME_FIELD='email'. Also, I do not need a Model.py for the SendEmailForm as I don't need to save the data or update the user models, just send the input message using the chosen template and queryset. Help is much appreciated!


Solution

  • It will never work in your case:

    1. You call the action.
    2. You receive the Action Confirmation template render.
    3. After pressing "SEND" in your "confirmation" step, you send a POST request to ModelAdmin, not in your FB-Action.
    4. ModelAdmin gets a POST request without special parameters and shows you a list_view by default.

    In your case, you should add a send_email.html template:

    {% load l10n %}
    {# any your staff here #}
    
    {% block content %}
    <form method="POST" enctype="multipart/form-data">
    {# any your staff here #}
        <div>
            
            <p>{{ form.attachment.label_tag }}</p>
            <p>{{ form.attachment.errors }}</p>
            <p>{{ form.attachment }}</p>
        </div> 
    
        {% for obj in accounts %}
            <input type="hidden" name="_selected_action" value="{{ obj.pk|unlocalize }}" />
        {% endfor %}
        <input type="hidden" name="action" value="broadcast" />
    {# any your staff here #}
    </form>
    {% endblock %}
    

    You should change your action view, some things are not working in your code:

    def broadcast(self, request, queryset):
        form = SendEmailForm(data=request.POST, files=request.FILES, initial={'accounts': queryset})
    
        if 'send' in request.POST:
            ...  # your staff here
                if form.is_valid():
                    ...  # your staff here
                    # return HttpResponseRedirect(request, 'success.html', {} )  this is NEVER WORK
                    return TemplateResponse(request, 'success.html', {})
        ...  # your staff here
        return TemplateResponse(request, "send_email.html",context={'accounts': queryset, 'form': form},)
    

    I am giving you a solution that I have TESTED on my project. I am sure, it works.

    We were told on DjangoCon Europe 2022 that django-GCBV is like a ModelAdminAction and I've added a link below for the talk.

    https://youtu.be/HJfPkbzcCJQ?t=1739