Getting ValuError: Field 'id' expected a number but got a html - django

I am a newcomer, about 10 days into a self-taught Django, and I am building a Django (v3.2) app.
The main view in the app is a list of countries (view: CountryListView, template: countries_view.html).
When I click on a country in that list, a detail view is brought up to see detailed country data (CountryDetailView and country_detail.html).
In CountryDetailView, there are navigation buttons: either back to CountryListView, or 'Edit', which shall bring the user to CountryEditView, where the country parameters can be changed & saved.
However, when I click on 'Edit', I get the following error:
Request Method: GET
Request URL: http://127.0.0.1:8000/manage_countries/countries/country_edit.html/
Django Version: 3.2.4
Exception Type: ValueError
Exception Value: Field 'id' expected a number but got 'country_edit.html'
I am guessing this might to do something with values returned (or rather expected but not returned) from CountryDetailView, but what they are? And how to make CountryDetailView to return the object id? (I am using plain integer id's in my model)
views.py
class CountryListView(LoginRequiredMixin, ListView):
model = Countries
context_object_name = 'countries_list'
template_name = 'manage_countries/countries_view.html'
class CountryDetailView(LoginRequiredMixin, DetailView):
model = Countries
template_name = 'manage_countries/country_detail.html'
class CountryEditView(LoginRequiredMixin, UpdateView):
model = Countries
template_name = 'manage_countries/country_edit.html'
success_url = reverse_lazy('manage_countries:countries_view')
urls.py
path('', CountryListView.as_view(),name='countries_view'),
path('countries/<pk>/', CountryDetailView.as_view(), name='country-detail'),
path('<pk>/edit', CountryEditView.as_view(), name='country_edit'),
countries_view.html
{% block content %}
<div class="list-group col-6">
Click here to add country data
{% for country in countries_list %}
<small><span class="text-dark">{{ country.name }}</span></small>
{% endfor %}
</div>
{% endblock content %}
country_detail.html, with two navigation buttons (back to the list), and further to Edit form (this is the one that does not work).
{% block content %}
<div class="card col-5 shadow-mt">
<h5 class="card-header bg-light text-center">Country data</h5>
<div class="card-body">
<table class="table">
<thead><tr>
<th scope="col"><small>Name: </small></th>
<th scope="col"><small>{{ object.name }}</small></th>
</tr></thead>
</table>
<button class="btn btn-secondary mt-3" onclick="javascript:history.back();">Back</button>
<button class="btn btn-secondary mt-3" onclick="window.location.href='../country_edit.html';">Edit</button>
</div>
</div>
{% endblock content %}

Your onclick attribute of button contains invalid url:
<button>onclick="window.location.href='../country_edit.html';">Edit</button>
Use instead template tag url (Django Docs):
<button class="btn btn-secondary mt-3" onclick="window.location.href='{% url 'country_edit' object.pk %}';">Edit</button>

Related

The right way to dynamically add Django formset instances and POST usign HTMX?

I'm making a form with a nested dynamic formset using htmx i (want to evade usign JS, but if there's no choice...) to instance more formset fields in order to make a dynamic nested form, however when i POST, only the data from 1 instance of the Chlid formset (the last one) is POSTed, the rest of the form POSTs correctly and the Child model gets the relation to the Parent model
I read the django documentation on how to POST formset instances and tried to apply it to my code, also i got right how to POST both Parent and Child at the same time. For the formsets i'm making a htmx get request hx-get to a partial template that contains the child formset and that works great, the only problem is that this always returns a form-0 formset to the client side, so for the POST the data repeats x times per field and only takes the data placed in the last instance, however i tried to change the extra=int value on my formset to get more forms upright, this gave the expected result, one Child instance per form in extra=int, so my problem is up with htmx and the way i'm calling the new Child formset instances.
here's my code. (i plan to nest more child formsets inside this form so i call this sformset for conveniece)
****views.py****
def createPlan(request):#Requst for the Parent form
form = PlanForm(request.POST or None)
sformset = StructureFormset(request.POST or None) #Nesting the Child formset
context = {
'form':form,
'sformset':sformset,
}
if request.method == 'POST':
print(request.POST)
if form.is_valid() and sformset.is_valid():
plan = form.save(commit=False)
print(plan)
plan.save()
sform = sformset.save(commit=False)
for structure in sform:
structure.plan = plan
structure.save()
return render(request, 'app/plan_forms.html', context)
def addStructure(request):
sformset = StructureFormset(queryset=Structure.objects.none())#add a empty formset instance
context = {"sformset":sformset}
return render(request, 'app/formsets/structure_form.html', context)
****forms.py****
StructureFormset = modelformset_factory(Structure,
fields = (
'material_type',
'weight',
'thickness',
'provider'
))
****relevant part for plan_forms.html template****
<form method="POST">
{% csrf_token %}
<div class="col-12 px-2">
<div class="row px-3 py-1">
<div class="col-3 px-1">{{ form.format }}</div>
<div class="col-3 px-1">{{ form.pc }}</div>
<div class="col-3 px-1">{{ form.revission }}</div>
<div class="col-3 px-1">{{ form.rev_date }}</div>
</div>
<div class="row px-3 py-1">
<div class="col-3 px-1">{{ form.client }}</div>
<div class="col-3 px-1">{{ form.product }}</div>
<div class="col-3 px-1">{{ form.gp_code }}</div>
<div class="col-3 px-1">{{ form.code }}</div>
</div>
</div>
<div>
<table>
<tbody style="user-select: none;" id="structureforms" hx-sync="closest form:queue">
<!--Structure formset goes here-->
</tbody>
<tfoot>
<a href="" hx-get="{% url 'structure-form' %}" hx-swap="beforeend" hx-target="#structureforms">
Add structure <!--Button to call structure formset-->
</a>
</tfoot>
</table>
</div>
<div class="col-12 px-2">
<div class="row px-4 py-1">{{ form.observation }}</div>
<div class="row px-4 py-1">{{ form.continuation }}</div>
<div class="row px-4 py-1">{{ form.dispatch_conditions }}</div>
<div class="row px-3 py-1">
<div class="col-6 px-1">{{ form.elaborator }}</div>
<div class="col-6 px-1">{{ form.reviewer }}</div>
</div>
</div>
<button type="submit">Submit</button>
</form>
****formsets/structure_form.html****
<tr>
<td class="col-12 px-1">
{{ sformset }}
</td>
</tr>
**** relevant urls.py****
urlpatterns = [
path('create_plan/', views.createPlan, name='create_plan'),
path('htmx/structure-form/', views.addStructure, name='structure-form')]
Additionally, the form that i built in admin.py using fields and inlines is just exactly what i want as the raw product (except for the amount of initial formsets and styles)
To summarize the problem: At present, your code successfully brings in the new formset, but each new formset comes with a name attribute of form-0-title (ditto for id and other attributes). In addition, after adding the new formset with hx-get the hidden fields originally created by the ManagementForm will no longer reflect the number of formsets on the page.
What's needed
After a new formset is added to the site, here's what I think needs to happen so Django can process the form submission.
Update the value attribute in the input element with id="id_form-TOTAL_FORMS" so the number matches the actual number of formsets on the page after hx-get brings in the new formset.
Update the name and id of the new formset from form-0-title to use whatever number reflects the current total number of formsets.
Update the labels' for attributes in the same way.
You can do this with Javascript on the client side. Alternatively, you can do effectively the same thing with Django on the server side and then htmx can be the only javascript needed to do the rest. For that, I have used empty_form to create the html content of a formset which can be altered as needed. That work is shown in the build_new_formset() helper, below.
Example
Here's what I have working:
forms.py
from django import forms
from django.forms import formset_factory
class BookForm(forms.Form):
title = forms.CharField()
author = forms.CharField()
BookFormSet = formset_factory(BookForm)
views.py
from django.utils.safestring import mark_safe
from app2.forms import BookFormSet
def formset_view(request):
template = 'formset.html'
if request.POST:
formset = BookFormSet(request.POST)
if formset.is_valid():
print(f">>>> form is valid. Request.post is {request.POST}")
return HttpResponseRedirect(reverse('app2:formset_view'))
else:
formset = BookFormSet()
return render(request, template, {'formset': formset})
def add_formset(request, current_total_formsets):
new_formset = build_new_formset(BookFormSet(), current_total_formsets)
context = {
'new_formset': new_formset,
'new_total_formsets': current_total_formsets + 1,
}
return render(request, 'formset_partial.html', context)
# Helper to build the needed formset
def build_new_formset(formset, new_total_formsets):
html = ""
for form in formset.empty_form:
html += form.label_tag().replace('__prefix__', str(new_total_formsets))
html += str(form).replace('__prefix__', str(new_total_formsets))
return mark_safe(html)
Note re: build_new_formset() helper: formset.empty_form will omit the index numbers that should go on the id, name and label attributes, and will instead use "__prefix__". You want to replace that "__prefix__" part with the appropriate number. For example, if it's the second formset on the page its id should be id_form-1-title (changed from id_form-__prefix__-title).
formset.html
<form action="{% url 'app2:formset_view' %}" method="post">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<p>{{ form }}</p>
{% endfor %}
<button type="button"
hx-trigger="click"
hx-get="{% url 'app2:add_formset' formset.total_form_count %}"
hx-swap="outerHTML">
Add formset
</button>
<input type="submit" value="Submit">
</form>
formset_partial.html
<input hx-swap-oob="true"
type="hidden"
name="form-TOTAL_FORMS"
value="{{ new_total_formsets }}"
id="id_form-TOTAL_FORMS">
<p>{{ new_formset }}</p>
<button type="button"
hx-trigger="click"
hx-get="{% url 'app2:add_formset' new_total_formsets %}"
hx-swap="outerHTML">
Add formset
</button>
Note re: the hidden input: With every newly added formset, the value of the input element that has id="id_form-TOTAL_FORMS" will no longer reflect the actual number of formsets on the page. You can send a new hidden input with your formset and include hx-swap-oob="true" on it. Htmx will then replace the old one with the new one.
Docs reference: https://docs.djangoproject.com/en/4.1/topics/forms/formsets/

How to add data to Django's database by the click of a button using JS and AJAX

I'm trying to add data that I rendered on a page from an API endpoint, to my database when I click "Add to my records" button, as can be seen in the image below, and I'm only trying to store "Date and Country" into the database (my model table has only date and country)
enter image description here
I've seen many resources talking about how JS and AJAX are useful in this case but I'm lost with logic of it all. Is there any way someone could explain how it's supposed to be done.
models.py
from django.db import models
class CountryData(models.Model):
country = models.CharField(max_length=100)
date = models.DateTimeField()
def __str__(self):
return self.country
views.py
def all_countries(request):
first_response = requests.get("https://api.covid19api.com/summary").json()
results = len(first_response["Countries"])
my_new_list = []
data_list = []
for i in range(0, results):
my_new_list.append(first_response["Countries"][i])
# print(my_new_list)
if request.method == "POST":
if request.POST.get("country") and request.POST.get("date"):
added_record = CountryData()
added_record.country = request.POST.get("country")
# 2022-12-19T08:53:48.179Z
added_record.date = datetime.datetime.strptime(
request.POST.get("date"), "%Y-%m-%dT%I:%M:%S.%fZ"
)
added_record.save()
return render(request, "allcountries.html")
else:
return render(request, "allcountries.html", )
context = {"my_new_list": my_new_list}
return render(request, "allcountries.html", context)
urls.py
from django.urls import path, include
from .views import home, all_countries
urlpatterns = [
path("", home, name="home"),
path("allcountries", all_countries, name="all_countries")
]
allcountries.html
{% extends '_base.html' %}
{% block page_title %} Covid19 Statistics For All Countries {% endblock %}
{% block content %}
<h3 class="text-center">Covid19 Statistics For All Countries </h3>
{% for object in my_new_list %}
<div class="row justify-content-center">
<div class="col-sm-10 d-flex justify-content-center">
<div class="card text-dark text-center" style="width: 20rem;">
<div class="card-block card-body">
<form method="POST" action="">
{% csrf_token %}
<h5 class="card-header" name="country">Country: {{object.Country}}, {{object.CountryCode}}</h5>
<br>
<p class="card-text">Total Confirmed Cases: {{object.TotalConfirmed}} </p>
<p class="card-text">Total Deaths Cases: {{object.TotalDeaths}} </p>
<p class="card-text">Total Recovered Cases: {{object.TotalRecovered}} </p>
<p class="card-text" name="date">Date: {{object.Date}}</p>
<button class="btn btn-success" type="submit">ADD TO MY RECORDS </button>
</form>
</div>
</div>
<br>
</div>
</div>
{% endfor %}
{% endblock %}
You can use the following ajax snippet to send data in the backend:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script>
$.ajax({
type: 'POST',
url: 'url', #leave blank if URL is same as the current one
data: {
'country': 'county_name',
'date': 'date_value',
},
success: function(response) {
# depending upon the response from the server you can make an alert here.
},
error: function(response){
# If some error occurred on the backend you can show error message from here
}
});
</script>

Django href for html

How i can make link to this urls.py? I tried to pass the link through all the methods I know but they don't work and gave an error
path('category/<slug:gender_slug>/<slug:category_slug>/', views.StuffCategory.as_view(), name='category'),
html:
{% get_genders as genders %}
{% for gender in genders %}
<li>
<!-- First Tier Drop Down -->
<label for="drop-2" class="toggle">Категории <span class="fa fa-angle-down"
aria-hidden="true"></span> </label>
{{ gender }} <span class="fa fa-angle-down" aria-hidden="true"></span>
<input type="checkbox" id="drop-2">
<ul>
{% get_categories as categories %}
{% for category in categories %}
<li>{{category.name}}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
views.py
class StuffCategory(ListView):
model = Stuff
template_name = 'shop/shop.html'
context_object_name = 'stuffs'
def get_queryset(self):
queryset = Stuff.objects.filter(draft=False)
if self.kwargs.get('category_slug'):
queryset = queryset.filter(category__slug=self.kwargs['category_slug'])
if self.kwargs.get('gender_slug'):
queryset = queryset.filter(gender__slug=self.kwargs['gender_slug'])
return queryset
Just pass the parameters that have been defined in url as below:
{{category.name}}
For more information refer to official docs
When trying to access a link with a href attribute you can simply use the name attribute of your path object that you defined in urls.py. You have to make sure, to submit every parameter the path object needs:
For example for an edit field:
<li><a href="{% url 'category' object.pk gender_slug=gender arg %}">
here:
category is the name
object.pk could be the primary key of the object transmitted to the template
gender_slug & arg are additional parameters that are defined by your path object

How to set success url as the previous page after updating an instance in django

I am trying to redirect the user to the previous page after they have updated an instance in the Model Class. So, here is the view for the update:
class ClassStatusDetailView(OrganisorAndLoginRequiredMixin, generic.UpdateView):
model = Class
template_name = "agents/class_status_detail.html"
context_object_name = "class"
fields = ['status']
def get_success_url(self):
return reverse("agents:agent-list")
Right now, as you can see, the get_success_url is set to "agents:agent-list", which is not the previous page. Also, here is the template for the update view in case you need it:
{% extends "base.html" %}
{% load tailwind_filters %}
{% block content %}
<div class="max-w-lg mx-auto">
<a class="hover:text-blue-500" href="#">Something</a>
<div class="py-5 border-t border-gray-200">
<h1 class="text-4xl text-gray-800">{{ class.student }}</h1>
</div>
<form method="post" class="mt-5">
{% csrf_token %}
{{ form|crispy }}
<button type='submit' class="w-full text-white bg-blue-500 hover:bg-blue-600 px-3 py-2 rounded-md">
Update
</button>
</form>
</div>
{% endblock content %}
However, there is a catch. The previous page I want to return to is a function view with a primary key. So, not only do I have to go back to this function view, but I also have to go to the correct primary key. Please tell me if you guys need any other information. Thank you!
When user successfully update their data then he/she redirect to class_list.html page..
urls.py(I assume):
path('class_list/<int:pk>/', class_list,name = 'class_list'),
path('edit_class/<int:pk>/', ClassStatusDetailView.as_view(),name = 'edit_class')
models.py:
class ClassStatusDetailView(OrganisorAndLoginRequiredMixin, generic.UpdateView):
model = Class
template_name = "agents/class_status_detail.html"
context_object_name = "class"
fields = ['status']
def get_success_url(self):
agent_id = self.object.teacher.id
return reverse_lazy('class_list', kwargs={'pk': agent_id})
Use reverse_lazy

Django - Taking of object model by select tag and passing this object to another view

I'm struggling with the following problem. In my project I have the following model:
models.py
class InputSignal(models.Model):
name = models.CharField(max_length=512)
author = models.ForeignKey(User, on_delete=models.CASCADE)
adnotations = models.TextField(blank=True, null=True)
input_file = models.FileField(upload_to='signals/', null=False, validators=[validate_file_extension])
add_date = models.DateTimeField(default=datetime.now())
last_edit_date = models.DateTimeField(default=datetime.now())
last_json_results = models.FileField(upload_to='resuts/')
objects = models.Manager()
def delete(self):
self.input_file.delete()
super().delete()
def __str__(self):
return self.name
def add_date_pretty(self):
return self.add_date.strftime('%b %e %Y')
Two url addresses:
urls.py
path('display/list', displayviews.display_list, name='display-list'),
path('display/details/<int:signal_id>', displayviews.display_details, name='display-details'),
And two view functions:
views.py
def display_list(request):
signals = InputSignal.objects.filter(author=request.user)
return render(request, 'display_list.html', {'signals': signals})
def display_details(request, signal_id):
signal = get_object_or_404(InputSignal, pk=signal_id)
The template of the first function of the view at this moment looks like this:
display_list.html
<div class="row mt-2">
<div class="col-lg-1"></div>
<div class="col-lg-10">
<select class="form-control mt-2 text-center">
{% for signal in signals %}
<option>
<h2>{{ signal.name }}</h2>
</option>
{% endfor %}
</select>
</div>
<div class="col-lg-1"></div>
</div>
<div class="row mt-3 mb-2">
<div class="col-lg-4"></div>
<div class="col-lg-4">
Perform Analysis
</div>
<div class="col-lg-4"></div>
</div>
I would like based on the structure of this template to design a solution that after selecting the signal name from the select tag and clicking the 'perform analysis' button, go to the next view - display_details (request, signal_id). Where I will save the previously selected model object to the variable. Choosing the right object I would like to use the object ID. I would like to ask for help, what I was able to design presented above.
A couple of things here.
To submit data from a web page you need a form element (unless you're using Ajax, which doesn't seem necessary here). Your select box needs a name attribute, and each option needs a value to submit. Also, you can't submit a form with an a link; you need a submit button. So:
<form action="{% url 'display-details' %}">
<select name="signal_id" class="form-control mt-2 text-center">
{% for signal in signals %}
<option value="{{ signal.id }}">
<h2>{{ signal.name }}</h2>
</option>
{% endfor %}
<button class="btn btn-outline-success btn-block">Perform Analysis</button>
</form>
Now this data is being submitted in the querystring to your display_details view, eg "/display/details/?signal_id=5". So you need to remove the parameter from the URL pattern, and get the data inside the view from the GET params:
path('display/details/', displayviews.display_details, name='display-details'),
...
def display_details(request):
signal_id = request.GET['signal_id']
signal = get_object_or_404(InputSignal, pk=signal_id)