How to create a complex admin action that requires additional information? - django

I'm interested in creating an action for the admin interface that requires some additional information beyond the items selected. My example is bulk adding comics to a series. (Yes I know the obvious answer is to create a schema with X-to-X relationships, but bear with me for the sake of a simple example).
In this example, I've created 100 comics. After they're created, I'd like to associate them with a series object that's already been created. To execute this action within the admin, I'd like to select the items then initiate the action. I should then be asked which series object to use (via a popup, intermediate form, etc.).
I've followed the instructions here which claim to accomplish this via an intermediate form. After working with it, I'm not getting any more errors, but the action itself isn't being executed either - the forloop never gets executed. Instead, it returns to the admin list of comics with the message: "No action selected."
my admin.py method:
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.http import HttpResponseRedirect
def addSeries(self, request, queryset):
form = None
if 'cancel' in request.POST:
self.message_user(request, 'Canceled series linking.')
return
elif 'link_series' in request.POST:
form = self.SeriesForm(request.POST)
if form.is_valid():
series = form.cleaned_data['series']
for x in queryset:
y = Link(series = series, comic = x)
y.save()
self.message_user(request, self.categorySuccess.render(Context({'count':queryset.count(), 'series':series})))
return HttpResponseRedirect(request.get_full_path())
if not form:
form = self.SeriesForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})
return render_to_response('setSeries.html', {'comics': queryset, 'form': form, 'path':request.get_full_path()}, context_instance=RequestContext(request))
addSeries.short_description = 'Set Series'
My intermediate form setSeries.html:
<!DOCTYPE html>
<html>
<head>
<title>Create Series Links</title>
</head>
<body>
<h1>Create Series Links</h1>
<p>Choose the series for the selected comic(s):</p>
<form method="post" action="{{ path }}">
<table>
{{ form }}
</table>
<p>
<input type="hidden" name="action" value="changeSeries" />
<input type="submit" name="cancel" value="Cancel" />
<input type="submit" name="link_series" value="link_series" />
</p>
</form>
<h2>This categorization will affect the following:</h2>
<ul>
{% for comic in comics %}
<li>{{ comic.title }}</li>
{% endfor %}
</ul>
</body>
</html>

One thing I notice is that your action’s method is “addSeries”, but in the form you’re calling it “changeSeries”.
In your ModelAdmin, you should have a line like this:
actions = ['addSeries']
If that’s the line you have, then you need to change:
<input type="hidden" name="action" value="changeSeries" />
to:
<input type="hidden" name="action" value="addSeries" />
That’s how Django’s admin knows which action was selected. When you have an intermediary form between choosing the action and performing the action, you’ll need to preserve the action name from the select menu on the admin interface.

Related

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 %}, {% 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' %}"/>
{% trans "Cancel this Message" %}
</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:
the Form on an intermediate page must appear with the queryset passed from the admin changelist filter.
The message input field on the form is a required field.
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!
It will never work in your case:
You call the action.
You receive the Action Confirmation template render.
After pressing "SEND" in your "confirmation" step, you send a POST request to ModelAdmin, not in your FB-Action.
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
I can't get that to work because I can't get a redirect to 'forward' the queryset
I have a similar use case and save the primary keys of the filtered query set in the session (in your case you may be able to save emails and avoid another query)
def broadcast(self, request, queryset):
request.session["emails"] = list(queryset.values_list("emails", flat=True))
return HttpResponseRedirect("url_to_new_view")
I can then use primary keys to filter query set in the new view. You also handle the form in this new view.
User.objects.filter(email__in=self.request.session["emails"])

django-filters and HTMX: Trigger a Filter on Multiple Views at Once

I have a simple app where I want to have a single filter dynamically update the queryset in two different Views at the same time.
In this example, the user can filter based on city/country/continent and I'd like to dynamically update (1) the table showing the relevant objects in the model, and (2) the leaflet map to plot the points.
I think the main issue here is that I need to trigger an update to the filter queryset on multiple 'views' at the same time and not sure how to structure my project to achieve that. Or if that's the right way to think about the problem.
I'm trying to have a filter work with{% for city in cityFilterResults %} iterator in two different views at the same time.
How can I achieve having two different views update based on a Filter using HTMX?
index.html:
(Note: My expected behaviour works if I switch the hx-get URL between either the table or the map. But they don't filter together and this is the issue I'm stuck on.)
<body>
<h3>Filter Controls:</h3><br>
<form hx-get="{% url '_city_table' %}" hx-target="#cityTable">
<!-- <form hx-get="{% url '_leaflet' %}" hx-target="#markers"> -->
{% csrf_token %}
{{ cityFilterForm.form.as_p }}
<button type="submit" class="btn btn-primary"> Filter </button>
</form>
[...]
<!-- Table Body -->
<tbody id="cityTable" hx-get="{% url '_city_table' %}" hx-trigger="load" hx-target="nearest tr"> </tbody>
[...]
<!-- Div to get the markers -->
<div id="markers" hx-get="{% url '_leaflet' %}" hx-trigger="my_custom_trigger from:body"> </div>
</body>
<script>
[... Leaflet stuff ...]
document.getElementById('markers');
</script>
_city_table.html:
{% for city in cityFilterResults %}
<tr>
<td> {{ city.city }} </td>
<td> {{ city.country }} </td>
<td> {{ city.continent }} </td>
<td> {{ city.latitude|floatformat:4 }} </td>
<td> {{ city.longitude|floatformat:4 }} </td>
</tr>
{% endfor %}
_leaflet.html:
<script>
if (map.hasLayer(group)) {
console.log('LayerGroup already exists');
group.clearLayers();
map.removeLayer(group);
} else {
console.log('LayerGroup doesn't exist');
}
{% for city in cityFilterResults %}
var marker = L.marker([{{city.latitude}}, {{city.longitude}}]).addTo(group)
.bindPopup("{{city.city}}")
{% endfor %}
group.addTo(map);
</script>
views.py:
def _city_table(request):
city_filter_form = cityFilter(request.GET, queryset=cityModel.objects.all())
city_filter_results = city_filter_form.qs
context={ 'cityModel': cityModel.objects.all(),
'cityFilterResults': city_filter_results }
response = render(request, '_city_table.html', context)
response['HX-Trigger'] = 'my_custom_trigger'
return response
def _leaflet(request):
city_filter_form = cityFilter(request.GET, queryset=cityModel.objects.all())
city_filter_results = city_filter_form.qs
context={ 'cityModel': cityModel.objects.all(),
'cityFilterResults': city_filter_results }
return render(request, '_leaflet.html', context)
When the hx-get on Filter form points to the URL for _city_table template, then the table filters as expected but the map does not:
When the hx-get on Filter form points to the URL for the _leaflet template, then the map filters but the table does not:
You can find some solution here:
https://htmx.org/examples/update-other-content/
In particular ther are two solutions
swap-oob (
https://htmx.org/examples/update-other-content/#oob )
You have to post your form to the action of your controller.
The action return the two element that will be replaced
<button hx-get="/Form/OutOfBandResponse" hx-swap="none">ok</button>
<div id="A"></div>
<div id="B"></div>
This is the response:
<!-- this will replace the element with id A -->
<div id="A" hx-swap-oob="true">
Joe Smith
</div>
<!-- this will replace the element with id B -->
<div id="B" hx-swap-oob="true">
Antony Queen
</div>
In alternative you can use events. ( https://htmx.org/examples/update-other-content/#events )
So you have to create a handler for a event, then when you submit the form you have to response with a header that fire that event.
When the event fire, the divs react updating their self.
I'm sharing a solution to my question. It works, which is awesome, but I would very much like to understand what alternative approaches could be implemented. My approach feels like a hack and think I'm missing something.
I was able to use a session to pass the filter between the views. Basically, I am using the Filter form on the table. In the 'views' for the table, two things are happening: (1) I create a session and store the filter results, and (2) return a HTTP response with a header that serves as a trigger for the HTMX on the Leaflet part.
In the HTML template, I have the Leaflet URL waiting for the custom HTMX trigger to come back with the table. Then, in the Leaflet views, I take the data from the session.
views.py:
def _city_table(request):
city_filter_form = cityFilter(request.GET, queryset=cityModel.objects.all())
city_filter_results = city_filter_form.qs
request.session['my_city_filter'] = [city.id for city in city_filter_results]
request.session.modified=True
context={ 'cityModel': cityModel.objects.all(),
'cityFilterResults': city_filter_results }
response = render(request, '_city_table.html', context)
response['HX-Trigger'] = 'my_custom_trigger'
return response
def _leaflet(request):
city_filter_results = [cityModel.objects.get(id=id) for id in request.session['my_city_filter']]
context={ 'cityModel': cityModel.objects.all(),
'cityFilterResults': city_filter_results }
return render(request, '_leaflet.html', context)
index.html:
<div id="markers" hx-get="{% url '_leaflet' %}" hx-trigger="my_custom_trigger from:body"> </div>

What can cause MultiValueDictKeyError in Django?

i almost finished my register form, but something went wrong and i totally don't know what. It is a little difficult to describe but i will try.
So, as you can see here is login form:
login.html
<h1>login page</h1>
<table>
<tr>return to register page<br> </tr>
<tr>return to home page </tr>
</table>
<br>
<div>
<form method="post">
{% csrf_token %}
<div>
<div><label>Login/Email </label><input type="text" name="login_name" placeholder="Login/Email"></div>
<div><label>Password </label><input type="password" name="login_password" placeholder="enter password"></div>
<div><input type="submit" value="Login"></div>
</div>
</form>
</div>
and here is register form:
register.html
<h1>Register page</h1>
<table>
<tr>return to login page <br></tr>
<tr>return to home page </tr>
</table>
<br>
<div>
<form method="POST">
{% csrf_token %}
<div>
<div><label>Name </label><input type="text" name="registerFrom_name" placeholder="Enter the name"></div>
<div><label>Surname </label><input type="text" name="registerFrom_surname" placeholder="Enter the surname"></div>
<div><label>Login/Email </label><input type="text" name="registerFrom_login" placeholder="Login/Email"></div>
<div><label>Password </label><input type="registerForm_password" name="registerFrom_password" placeholder="Enter password"></div>
<div><label>Email </label><input type="text" name="registerForm_email"></div>
<div><input type="submit" value="Register"> </div>
</div>
</form>
</div>
Bellow my own backend to handle froms:
view.html
# BACKEND
from django.shortcuts import render
from django.views import View
from . import ValidateUser, RegisterUser
# Create your views here.
CSRF_COOKIE_SECURE = True
class WebServiceView(View):
# INDEX - MAIN PAGE
def indexPage(self, request):
return render(request, "index.html")
def register(self, request):
res = RegisterUser.RegisterUser("user", "user", "login", "test", "emai#email")
res.createUser()
return render(request, "register.html")
def login(self, request):
print("Login function")
res = ValidateUser.ValidateUser('/config/dbConfig.ini', '127.0.0.1') # Connection to mysql database
formParametr = request.POST
print(formParametr)
login = formParametr['register_name']
password = formParametr['register_password']
res.checkUser(login, password.encode("utf8"))
return render(request, "login.html")
Problem rise when i first open register.html and then i will go to the login.html page. Django throw MultiValueDictKeyError at /shop/login.html. I completely don't understand why. As you can see, a key "name" has 'register_name' already. So what can cause problem ?
Below Full error:
'register_name'
Request Method: GET
Request URL: http://127.0.0.1:8000/shop/login.html
Django Version: 2.2.5
Exception Type: MultiValueDictKeyError
Exception Value:
'register_name'
Exception Location: /usr/local/lib/python3.7/dist-packages/django/utils/datastructures.py in __getitem__, line 80
Python Executable: /usr/bin/python3.7
Python Version: 3.7.4
Python Path:
['/home/reg3x/PycharmProjects/lovLevelMusic',
'/usr/lib/python37.zip',
'/usr/lib/python3.7',
'/usr/lib/python3.7/lib-dynload',
'/usr/local/lib/python3.7/dist-packages',
'/usr/lib/python3/dist-packages',
'/usr/lib/python3.7/dist-packages']
It's a KeyError. It's telling you that you don't have a key for register_name in your POST dictionary. And that's because you used login_name in the template.
Really, you should be using Django forms for this, which would a) take care of outputting the fields in the template with the correct names and b) ensure the data was valid and fully populated before you accessed it in the view.
(There are other things in your code that make me very concerned as well. Why have you got login and register methods within a view class? That's not how class-based views work. And why is your URL ending in .html? That's not how Django URLs work. And, most importantly, what is ValidateUser and RegisterUser? Why are you connecting to your database explicitly in each view? Why do you have those classes? That is not how you work with the database in Django. Why are you doing any of this?)

Django forms - unwanted error message on first display of the form

I've been doing the django tutorial and I'm now trying to adapt it to my needs.
In part 04, the turorial teaches us how to write forms.
I have re-used this part of the tutorial to try and write a radio-button select form.
Most of it is working as the different inputs are shown, selectable, and validating does send me to the associated page.
The code is supposed to display an error_message if the form is validated without an answer.
My problem is that this message is already displayed the first time I open the page.
I've looked for the reason everywhere but I seem to be the only one having this problem.
Here is the index.html file
<!DOCTYPE html>
<html>
<head>
<title>Syl - Projects</title>
</head>
<h1>Project List</h1>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
<form action="" method="post">
{% csrf_token %}
{% for choice in project_list %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
<label for="choice{{ forloop.counter }}">{{ choice.name }}</label><br />
{% endfor %}
<input type="submit" value="Accéder au projet" />
</form>
</html>
And here is the views.py file
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse, HttpResponseRedirect
from django.template import RequestContext, loader
from django.core.urlresolvers import reverse
from gantt.models import Project
from gantt.models import *
def index(request):
project_list = Project.objects.order_by('name')
try:
selected_project = project_list.get(pk=request.POST['choice'])
except (KeyError, Project.DoesNotExist):
return render(request, 'gantt/index.html', {
'project_list': project_list,
'error_message': "Vous n'avez pas fait de choix.",
})
else:
return HttpResponseRedirect(reverse('syl:project', args=(selected_project.slug,)))
...
...
Fixed thanks to your answers.
I had to add an IF condition to prevent the call to request.post on the first page load.
Adding this however, meant I had to add another render for the initial page load since it wasn't possible to use the Redirect for that purpose (its arguments didnt exist yet).
Thansk again.
New version of the views.py file :
def index(request):
project_list = Project.objects.order_by('name')
if request.method=="POST":
try:
selected_project = project_list.get(pk=request.POST['choice'])
except (KeyError, Project.DoesNotExist):
return render(request, 'gantt/index.html', {
'project_list': project_list,
'error_message': "Vous n'avez pas fait de choix.",
})
else:
return HttpResponseRedirect(reverse('syl:project', args=(selected_project.slug,)))
else:
return render(request, 'gantt/index.html', {'project_list': project_list})
Try adding an "If request.method == 'POST'" before you try to get your selected project in the view. When this view is loaded with a GET request by someone viewing the page for the first time, your code is already looking for a POSTed variable to use. It isn't there yet, so you get the error on load.
Wrapping that part in such an if block will tell your code "if there is no submission yet, just display the form. If there is a submission, process it." Right now it tries to skip to processing right away, sees that no selection has been made, and throws the error.

How do I change the template being displayed after the user clicks a button?

I'm quite new to Django, so I aplogize if I am making dumb mistakes.
Here is the code I have so far:
For views.py:
def bylog(request):
if request.POST.get('Filter'):
return render(request, 'index.html', context)
filtered_login = Invalid.objects.values_list('login').distinct()
filtered = []
for item in filtered_login:
filtered.append(item[0])
results = {'results': results, 'filtered': filtered}
return render(request, 'bylog.html', context)
Here is a snippet of bylog.html:
<select id>"dropdown">
{% for item in filtered %}
<option value={{ item }}">{{ item }}</option>
{% endfor %}
</select>
<input type="submit" value="Filter" name="Filter" />
My main goal is to get the value from the drop down list, and after the user clicks the Filter button, the value gets passed to another template.
Is that even possible?
Thanks for you help.
The basic for your goal I supose it is to manage POST in django, meaning that you want to send any data/variables from a template to a view and then do any operation with it (send it to another template, or store...)
The basic for this is(Using HTML form, not Django form):
- Create a HTML form in the template
- Add the selects/inputs with the data you want to manage and a button/input to make the post
- Manage the post in the view
EXAMPLE
template form
<form id="" method="post" action=".">
{% csrf_token %}
<select id="any_name" name="any_name">"dropdown">
{% for item in filtered %}
<option value={{ item }}">{{ item }}</option>
{% endfor %}
</select>
<input type="submit" value="Filter" name="Filter" />
</form>
view.py
def your_view(request):
if request.method == 'POST': # If anyone clicks filter, you receive POST method
data = request.POST['any_name']
# Do what you need here with the data
# You can call another template and send this data
# You can change any_name for the variable you want, changing the name and id in the select
#Your view code
I recommend you to read about Django forms because if you need a bigger form, to manage the data of a model with a lot of fields, a Django Form will save you a lot of time
Working with Django Forms