How to jump to anchor with invalid Django form? - python

When my Django form fails to validate it automatically goes back to the page where the form failed but I'd like it to go to an html anchor on the page. Here's the code I have:
def detail(request, post_id):
p = get_object_or_404(Post, pk=post_id)
# get all the comments for the post
comments = p.comment_set.all().order_by('pub_date')
# to add a comment to the post
if request.method=='POST':
form = CommentForm(request.POST)
if form.is_valid():
c = Comment()
c.body = request.POST['body']
c.post = p
c.save()
return HttpResponseRedirect(reverse('journal.views.detail', kwargs={'post_id': post_id}) + "#comment-" + str(c.id))
else:
form = CommentForm()
return render(request, 'posts/detail.html', {'post': p, 'comments': comments, 'form': form})
The form is a comment form at the bottom of a blog post. When the form is invalid I'd like it to jump to the comment form's html anchor at the bottom of the page. Right now it goes to the top of the page and you have to scroll down to notice that there's an error.
I tried a couple things that didn't work. Please help!

I think that is fair enough:
<script>
function goToId() {
if (!window.location.hash) window.location = window.location + '#your-id';
}
{% if form.is_bound %}
$(window).on("load", goToId);
{% endif %}
</script>

One approach is to pass an extra key-value pair in the dict to contain the anchor name, and use JavaScript in the template to jump to it if needed.
Python side:
// ...
return render(request,
'post/detail.html',
{
// ...
'anchor': 'comment'
})
The template:
function goToOnLoadAnchor()
{
window.location.hash('{{ anchor }}'); // Or something with the same effect
}
{% ... %}
<body onload="goToOnLoadAnchor">
{% ... %}
You can also take advantage of custom template tags to add more flexibility.

CBV & jQuery
FormView
def form_invalid(self, form):
return self.render_to_response(self.get_context_data(form=form, anchor='my-form'))
template
<form ... id='my-form'> ... </form>
<script>
$(function () {
{% if anchor %}
$(document.body).animate({
'scrollTop': $('#{{ anchor }}').offset().top
}, 2000);
{% endif %}
});
</script>
Uses jQuery for scrolling (instead of jumping).

With Vanilla JavaScript:
{% if form.is_bound %}
<script>
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("your_id").scrollIntoView();
});
</script>
{% endif %}
It works well with the addEventListener("DOMContentLoaded") listener. Otherwise it can give some weird results.

Related

Django CreateView not updating context data

I have a form in a bootstrap modal, and I want to keep that modal open after submission. I am using CreateView and trying to pass an additional variable to the template in front-end where I could check if the flag is set or not, but the flag is always False even after submission. Here is what I have:
url.py
from django.urls import path
from .views import MescData
urlpatterns = [
path('mesc', MescData.as_view(), name='mesc')
]
views.py
from django.urls import reverse
from django.views.generic.edit import CreateView
from .forms import MescForm
from .models import Mesc
class MescData(CreateView):
model = Mesc
form_class = MescForm
template_name = 'Materials/mesc_data.html'
successful_submit = False # Flag to keep the add entry modal open
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['successful_submit'] = self.successful_submit
return context
def get_success_url(self):
return reverse('mesc')
def post(self, request, *args, **kwargs):
form_class = self.get_form_class()
form = self.get_form(form_class)
self.successful_submit = True
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form, **kwargs):
# self.successful_submit = True
return super(MescData, self).form_valid(form, **kwargs)
And in Template, I'm checking it like this:
{% if successful_submit %}
<h1>Flag is set</h1>
{% endif %}
Is there a way to pass non-form-related data in CreateView to the template without having to change the url.py (i.e. adding variable data to url path)?
EDIT:
I tried printing the self.successful_submit in form_valid() and post() methods and it is indeed being updated to True, but in the template it is still being passed on as False.
This is the core problem: "I have a form in a bootstrap modal, and I want to keep that modal open after submission."
Simple answer: Use Ajax.
We do now have HTML over the wire as a paradigm gaining popularity, but I'm not familiar enough with it to discuss it. So I will use Ajax to show a solution. This particular solution uses a general ajax template, and the result of a post is a rendered Django template that you can use to just replace the HTML in your already rendered page.
Also, few people like JavaScript, but that is not a good enough reason to avoid using it. It is basically mandatory in any web application you're running. Even HTML over the wire uses a minimal amount of JavaScript to accomplish its goals.
First, write your Ajax view. I'm using django-rest-framework classes for this, and I'm providing an example for filling out a Calibration record. You can use whatever form you want; this is just my recipe for handling modals that you want keep open. In this view, I return a JSON response if the POST was successful. Otherwise, I return a rendered Django template.
from rest_framework.generics import (CreateAPIView, RetrieveAPIView,
UpdateAPIView, get_object_or_404)
from rest_framework.renderers import JSONRenderer, TemplateHTMLRenderer
from rest_framework.response import Response
class CalibrationCreateAjaxView(CreateAPIView, UpdateAPIView, RetrieveAPIView):
renderer_classes = (TemplateHTMLRenderer,)
template_name = "documents/form/cal.html"
def post(self, request, *args, **kwargs):
context = self.get_context(request)
calibration_form = context['calibration_form']
if calibration_form.is_valid():
calibration_form.save()
request.accepted_renderer = JSONRenderer()
return Response(status=201)
return Response(context, status=400)
def get(self, request, *args, **kwargs):
return Response(self.get_context(request))
#staticmethod
def get_context(request):
pk = request.GET.get("pk")
calibration_entry = get_object_or_404(CalibrationEntry, pk=pk) if pk else None
return {
'calibration_form': CalibrationFormAjax(request.POST or None, instance=calibration_entry)
}
I have my view template as well. It takes advantage of request.is_ajax, which is being deprecated. You'll need to add some middleware to keep using it. Here's my middleware. Add it to your settings file as well.
class IsAjaxMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
"""
request.is_ajax is being removed in Django 4
Since we depend on this in our templates, we are adding this attribute to request
Please review:
https://docs.djangoproject.com/en/4.0/releases/3.1/#id2
"""
def __call__(self, request):
request.is_ajax = request.headers.get('x-requested-with') == 'XMLHttpRequest'
return self.get_response(request)
general/ajax_modal.html
<!-- {% block modal_id %}{% endblock %}{% block modal_title %}{% endblock %} -->
{% block modal_body %}
{% endblock modal_body %}
general/modal.html
<div class="modal fade" id="{% block modal_id %}{{ modal_id }}{% endblock modal_id %}" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">×</span>
<span class="sr-only">Close</span>
</button>
<h4 class="modal-title">
{% block modal_title %}{{ modal_title }}{% endblock modal_title %}
</h4>
</div>
<div class="modal-body">
{% block modal_body %}
{% endblock modal_body %}
</div>
</div>
</div>
</div>
Even though, we're using Crispy Forms, you can get away without using it. I also have a general templatetag library that renders any errors on a form. You can write your own.
documents/form/cal.html
{% extends request.is_ajax|yesno:'general\ajax_modal.html,general\modal.html' %}
{% load crispy_forms_tags general %}
{% block modal_id %}new-cal-modal{% endblock modal_id %}
{% block modal_title %}Enter Calibration Information{% endblock modal_title %}
{% block modal_body %}
<div id="new-cal-form-container">
<form action="{% url 'calibration-create' %}" method="post" id="new-cal-modal-form" autocomplete="off" novalidate>
{% if request.is_ajax %}
{% crispy calibration_form %}
{% form_errors calibration_form %}
{% endif %}
<button type="submit" name="Submit" value="Save" class="btn btn-success button" id="submit">save</button>
</form>
</div>
{% endblock modal_body %}
So now that the Ajax view is all set up, I go back to the main page that will render the modal dialog when the user clicks a button. I have a block called "extraContent" in which I include the template of the modal form.
{% block extraContent %}
{% include 'documents/form/cal.html' %}
{% endblock extraContent %}
And now, the JavaScript, which requires jQuery, that I've added to the template. I guess I made my own jQuery plugin on top of that...
$.fn.modalFormContainer = function(optionsObject) {
//call it on the modal div (the div that contains a modal-dialog, which contains modal-header, modal-body, etc
// we expect there to be a form within that div, and that form should have an action url
// when buttons that trigger the modal are clicked, the form is fetched from that action url and replaced.
// optionsObject has formAfterLoadFunction and ajaxDoneFunction
var options = $.extend({}, $.fn.modalFormContainer.defaults, optionsObject);
var $modalFormContainer = $(this);
// get the buttons that trigger this modal to open
// add a click event so that the form is fetched whenever the buttons are clicked
// if data-pk is an attribute on the button, apply that to the querystring of the
// ajaxURL when fetching the form
var modalID = $modalFormContainer.prop("id");
var modalFormButtonSelector = "[data-target=#" + modalID + "][data-toggle=modal]";
function handleModalButtonClick(event) {
//does the button have an associated pk? if so add the pk to the querystring of the ajax url
// this is wrapped in a form so that it gets replaced by the ajax response.
var $button = $(this);
if (!$button.hasClass("disabled") && !$button.prop("disabled")) { //only do it if the button is "enabled"
var $placeholder = $("<form><h1>loading...</h1></form>");
var $modalForm = $modalFormContainer.find("form");
var ajaxURL = $modalForm.prop("action");
$modalForm.replaceWith($placeholder);
var pk = $button.data().pk;
if (pk) {
if (ajaxURL.indexOf("?") > 0) {
ajaxURL += "&pk=" + pk;
} else {
ajaxURL += "?pk=" + pk;
}
}
//fetch the form and replace $modalFormContainer's contents with it
$.ajax({
type: "GET",
url: ajaxURL
}).done(function(response) {
// re-create the form from the response
$modalFormContainer.find(".modal-body").html(response);
$modalForm = $modalFormContainer.find("form"); //we would still need to find the form
options.formAfterLoadFunction($modalForm);
});
} else {
return false; //don't trigger the modal.
}
}
//using delegation here so that dynamically added buttons will still have the behavior.
// maybe use something more specific than '.main-panel' to help with performance?
$(".main-panel").on("click", modalFormButtonSelector, handleModalButtonClick);
$modalFormContainer.on("submit", "form", function(event) {
// Stop the browser from submitting the form
event.preventDefault();
var $modalForm = $(event.target);
var ajaxURL = $modalForm.prop("action");
$modalForm.find("[type=submit]").addClass("disabled").prop("disabled", true);
var formData = $modalForm.serialize();
var internal_options = {
url: ajaxURL,
type: "POST",
data: formData
};
// file upload forms have and enctype attribute
// we should not process files to be converted into strings
if ($modalForm.attr("enctype") === "multipart/form-data") {
internal_options.processData = false;
internal_options.contentType = false;
internal_options.cache = false;
formData = new FormData($modalForm.get(0));
internal_options.data = formData;
}
$.ajax(internal_options).done(function(response) {
// blank out the form
$modalForm.find("input:visible, select:visible, textarea:visible").val("");
// remove errors on the form
$modalForm.find(".has-error").removeClass("has-error");
$modalForm.find("[id^=error]").remove();
$modalForm.find(".alert.alert-block.alert-danger").remove();
// hide the modal
$(".modal-header .close").click();
options.ajaxDoneFunction(response);
}).fail(function(data) {
// re-create the form from the response
$modalFormContainer.find(".modal-body").html(data.responseText);
options.formAfterLoadFunction($modalForm);
});
});
return this;
};
$.fn.modalFormContainer.defaults = {
formAfterLoadFunction: function($form) { return; },
ajaxDoneFunction: function(response) { return; }
};
$("#new-cal-modal").modalFormContainer({
formAfterLoadFunction: function($modalForm) {
$(".datetimeinput").datepicker('destroy');
$(".datetimeinput").datepicker();
},
ajaxDoneFunction: function(event) {
location.reload();
}
});
So upon reviewing this, I've realized that this recipe is far more complicated than I have tricked myself into believing. I sincerely apologize for that. I hope that you can review the code and get an idea of what is happening. There are some edge cases, such as dealing with dates and file uploads, that this recipe handles right now, but you may not actually need them. I should mention that the application that this came from is using Bootstrap 3, so its styling is not updated to the current Bootstrap 5 as of this writing. I should add that the main content of the app has a class of "main-panel" as used in this not-so-generic jQuery plugin.
I'm worried that I've gone and overwhelmed you into maintaining your position of trying to keep using a standard POST request. I guess you could just re-render the template with your POST since it'll be standard practice in your project. You could still get away without using a query string that way.

sending value(a string) from html input to python script (views.py) with Django

I have a sentence that was made in java in html (var Output).
I want to send this var to views.py to analyse. I saw several similar posts in "stackoverflow" and follow the instructions. still I do not get the response. here is part of my code:
Hope you could help me please;
ODMS2FixEnd.html
<div>
....
<form id="Sertegh" name="OUTPUT" method= 'POST' action="{% url 'Calculator' %}">
{% csrf_token %} <input id="outputtext1" name="outputtext1">
</form>
...
</div>
<script>
function....
document.getElementById("outputtext1").value = OutPut;
...
</script>
urls.py
...
url(r'^myprojects/ODMS2FixEnd', blog_views.ODMS2FixEnd, name= 'Calculator'),
...
views.py:
In here I have tried many things one is this:
def ODMS2FixEnd(request):
if 'outputtext1' in request.POST:
StringInput0 = request.POST.get('outputtext1')
print(StringInput0) #just to check
print("OK") #just to check
return render(request, 'ODMS2FixEnd.html', {'StringInput0'})
else:
print("NOOOOOOO") #just to check
return render(request, 'ODMS2FixEnd.html')
Answering this from my phone so hope you understand. I will give a short answer on how to handle data on post, but search on how to do this as well to get deeper understanding.
if request.method == "post":
var = request.form.get("id", none)
I had practice your advice and learned deeper. I now write the below code, But when I want to submit the value inside the input tag, page pops up with 403 CSRF verification failed. Request aborted.. would you please tell me whats wrong
here is my new code:
views.py
class ODMS2FixEndView(TemplateView):
template_name = 'ODMS2FixEnd.html'
def get(self, request):
form = HomeForm()
return render(request, self.template_name, {'form': form})
#csrf_exempt
def post(self,request):
form = HomeForm(request.POST)
if form.is_valid():
print("YESSSS")
text = request.form['post']
print(text)
form = HomeForm()
args = {'form': form, 'text': text}
return render(request, self.template_name, args)
else:
print("NOOOO")
return render(request, self.template_name)
ODMS2FixEnd.html
<!DOCTYPE html>
{% extends 'base.html' %}
<meta name="csrf_token" content="{{ csrf_token }}">
....
<div id="forID" class="container">
<form method="post" id="form1ID" action="">{% csrf_token %}
<input type="text" id="OUtPUTCal">
<button type="submit"> Calculate</button>
</form>
<h2>{{ text }}</h2>
</div>
<script>
.....
document.getElementById("OUtPUTCal").value=TEXT4;
}
......
</script>
urls.py
urlpatterns = [
....
url(r'^myprojects/ODMS2FixEnd', TemplateView.as_view(template_name =
'ODMS2FixEnd.html'), name='ODMS2FixEnd'),
....
form.py
from django import forms
class HomeForm(forms.Form):
post = forms.CharField()

How to run a view function containing an operation parameter without reloading page

I am creating a site which allows users to post comments and like posts. I have created a view function which allows a user to like posts but am not sure how to implement ajax or a similar technique to perform the request without reloading the page. So that the user doesn't lose where they were on the page and have to scroll down to find it after liking a post.
In my views.py:
#this is the function i wish to run without reloading
def like(request, operation, id):
like = Post.objects.get(id=id)
if operation == 'add':
Like.make_like(request.user, like)
elif operation == 'remove':
Like.lose_like(request.user, like)
return HttpResponseRedirect('/Feed/')
#this is the view in which it is called
class FeedView(TemplateView):
template_name = 'feed.html'
def get(self, request):
form = HomeForm()
posts = Post.objects.all().order_by('-created')
users = User.objects.exclude(id=request.user.id)
try:
like = Like.objects.get(user=request.user)
likes = like.posts.all()
except Like.DoesNotExist:
like = None
likes = None
args = {
'form': form, 'posts': posts, 'users': users, 'likes': likes
}
return render(request, self.template_name, args)
def post(self, request):
form = HomeForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.user = request.user
post.save()
text = form.cleaned_data['post']
form = HomeForm()
return HttpResponseRedirect('/Feed/')
args = {'form': form, 'text': text}
return render(request, self.template_name, args)
feed.html:
{% for post in posts %}
<p class="name">{{ post.user.username }}</p>
<h3>{{ post.post }}</h3>
{% if not post in likes %}
<li-r><a class="like" href="{% url 'like' operation='add' id=post.id %}"> {{ post.total_likes }} </a></li-r>
{% endif %}
{% if post in likes %}
<li-r><a class="unlike" href="{% url 'like' operation='remove' id=post.id %}"> {{ post.total_likes }} </a></li-r>
{% endif %}
<p class="date">{{ post.created }}</p>
{% endfor %}
urls.py:
urlpatterns = [
url(r'^Feed/$', FeedView.as_view(), name='feed'),
url(r'^like/(?P<operation>.+)/(?P<id>\d+)/$', views.like, name='like')
]
I have tried putting the function inside of the Feed class but I don't know how to write the url to include the class along with the "(?P<operation>.+)/(?P<id>\d+)" section or even if that would solve my problem. But I think I can use ajax though i have no idea how.
You need to use Ajax here. Use onclick event listener on the anchor tags for like/dislike, make an ajax call instead and update the count based on the response.
$('.like, .unlike').click(function(e){
e.preventDefault();
var url = $(this).attr('href'));
// make a get call on this url, and update the total_likes based on response
$.get(url, function( data ) {
$(this).text(data.total_likes); //something like that
});
});

Updating an HTML fragment with ajax response Django using CBV(ListView)

So I have a homepage that consists of a base listview which includes all of the objects from my db(as shown in the cbv .all() query). What I wanted to do is include a search filter, hence I thought it would be a good idea to isolate it into a separate "list.html" fragment in order to make it reusable later on. Currently, I have an ajax call that sends information to the cbv and the return is a render to list.html fragment. However, when I visit the homepage, the page doesn't get rendered to begin with.
Help or advice would be very much appreciated, thank you again.
urls.py
urlpatterns = [
url(r'^$', exp_view.DrugListView.as_view() , name = 'drug_list')]
here is my CBV template:
views.py
class DrugListView(ListView):
context_object_name = 'drugs'
model = Drug
template_name = 'expirations/drug_list.html'
def get(self, request):
if self.request.is_ajax():
text_to_search = self.request.GET.get('searchText')
print(text_to_search)
return render(request, "expirations/list.html", {'drug':Drug.objects.filter(name__contains = text_to_search).order_by('name')})
else:
return render(request, "expirations/list.html", {'drug':Drug.objects.all().order_by('name')})
here is
drug_list.html
{% extends 'expirations/index.html' %}
{% block content %}
{% include 'expirations/list.html' %}
{% endblock %}
{% block javascript %}
<script>
$(document).ready(function(){
var input = $("#searchText")
input.keyup(function() {
$.ajax({
type: "GET",
url: "{% url 'drug_list' %}",
data: {'searchText' : input.val()},
success: function(data){
$("#list_view").load("expirations/list.html")
}
})
});
})
</script>
{% endblock %}
here is list.html:
{% for drug in drugs %}
<div class = '.col-sm-12'>
<ul class = 'list-group'>
<li class = "list-group-item" >
<span class = "badge">First Expiration: {{ drug.early_exp|date:"l M d Y" }}</span>
{{ drug.name }}
{% regroup drug.expiration_dates.all by facility as facility_list %}
{% for x in facility_list %}
<span class="label label-info">{{ x.grouper }}</span>
{% endfor %}
</li>
</ul>
</div>
{% endfor %}
You've misunderstood what you need to do in your success callback.
That function has no access to Django templates. But it doesn't need to, because Django has already sent the fragment in the response, which is available in the data parameter. You just need to insert that into the existing page.
For that you'll need a container; so you should modify your drug_list.html so that there is a div around the include:
{% block content %}
<div id="list_contents">
{% include 'expirations/list.html' %}
</div>
{% endblock %}
and now your success function can be:
success: function(data) {
$("#list_contents").html(data)
}
I do not think that it will work this way. For Ajax calls and CBV look at example in the docs.
Then you would need to return the new, filtered data (in html format) to the ajax call and replace the html in the sucess function via JS.
Daniel Roseman definitely helped solve the puzzle, here is the updated code:
views.py: updated if/else statement to include a default list view of the general template. also added the context name 'drugs' instead of 'drug'
class DrugListView(ListView):
context_object_name = 'drugs'
model = Drug
template_name = 'expirations/drug_list.html'
def get(self, request):
if self.request.is_ajax():
text_to_search = self.request.GET.get('searchText')
print(text_to_search)
return render(request, "expirations/list.html", {'drugs':Drug.objects.filter(name__contains = text_to_search).order_by('name')})
else:
return render(request, "expirations/drug_list.html", {'drugs':Drug.objects.all().order_by('name')})
drug_list.html: changed success callback function load html as suggested by daniel.
{% extends 'expirations/index.html' %}
{% block content %}
<div id = 'list_view'>
{% include 'expirations/list.html' %}
</div>
{% endblock %}
{% block javascript %}
<script>
$(document).ready(function(){
var input = $("#searchText")
input.keyup(function() {
$.ajax({
type: "GET",
url: "{% url 'drug_list' %}",
data: {'searchText' : input.val()},
success: function(data){
$("#list_view").html(data)
}
})
});
})
</script>
{% endblock %}

How do I send an array of objects from a django class based view to my index.html file?

The Goal:
I want to use FullCalendar(https://fullcalendar.io/) to display event objects from my database. FullCalendar accepts an array of event objects as a property.
The Problem I'm having:
I can send context data back to the template with the event objects but as far as I know I can only interact with the data using Django's template tagging system. *EDIT: When I replace the hardcoded array with ptoHistory I receive the following error in the chrome console:
jquery-3.1.1.min.js:2 Uncaught ReferenceError: ptoHistory is not
defined
at HTMLDocument. ((index):141)
at j (jquery-3.1.1.min.js:2)
at k (jquery-3.1.1.min.js:2)
index.html:
{% extends 'base.html' %}
{% block content %}
//ACCESSING THE CONTEXT DATA LIKE THIS WORKS BUT I CAN'T USE ptoHistory IN MY FULLCALLENDAR SCRIPT
{% for history in ptoHistory %}
<li>{{obj.leave_type}}</li>
{% endfor %}
<div class="container">
<div id="calendar">
<!-- Calendar is injected here -->
</div>
<!----------------------- SCRIPTS ----------------------------->
<script>
$(document).ready(function() {
$('#calendar').fullCalendar({
defaultView:'month',
editable: true,
// MY ARRAY OF OBJECTS WILL REPLACE THIS HARDCODED ARRAY
events: [
{
title: 'All Day Event',
start: '2017-01-12',
},
{
title: 'Meeting',
start: '2017-01-13T10:30:26',
end: '2014-06-13T12:30:00'
},
],
});
});
</script>
{% endblock content%}
IndexView.py:
class IndexView(FormView):
template_name = 'accounts/index.html'
form_class = PtoRequestForm
success_url = 'login/'
def form_valid(self, form):
form.save()
return super(IndexView, self).form_valid(form)
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
context['ptoHistory'] = PtoHistory.objects.all()
print(context['ptoHistory'])
return context
Could someone point me in the right direction?
You are right, it won't work. Django template system is able to process python objects because they are executed already before the template is finished rendering.
You could still assign python list in javascript, though, but not with python objects but json string. The solution is basically compose for only what you need in the views using values() then . I don't know what fields do you need for PtoHistory, but you could do:
# views.py
import json
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
ptoHistory = json.dumps(list(PtoHistory.objects.values()))
# or do this if you only need several fields' value
# ptoHistory = json.dumps(list(PtoHistory.objects.values('field1', 'field2')))
context['ptoHistory'] = ptoHistory
return context
// in javascript
$(document).ready(function() {
$('#calendar').fullCalendar({
defaultView:'month',
editable: true,
var histories = {{ ptoHistory|safe }};

Categories