I have a page with a POST form, that have a action set to some url.
i.e assume this page url is /form_url/ :
..
The view in /submit_url/ take care of the form data. After this, I want to return the same page of the form with a success message.
In the view that take care for the POST form, I use HttpResponseRedirect, in order to "clear" the form data from the browser.
But in this way I can't display a message in the form page, unless I do something like:
return HttpResponseRedirect("/form_url/?success=1")
and then check for this parameter in the template. I don't like this way, since if the user refreshes the page, he will still see the success message.
I've noticed that in django admin site, the delete/add of objects does use redirect after POST submit, and still display a success message somehow.
How?
I've already briefly seen django "messaging" app, but I want to know how it work first..
The django admin uses django.contrib.messages, you use it like this:
In your view:
from django.contrib import messages
def my_view(request):
...
if form.is_valid():
....
messages.success(request, 'Form submission successful')
And in your templates:
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li {% if message.tags %} class=" {{ message.tags }} " {% endif %}> {{ message }} </li>
{% endfor %}
</ul>
{% endif %}
For Class Based Views use self.request
I also use self.request.path_info in my return
from django.contrib import messages
class MyCreateView(CreateView):
...
def form_valid(self, form):
....
self.object.save()
messages.success(self.request, 'Form submission successful')
return HttpResponseRedirect(self.request.path_info)
Same template as damio's answer:
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li {% if message.tags %} class=" {{ message.tags }} " {% endif %}> {{ message }} </li>
{% endfor %}
</ul>
{% endif %}
from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic.edit import CreateView
from myapp.models import Author
class AuthorCreate(SuccessMessageMixin, CreateView):
model = Author
success_url = '/success/'
success_message = "%(name)s was created successfully"
https://docs.djangoproject.com/en/1.11/ref/contrib/messages/
You don't need to do a redirect to clear the form data. All you need to do is re-instantiate the form:
def your_view(request):
form = YourForm(request.POST or None)
success = False
if request.method == 'POST':
if form.is_valid():
form.save()
form = YourForm()
success = True
return render(request, 'your_template.html', {'form': form})
If the user refreshes the page, they're going to initiate a GET request, and success will be False. Either way, the form will be unbound on a GET, or on a successful POST.
If you leverage the messages framework, you'll still need to add a conditional in the template to display the messages if they exist or not.
Django messages framework stores the messages in the session or cookie (it depends on the storage backend).
If you're using a classed based view with django forms, and granted you've got the messages.html template set up, you can simply pass the 'SuccessMessageMixin' into your view like below
class YourView(SuccessMessageMixin,View):
success_message = 'your message_here'
This will display your message upon form success
Related
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"])
I'm making a messaging system which has a message model and some views for things like inbox and conversation.
Inbox view and template works and is linked to the conversation through the id of the sender. However, when I click on the senders name (The senders name is the link (template link is {{ sender }}) that is meant to trigger the conversation view which is supposed to render the template convo.html ( the project level url for that is url(r'^messaging/inbox/conversation/(?P<id>[\w-]+)/$', message_views.Conversation.as_view(), name="conversation"),) ), Django changes the url but stays on the same page(So it triggers the inbox view and stays on inbox.html and changes the url but doesn't trigger the conversation view and doesn't render the convo.html template). I've tried changing from a function based view to a class based view and it did nothing. What am I doing wrong?
Note: I'm still new to django so forgive me for any bad practices; I'm attempting to teach myself.
views.py (inbox and conversation views)
def inbox(request):
messages = Message.objects.filter(receiver=request.user.id)
senders = {}
for message in messages:
if message.sender not in senders:
senders[message.sender] = message
return render(request, 'inbox.html', {'senders': senders})
# There is only ever one of this view, either the CBV or FBV
def conversation(request, other_user_id):
print(request.method)
if request.method == "POST":
reply = request.post['reply']
other_user = CustomUser.objects.get(id=other_user_id)
message = Message(sender=request.user, receiver=other_user)
message.save()
return redirect("conversation")
else:
other_user = CustomUser.objects.get(id=other_user_id)
messages = Message.objects.filter(Q(receiver=request.user) & Q(sender=other_user))
print(messages)
return render(request, 'convo.html', {'messages': messages})
# Class based conversation view
class Conversation(View):
def post(request, other_user_id):
reply = request.post['reply']
other_user = CustomUser.objects.get(id=other_user_id)
message = Message(sender=request.user, receiver=other_user)
message.save()
return redirect("conversation")
def get(request, other_user_id):
other_user = CustomUser.objects.get(id=other_user_id)
messages = Message.objects.filter(Q(receiver=request.user) & Q(sender=other_user))
print(messages)
return render(request, 'convo.html', {'messages': messages})
template - inbox.html
<!DOCTYPE html>
{% extends "accbase.html" %}
<!--Omitted outbox in favor of conversation-->
{% block content %}
<div class="inbox">
<h1>Inbox</h1>
<hr>
{% for sender in senders %}
{{ sender }}
<br>
{{sender.message.created_at}}
<br>
{% endfor %}
</div>
{% endblock %}
template - convo.html
<!DOCTYPE html>
<!--Template for reciever side of conversation-->
{% extends "accbase.html"%}
{% block content %}
<div class="conversation">
<h1>Conversation with {{sender}}</h1>
<p>
{% for msg in messages %}
<!--Order is descending ~~ bottom to top: newest to oldest-->
{{ msg.content }}
<br>
{% endfor %}
<hr>
<form method="POST">
<label>Reply</label>
<input type="text" name="reply">
</form>
</p>
<button type="submit">Send</button>
</div>
{% endblock %}
project level urls.py
from django.urls import path, re_path, include
from django.conf.urls import url
from messaging import views as message_views
urlpatterns = [
url(r'^messaging/inbox', message_views.inbox, name="inbox"),
url(r'^messaging/inbox/conversation/(?P<id>[\w-]+)/$', message_views.Conversation.as_view(), name="conversation"),
]
As Daniel Roseman said in comments " The problem here is that you didn't terminate the regex for your inbox URL, so it also matches the conversation URL. " so I added the needed $ to the end of the inbox url pattern and that fixed it!
When i submits the form i want to pass message as "form successfully submitted" to the same page "successs.html".How it possible?
class SuccessView(FormView):
template_name = 'success.html'
form_class = QuestForm
success_url='success'
def form_valid(self, form):
ob9=Logs.objects.all()
for i in ob9:
logids=i.id
str(logids)
q1= form.cleaned_data['ques']
obj1=Quest.objects.create(logid=logids,status=0,question=q1)
return super(SuccessView,self).form_valid(form)
If you use Django's messages framework, it's as simple as adding a message at any point during the call processing (this adds the message to the current request object) and it will be added to the response:
from django.contrib import messages
messages.success("Form successfully submitted")
You can do this just before your call to return super().form_valid(form).
In your template you need to add the code to diplay the messages of course, the example in the documentation shows you exactly how to do that.
The django message Framework offers different ways to do it:
from django.contrib import messages
messages.add_message(request, messages.INFO, 'Your message', extra_tags='more_tags')
# extra_tags is optional
or
messages.success(request,"Your message")
To retrieve it in your template, use something like
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
The following problem is occurring in a large django project. I've been able to replicate the issue in a small mock-up project (code below).
I am trying to use the django messaging framework within an inclusion tag to display a message when a POST'ed form returns is_valid(). This approach has also been used in an another answer here (see 'final update' section).
The problem is that the message is not immediately displayed when the page is rendered after the POST. Instead the message appears the next time you navigate elsewhere or refresh the page after the POST response is received.
I am not receiving any errors. Everything appears to be operating normally, except for the delayed message display.
The reason for this approach is because I'm reusing multiple small forms across multiple apps and I need to use DRY principals for the GET and POST logic. This approach works perfectly - except for the issue with the delayed 'success' message display!
Really appreciate any feedback or assistance!
EDIT: To be clear the line which sets the message is in 'my_template.py':
messages.add_message(context['request'], messages.SUCCESS, "Successfully added entry")
The Demo Project:
settings.py:
...
TEMPLATE_CONTEXT_PROCESSORS = (
"django.core.context_processors.request",
"django.core.context_processors.media",
"django.contrib.messages.context_processors.messages"
)
...
base_layout.html:
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
{% for message in messages %}<div class="alert{% if message.tags %} alert-{{ message.tags }}{% endif %}" role="alert">{{ message }}</div>{% endfor %}
{% block content %}{% endblock %}
</body>
</html>
my_template.html:
<form action="" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit" />
</form>
forms.py:
from django.forms.models import ModelForm
from app.models import ContactMessage
class ContactForm(ModelForm):
class Meta:
model = ContactMessage
fields = ['name']
index.html:
{% extends "app/base_layout.html" %}
{% load my_template %}
{% block content %}
{% my_template %}
{% endblock %}
my_template.py:
from django import template
from django.contrib import messages
from app.forms import ContactForm
register = template.Library()
#register.inclusion_tag('app/my_template.html', takes_context=True)
def my_template(context):
if context['request'].method=='GET':
return { 'form':ContactForm() }
if context['request'].method=='POST':
form = ContactForm(context['request'].POST)
if not form.is_valid():
return { 'form': form }
form.save()
messages.add_message(context['request'], messages.SUCCESS, "Successfully added entry")
return { 'form':ContactForm() }
The messaging framework works through middleware, what you need is some way of informing the posting in the same request/response cycle. You have the context variable at hand, so why not add a value to it:
if form.is_valid():
context['success']=True
else:
context['success']=False
Then in your template:
{%if success %}<div>whoohoo!</div>{%endif%}
According to Django, the messages are queued for rendering until it is cleared by renderer. (reference)
In your case, you are adding messages after {{ message }} tags in base.html has been rendered. So your message is stored until your next view when {{ message }} in base.html is rendered again.
To solve this, you can move your {{ message }} tags behind {% endblock %} of content. Another possible solution is to use javascript to append {{ message }} tags either from my_template.html or from end of base.html.
How to pass the "render_to_response arguments" to an new view?
def show_list(request, id):
try:
profile = request.user.get_profile()
list = Lista.objects.get(user_profile = profile)
except Lista.DoesNotExist:
c = {}
c.update(csrf(request))
titlepage = "ooops, you don't have a list YET! click on button to create one!"
c = {'profile' : profile, 'request' : request, 'titlepage' : titlepage}
return render_to_response('/profiles/list/create/',c,context_instance=RequestContext(request))
with this last line this doesn't work, the url /profiles/list/create/ redirects to a view create_list.
Well, I know that I could write something like redirect(/profiles/list/create/) but with this I cannot pass the dictionary c.
Thanks in advance
You should use django.contrib.messages to display your message and redirect user to another view.
View code:
from django.contrib import messages
from django.shortcuts import redirect
def show_list(request, id):
try:
# normal flow
except Lista.DoesNotExist:
messages.warning("ooops, you don't have a list YET! click on button to create one!")
return redirect("/profiles/list/create/")
Somewhere in create template:
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
Also remember to properly enable messages.
return render_to_response('my_template.html',
my_data_dictionary,
context_instance=RequestContext(request))