1

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!

Lord Elrond
  • 13,430
  • 7
  • 40
  • 80
JP1
  • 69
  • 9

2 Answers2

2

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

Maxim Danilov
  • 2,472
  • 1
  • 3
  • 8
  • @MD Thanks for the suggestion, currently getting Exception Type: TemplateSyntaxError Exception Value: Invalid filter: 'unlocalize'. Will try to debug this one. – JP1 Nov 22 '22 at 15:58
  • you forget to load filter library {% load l10n %} – Maxim Danilov Nov 22 '22 at 16:23
  • Thank you, I have now tried multiple version of your suggestion to add to the HTML template but they have had no effect on how the 'submit' button functions. It still reverts to the default changelistview. It appears either the form is invalid or when I override the non-field errors the the form is automatically bound and valid and the intermediate page does not appear but the post request goes straight to the success page. – JP1 Nov 24 '22 at 03:56
  • Are you suggesting that slide 61-62 will solve this?if yes you can add here and show how. I worry that your patch to swap the order of how the arguments are passed will not enable the extra context i.e. the queryset and subject and message content to be passed. I dont think I need to return modelAdmin object that sent the post request but wont this create a thread waiting for a response? – JP1 Nov 27 '22 at 07:54
  • i corrected my answer and give you a working solution, i tested it. Also on my video talk you can find a link to repository with examples, have you check it? – Maxim Danilov Nov 27 '22 at 21:37
  • Thank you for the update. I dont appear to have much luck with this. After Selecting users from the changelistview and selecting the broadcast option from the actions dropdown menu, It correctly redirects to the send_email.html page (which shows no change from my version, as is correct). After filling in the subject and message fields and hitting the send button it reverts back to the changelistview (as my version did) but now displays the warning message 'No Action selected'. – JP1 Nov 28 '22 at 15:38
  • I put a debugging print statements inside the 'if send...' and 'if form.isvalid...' and neither get triggered. I also tried changing my SendEmailForm to an ActionForm subclass but there is no difference to the result I get from your suggestion here. I get the same message. Any thoughts? Also I tried to look at your repository for further insights but the Django-admin repository is empty and there is no link on the youtube video. – JP1 Nov 28 '22 at 15:38
  • Are you sure? You take my code from template? You have in template? Please put view code and template code in your question. – Maxim Danilov Nov 28 '22 at 19:47
  • @Mixim Danilov Update: now get no response from submit /send button Form is invalid
    • accounts
      • Select a valid choice. That choice is not one of the available choices.
    > Does your PrintSchemaForm have any required fields as per my requirement in my SendEmailForm message field?
    – JP1 Nov 29 '22 at 06:48
  • ok, it means - it works.Right now you have a problem with the form, it is not valid. Can you print form.errors ? – Maxim Danilov Nov 29 '22 at 11:52
  • Sorry your solution doesn't work. No progress to my solution. I stated in the initial description of the problem "a required field (message) causing non-field errors and resulting in an invalid form." I can override this but it still reverts back to the Changeviewlist without actioning 'Send' button. I suspect your suggestion works only on your Form without required fields? The form errors caused my your implementation are in my previous comment. Specifically yours causes "
  • accounts
    • Select a valid choice. That choice is not one of the available choices.
    • "
  • – JP1 Nov 29 '22 at 14:49
  • i don't know your account model, but, I think, problem is here: . Instead ID you send email. I am sure, my code is work for everywhere. If you think, it is not work in your case - i can not help you any more. – Maxim Danilov Nov 29 '22 at 17:27
  • A bit if a bad hack but if I added this function to the SendEmaiForm objects it progresses to success page. Not ideal :( ` def _clean_form(self): try: cleaned_data = self.clean() except ValidationError as e: pass else: if cleaned_data is not None: self.cleaned_data = cleaned_data try: if 'message' in self.errors: self.errors.pop('message') except: pass try: if 'accounts' in self.errors: self.errors.pop('accounts') except: pass` – JP1 Dec 01 '22 at 09:29