Django object update using htmx and SingleObjectMixin - django

I'm using htmx for the first time. I have a table where each cell is a grade object. Previous to htmx I made each cell a link to an UpdateView for the object. I am now trying to have the user modify the the object's score field directly in the table using htmx. I'm sure I'm doing several things wrong.
My page loads as expected and the table is displayed as expected. when I type in a cell, I get an error Forbidden 403. CSRF Verification Failed.
The purpose of this post/question is to figure out how to get past this 403 error. Having said that, if it turns out that I'm going down the completely wrong path with using a SingleObjectMixin, please let me know.
View
class GradeChange(SingleObjectMixin, View):
""" view to handle htmx grade change"""
model = Grade
def post(self, request, *args, **kwargs):
grade = self.get_object()
assessment = Assessment.objects.get(grade=grade.pk)
print(assessment)
classroom = Classroom.objects.get(classroom=grade.cblock)
print(classroom)
if grade.score == "EXT" or grade.score=="APP" or grade.score=="DEV" or grade.score=="BEG":
grade.save()
return HttpResponse("S")
else:
return HttpResponse("")
template
<table class="table table-bordered table-sm">
<thead>
<tr>
<th class="col-3" scope="col">Students</th>
{% for obj in objective_list %}
<th class="col-2" scope="col">{{ obj.objective_name }}</th>
{% endfor %}
<th scope="col">Comments</th>
</tr>
</thead>
<tbody>
<form action="" method="post" class="form-group">
{% csrf_token %}
{% for student in student_list %}
<tr>
<td >{{ student.student_first }} {{ student.student_last }}</td>
{% for g in grade_list %}
{% if g.student.id == student.id %}
<td>
<input type="text" hx-post="{% url 'gradebook:grade-change' g.pk %}" hx-target="" hx-trigger="keyup delay:2s" class="form-control score" title={{ g.score }} name="score" id="input-{{ forloop.counter0 }}" placeholder={{ g.score }} required>
</td>
{% endif %}
{% endfor %}
<td>
{% for comms in comment_list %}
{% if comms.student == student %}
<a class="grade-comment" href="{% url 'gradebook:addcomment' comms.pk assess_pk class_pk %}">{{ comms.comment|truncatewords:10 }}</a>
{% endif %}
{% endfor %}
</td>
</tr>
{% endfor %}
</form>
</tbody>
</table>
EDIT - a solution
I came across this, which works.
<script>
document.body.addEventListener('htmx:configRequest', (event) => {
event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
});
</script>

The CSRF token must be included in the HTMX POST request. Currently you include it only in the regular form, but the HTMX request is initiated from a child input element where the token is not present. So just include the following hx-header attribute to a parent element like the <form> or event the <body> is a good candidate:
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>

Related

HTMX form submission produces a duplicate form

{% extends "IntakeApp/base3.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% block heading %}
<h2>Allergies for {{request.session.report_claimant}}</h2>
{% endblock %}
{% block content %}
<form hx-post="{% url 'allergy' %}" hx-target="#allergy_target" hx-swap="outerHTML">{% csrf_token %}
<div class="form-row">
<div class="form-group col-md-2 mb-0">
{{ form.allergen|as_crispy_field }}
</div>
</div>
<button type="submit" class="btn btn-primary">Add</button>
</form>
<div class="container-fluid">
<table class="table table-striped table-sm" id="med-table">
<thead>
<tr>
<th>Allergen</th>
</tr>
</thead>
<tbody id="allergy_target">
{% for allergy in allergy_list %}
<tr>
<td>{{allergy.allergen}}</td>
<td>
<form method="POST" action="{% url 'allergy-delete' allergy.id %}">{% csrf_token %}
<input class="btn btn-danger btn-sm" type="submit" value="Delete">
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
class AllergyCreateView(generic.CreateView):
model = Allergy
template_name = 'IntakeApp/allergy_form.html'
form_class = AllergyForm
def form_valid(self, form):
form.instance.assessment = Assessment.objects.get(id=self.request.session['assessment_id'])
return super(AllergyCreateView, self).form_valid(form)
def get_success_url(self):
return reverse_lazy("allergy")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
assessment_id = self.request.session['assessment_id']
allergy_list = Allergy.objects.filter(assessment=assessment_id)
context["allergy_list"] = allergy_list
return context
I tried to all the different hx-swap options, but none fix it...It does post correctly and the swap does work, just not sure why I am getting another form in there. Please help
I added the View above. I think thats were my issue is...not sure if I am supposed to be doing this way or not?
Seems like you're trying to render the same html content in your view, which instead should only take a specific element.
You can try to use hx-select="#allergy_target" so htmx will only fetch the table content.

Django only show first POST element in loop

I have simple library page that user can register, add and edit\delete records from library.
Template.html
{% extends "book/base.html" %}
{% block content %}
<div class="row justify-content-center mt-5">
</div>
{% if books %}
{% for book in books %}
<div class="table-users">
<div class="header"> {{ book.title }} </a> </div>
<table cellspacing="0">
<tr><center>
<th>Name</th>
<th>Author</th>
<th>Delete</th>
</center>
</tr>
<td> {{ book.name }} </td>
<td> {{ book.author }} </td>
<td> <form class="delete" method="POST" action="{% url 'deletebook' book.id %}"> {% csrf_token %} <button class="delete" type="submit" class="btn btn-warning">Delete</button></td>
views.py:
def deletebook (request, book_pk):
book = get_object_or_404(Book, pk=book_pk, user=request.user)
if request.method == 'POST':
book.delete()
return redirect('currentbooks')
With this loop only first element of POST method is active and working. Actually I'd debuggging html after rendered and i've seen that theres a only first element has a POST method. I searched on google and on stackoverflow and found something about the change id to class. But my template doesnt have any id. I also tried to move {% csrf_token %} outside of the loop but it doesnt work either. I think so, i missed something important here. I really appreciate if someone could help me out. Thank you in advance.

got an unexpected keyword argument 'id'

I'm trying to do a query when the object has the status = 'Opened'. And display in a table where I will have a button to give a solution for my form and change status='Pending'. But when I click in the button I get this error.
What I'd like to do for really is display was a form for each data, but when I do a for loop for each one form my data insnt show, how you can see my editable.html. I just get the buttons to do an action, and they are working fine.
url:
path('manutencao_os_status/<int:id>', views.manutencao_ordem_de_servico, name='manutencao_os_status'),
path('manutencao_ordem_de_servico/', views.manutencao_ordem_de_servico, name='manutencao_ordem_de_servico'),
views.py
def manutencao_ordem_de_servico(request):
ordem_servico = OrdemServico.objects.filter(status='Aberto').order_by('id')
form = FormOrdemServico(ordem_servico)
if request.method != 'POST':
return render(request, 'ordemservico/manutencao_ordem_de_servico.html', {
'form': form,
'ordem_servico': ordem_servico
})
form = FormOrdemServico(request.POST, ordem_servico)
if not form.is_valid():
return render(request, 'ordemservico/manutencao_ordem_de_servico.html', {
'form': form,
'ordem_servico': ordem_servico
})
ordem_servico.save()
return redirect('ordemservico:manutencao_ordem_de_servico')
def manutencao_os_status(request, id):
ordem_servico = OrdemServico.objects.get(pk=id)
ordem_servico.status = 'Em Aprovação'
ordem_servico.save()
return redirect('ordemservico:manutencao_os_status')
html:
{%extends 'base.html' %}
{%block conteudo %}
<h1>Ordens de Serviço</h1>
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="card card-primary">
<div class="table table-bordered">
<table class="table table-bordered">
<thead>
<tr>
<td>Condition:</td>
<td>ID:</td>
<td>Name:</td>
<td>Status:</td>
<td>Solution:</td>
</tr>
</thead>
<tbody>
{% for os in ordem_servico %}
<tr>
<td>
<a href="{% url 'ordemservico:manutencao_os_status' os.id %}"
class="btn btn-success">Aprovar</a>
</td>
<td>{{os.id}}</td>
<td> {{os.name}}</td>
<td>{{os.status}}</td>
<td>{{os.solution}}</td>
</tr>
{%endfor%}
</tbody>
</table>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
my editable.html:
{%for os in ordem_servico %}
<form action="{% url 'ordemservico:manutencao_os_status' os.id %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
<fieldset>
<legend><h2>Ordens de Serviço</h2></legend>
<table class="table">
{{ os.form }}
<tr>
<td colspan="2">
<button type="submit" class="btn btn-primary">Solucionar</button>
</td>
</tr>
</table>
</fieldset>
</form>
{%endfor%}
{% endblock %}
You routed to the wrong view, it should be:
path(
'manutencao_os_status/<int:id>',
views.manutencao_os_status,
name='manutencao_os_status'
),
In the view you should likely redirect to the manutencao_ordem_de_servico view:
def manutencao_os_status(request, id):
ordem_servico = OrdemServico.objects.filter(pk=id).update(
status='Em Aprovação'
)
return redirect('ordemservico:manutencao_ordem_de_servico')
Note: A GET request is not supposed to have side-effects, hence updating
objects when a user makes a GET request, is not compliant with the HTTP
standard. Therefore it might be better to update a OrdemServico with a POST request.

request POST not working with <TABLE>

With Django 1.8 template, tried to POST table data. I set <TABLE> id = 'tasktable'. But in request information, there's no data of 'tasktable'
Also, how can I access dynamic table POST data in Django views?
Is only ways to make id each row?
(like <td><input id='1_data'></td><td><input id='2_data'></td>
POST request info
Below Code is template.
{% extends "base.html" %}
{% load staticfiles %}
{% block scripts %}
<script src="//code.jquery.com/jquery.min.js"></script>
<script>
$(function () {
$('#btn-add-row').click( function() {
$('#tasktable > tbody:last').append('<tr><td><input type="text"></td><td><select name="format"><option value="INT">INT</option><option value="TEXT">TEXT</option></select></td></tr>');
});
$('#btn-delete-row').click( function() {
$('#tasktable > tbody:last > tr:last').remove();
});
});
</script>
{% endblock %}
{% block title %}
Angel Feeder - Manager
{% endblock %}
{% block body %}
<div class="container">
<form class="" method="post" action="">
{% csrf_token %}
{{ taskform }}
<br>
<button id="btn-add-row" type="button">+</button><button id="btn-delete-row" type="button">-</button>
<table name='tasktable' id='tasktable' class="table">
<thead>
<tr>
<th>Data name</th>
<th>Format</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text"></td>
<td><select name="format"><option value="INT">INT</option><option value="TEXT">TEXT</option></select></td>
</tr>
</tbody>
</table>
<button class="btn btn-lg btn-primary" type="submit">set task</button>
</form>
</div>
{% endblock %}
Only form elements with a name attribute will have their values passed when submitting a form.
name is not an allowed attribute of <table> in HTML5
https://stackoverflow.com/a/13677670/3033586
Obviously you have to set unique names for each form element inside table>tr>td so you can reference it after post
If you want simple styled [model]forms (for example with bootstrap) - look at http://django-crispy-forms.readthedocs.org/en/latest/

Django model filter not pulling in object

I'm using Django 1.4 with Python 2.7 on Ubuntu 12.04.
I have a template that is supposed to show a product and a list of product features for each product and for some reason the features don't show in the template.
Here is the view:
#login_required
def view_products(request):
"""
.. function:: view_products()
View the Products
:param request: Django Request object
"""
data = { 'user' : request.user }
if (request.user.is_authenticated() and request.user.is_superuser):
products = Products.objects.all()
add_feature_form = rsb.forms.AddProductFeatureForm();
data.update({ 'form' : add_feature_form })
data.update({ 'products' : products })
data.update(csrf(request))
return render_to_response("view_products.html", data)
return render_to_response("index.html", data)
Here is the portion of the template working with the product features:
<table>
{% for product in products %}
<tr>
<td align="right">Product Name:</td><td>{{ product.name }}</td>
</tr>
<tr>
<td align="right">Price:<br /></td><td>${{ product.price }}</td>
</tr>
<tr>
<ul>
{% for productfeature in product.productfeature_set.all %}
<form action="/removeProductFeature/" method="post">{% csrf_token %}
<li>
{{ productfeature.feature }}
<input type="hidden" name="feature" value={{ productfeature.feature }}>
<input type="hidden" name="product_id" value={{ product.id }}>
<label class="formlabel"> </label><input type="submit" value="Remove ►">
</tr>
</form>
{% endfor %}
</ul>
</tr>
<tr>
<form action="/addProductFeature/" method="post">{% csrf_token %}
<table>
<tr>
<td align="right"><label class="formlabel">Add Feature:<br /></label></td><td>{{ form.feature }}</td>
</tr>
<input type="hidden" name="product_id" value={{ product.id }}>
<tr>
<td align="right"><label class="formlabel"> </label></td><td><input type="submit" value="Add ►"></td>
</tr>
</form>
</table>
</tr>
{% endfor %}
</table>
Basically this template should show you a product. Each feature will be listed below it with the option to "remove" that feature. Then, at the bottom, a field that allows you to add additional features.
The existing features do not show up at all. Any suggestions on what I might be doing wrong?
UPDATE 1:
I missed an s in the template. product.productfeatures_set.all not product.productfeature_set.all. I'm good to go. Thanks all!
Please don't do this:
product_features = []
for product in products:
features = ProductFeatures.objects.filter(product = product)
product_features.append(features)
product.features = product_features
Instead, just pass your products variable to the template context.
And in the template do:
{% for product in products %}
Product id: {{ product.pk }}
{% for productfeature in product.productfeature_set.all %}
{{ productfeature.feature }}
{% endfor %}
{% endfor %}
What is productfeature_set you would ask (or, I hope you would ask :D), and that's a very good question. Don't panic, it's all documented.
Now, this is going to cause subqueries to spawn. The solution is to use prefetch_related. But you don't have to worry about that for the moment I think :)
Yes, you are trying to implement something that is already build into Django!
You should use the build in reverse relations of django.
# give an Product instance product
# this should work
product.productfeature_set.all()
Which is accessible from the template a product.productfeature_set.all
and can be itterated over.