I have been pounding my head on this problem, been looking up Django docs on how to do form validation and can't seem to get any progress so I'm turning to SO for help.
I have a website with a Django form on which I display two ChoiceFields. One contains a list of car makes and the other contains a list of car models. The second field is initially blank and is populated with my Javascript when the user selects a car make. When the user selects both a make and model and clicks Submit, then a page will be loaded with info pertaining to the make/model that the user selected.
Now, if the user selects only a make but no model or no make at all, I want to display an error saying "Please select a make and model to continue."
So far, no matter what I've tried, I am unable to get that functionality.
forms.py
from django import form
makes = (('Honda', 'Honda'), ('Ford', 'Ford'), ('Nissan', 'Nissan'), ('GM', 'GM'))
class SearchForm:
make = forms.ChoiceField(choices=makes, required=True, widget=forms.Select(attrs={'size':'5', 'autofocus'='on'}))
model = forms.ChoiceField(required=True, widget=forms.Select(attrs={'size':'5'}))
def clean(self):
cleaned_data = super(SearchForm, self).clean()
m1 = cleaned_data.get("make")
m2 = cleaned_data.get("model")
if not m1 or not m2:
raise forms.ValidationError("Please select a make and model to continue.")
return cleaned_data
def clean_model(self):
data = self.cleaned_data["model"]
if not data: // if the user didn't select a model
raise forms.ValidationError("Please select a model to continue.")
return data
views.py
from mysite.forms import SearchForm
def search(request):
searchform = SearchForm()
return render(request, "search.html", {"form" : searchform})
def car_info(request):
searchform = SearchForm(request.GET)
if searchform.is_valid():
// code to parse the make and model and return the corresponding page
return render(request, "car_info.html", {})
else: // there is an error so redisplay the page with the new form object
return render(request, "search.html", {"form" : searchform})
search.html
...
<form action="{% url 'mysite.views.car_info' %}" method="GET" name="sform">
{{ searchform.make }}
{{ searchform.model }}
<button>Search away!</button>
</form>
{% if searchform.errors %}
<div class="error">{{ searchform.non_field_errors.as_text }}</div>
{% endif %}
...
In my index.html, I have a link to the search page by having: {% url 'mysite.views.search' %}
The behavior that I am getting is that when I don't select a make, I get the error. This is correct. When I do select a make but don't select a model, I also get the error. Good. But, when I do select a make and model, I get the same error and it will not take me to the car_info.html page.
Any help would be greatly appreciated.
EDIT:
Something weird is happening. I changed my forms.py to raise the Validation Error just to see what would pop out.
raise forms.ValidationError(str(clean_make) + " " + str(clean_model))
Then, I selected both make and model and the error was raised outputting the correct make for the make but "None" for the model, even though I had selected a model! Why is this?
EDIT 2:
Okay, I may know why the model is outputting "None". In my forms.py, I don't specify the choices for the model because that will be filled in with this Javascript code that runs on the search.html page:
$(document).ready(function() {
$("#id_make").change(init);
});
function init() {
populateModel();
}
function populateModel() {
var make = $("#id_make")
var model = $("#id_model") //select fields created by Django ChoiceField widget
var mod_values = values[mak.val()];
model.empty();
$.each(mod_values, function(k,v) {
model.append($("<option></option>").attr("value", v).text(k));
});
}
var values = {
"Honda" : {
"Accord" : "accord",
"Civic" : "civic",
"Odyssey" : "odyssey",
},
"Ford" : {
"F-150" : "f150",
"Taurus" : "taurus",
"Fusion" : "fusion",
},
"Nissan" : {
"Sentra" : "sentra",
"Maxima" : "maxima",
"Altima" : "altima",
},
}
So, when I do:
model = cleaned_data.get("version")
that'll be empty, right?
Now, I am now unsure how to fix it.
As a first debugging step, since you are using the GET method, you should be able to inspect the URL for correctness. When you submit the form with both a make and model selected, the URL should look like:
?make=XXXX&model=YYYY
Since it does, you are absolutely correct that the form is cleaning out the model because essentially it believes there are no valid entries for model. You'll need to create a custom field that validates the model:
class ModelField(forms.ChoiceField):
def valid_value(self, value):
if value is not None and value != '':
return True
return False
Then, in your form:
model = ModelField(required=True, widget=forms.Select(attrs={'size':'5'}))
Related
I would like to add a hyperlink to the related model Training
It would be nice to have declarative solution, since I want to use
this at several places.
The "pencil" icon opens the related model in a popup window. That's not what I want. I want a plain hyperlink to the related model.
BTW, if you use "raw_id_fields", then the result is exactly what I was looking for: There is a hyperlink to the corresponding admin interface of this ForeignKey.
Update Jan 4, 2023
From Django 4.1, this becomes a part of the official build (related PR).
Related widget wrappers now have a link to object’s change form
Result
Previous Answer
The class named RelatedFieldWidgetWrapper is showing the icons on the Django Admin page and thus you need to override the same. So, create a custom class as below,
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
class CustomRelatedFieldWidgetWrapper(RelatedFieldWidgetWrapper):
template_name = 'admin/widgets/custom_related_widget_wrapper.html'
#classmethod
def create_from_root(cls, root_widget: RelatedFieldWidgetWrapper):
# You don't need this method of you are using the MonkeyPatch method
set_attr_fields = [
"widget", "rel", "admin_site", "can_add_related", "can_change_related",
"can_delete_related", "can_view_related"
]
init_args = {field: getattr(root_widget, field) for field in set_attr_fields}
return CustomRelatedFieldWidgetWrapper(**init_args)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
rel_opts = self.rel.model._meta
info = (rel_opts.app_label, rel_opts.model_name)
context['list_related_url'] = self.get_related_url(info, 'changelist')
return context
See, the context variable list_related_url is the relative path that we need here. Now, create an HTML file to render the output,
#File: any_registered_appname/templates/admin/widgets/custom_related_widget_wrapper.html
{% extends "admin/widgets/related_widget_wrapper.html" %}
{% block links %}
{{ block.super }}
- Link To Related Model -
{% endblock %}
How to connect?
Method-1 : Monkey Patch
# admin.py
# other imports
from ..widgets import CustomRelatedFieldWidgetWrapper
from django.contrib.admin import widgets
widgets.RelatedFieldWidgetWrapper = CustomRelatedFieldWidgetWrapper # monket patch
Method-2 : Override ModelAdmin
# admin.py
class AlbumAdmin(admin.ModelAdmin):
hyperlink_fields = ["related_field_1"]
def formfield_for_dbfield(self, db_field, request, **kwargs):
formfield = super().formfield_for_dbfield(db_field, request, **kwargs)
if db_field.name in self.hyperlink_fields:
formfield.widget = CustomRelatedFieldWidgetWrapper.create_from_root(
formfield.widget
)
return formfield
Result
There are several ways to go. Here is one.
Add some javascript that changes the existing link behavior. Add the following script at the end of the overridden admin template admin/widgets/related_widget_wrapper.html. It removes the class which triggers the modal and changes the link to the object.
It will only be triggered for id_company field. Change to your needs.
{% block javascript %}
<script>
'use strict';
{
const $ = django.jQuery;
function changeEditButton() {
const edit_btn = document.getElementById('change_id_company');
const value = edit_btn.previousElementSibling.value;
const split_link_template = edit_btn.getAttribute('data-href-template').split('?');
edit_btn.classList.remove('related-widget-wrapper-link');
edit_btn.setAttribute('href', split_link_template[0].replace('__fk__', value));
};
$(document).ready(function() {
changeEditButton();
$('body').on('change', '#id_company', function(e) {
changeEditButton();
});
});
}
</script>
{% endblock %}
This code can also be modified to be triggered for all edit buttons and not only for the company edit button.
My simple web-application has two models that are linked (one to many).
The first model (Newplate) has a boolean field called plate_complete. This is set to False (0) at the start.
questions:
In a html page, I am trying to build a form and button that when pressed sets the above field to True. At the moment when I click the button the page refreshes but there is no change to the database (plate_complete is still False). How do I do this?
Ideally, once the button is pressed I would also like to re-direct the user to another webpage (readplates.html). This webpage does not require the pk field (but the form does to change the specific record) Hence for now I am just refreshing the extendingplates.html file. How do I do this too ?
My code:
"""Model"""
class NewPlate(models.Model):
plate_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(max_length=200)
created_date = models.DateTimeField(default=timezone.now)
plate_complete = models.BooleanField()
"""view"""
def publish_plates(request,plate_id):
newplate = get_object_or_404(NewPlate, pk=plate_id)
newplate.plate_complete = True
newplate.save()
#2nd method
NewPlate.objects.filter(pk=plate_id).update(plate_complete = True)
return HttpResponseRedirect(reverse('tablet:extendplates', args=[plate_id]))
"""URLS"""
path('readplates', views.read_plates, name='readplates'),
path('extendplates/<pk>/', views.show_plates, name='showplates'),
path('extendplates/<pk>/', views.publish_plates, name='publishplates'),
"""HTML"""
<form method="POST" action="{% url 'tablet:publishplates' newplate.plate_id %}">
{% csrf_token %}
<button type="submit" class="button" value='True'>Publish</button></form>
-------Added show plates view:---------
def show_plates(request,pk):
mod = NewPlate.objects.all()
newplate= get_object_or_404(mod, pk=pk)
add2plate= Add2Plate.objects.filter(Add2Plateid=pk)
return render(request, 'tablet/show_plates.html', {'newplate': newplate,'add2plate': add2plate})
Thank you
The problem is two of your urls have the same pattern 'extendplates/<pk>/'. Django uses the first pattern that matches a url. I suppose that one of these view views.show_plates is meant to display the form and the other views.publish_plates is meant to accept the posted form data.
This means that simply both of these views should simply be a single view (to differentiate if the form is submitted we will simply check the requests method):
from django.shortcuts import redirect, render
def show_plates(request, plate_id):
newplate = get_object_or_404(NewPlate, pk=plate_id)
if request.method == "POST":
newplate.plate_complete = True
newplate.save()
return redirect('tablet:extendplates', plate_id)
context = {'newplate': newplate}
return render(request, 'your_template_name.html', context)
Now your url patterns can simply be (Note: Also captured arguments are passed as keyword arguments to the view so they should be consistent for your view and pattern):
urlpatterns = [
...
path('readplates', views.read_plates, name='readplates'),
path('extendplates/<uuid:plate_id>/', views.show_plates, name='showplates'),
...
]
In your form simply forego the action attribute as it is on the same page:
<form method="POST">
{% csrf_token %}
<button type="submit" class="button" value='True'>Publish</button>
</form>
You should avoid changing state on a get request like your view does currently.
Handle the POST request and change the data if the request is valid (ensuring CSRF protection).
def publish_plates(request,plate_id):
newplate = get_object_or_404(NewPlate, pk=plate_id)
if request.method == "POST":
newplate.plate_complete = True
newplate.save(update_fields=['plate_complete']) # a more efficient save
#2nd method
NewPlate.objects.filter(pk=plate_id).update(plate_complete=True)
return HttpResponseRedirect(reverse('tablet:extendplates', args=[plate_id]))
You could also put a hidden input in the form, or make a form in Django to hold the hidden input, which stores the plate_id value and that way you can have a generic URL which will fetch that ID from the POST data.
Now the real problem you've got here, is that you've got 2 URLs which are the same, but with 2 different views.
I'd suggest you change that so that URLs are unique;
path('extendplates/<pk>/', views.show_plates, name='showplates'),
path('publish-plates/<pk>/', views.publish_plates, name='publishplates'),
I would like to do:
I am trying to create a form input on a detail view that will update a particular data column ('status') of the detailed model instance. Here is a picture of what I have in mind:
The selector would display the current status and the user could change it and update from the detail view without having to access the UpdateView.
my idea here would be to have this happen:
1. On submit, get the new user entered value.
2. get the model instance of the currently detailed class
3. assign the model instance attribute as the user entered value
4. save the model instance
I've tried: I don't know if this is the best way to do this but i've been trying to create an AJAX call, mostly by looking for examples online.
Results: Terminal shows Post on submit: "[19/Nov/2019 17:50:33] "POST /task/edit/4 HTTP/1.1" 200 41256". However, the data is not saved to the db. On refresh, the selector returns to previously saved status.
The console shows: "script is connected", and "Update Status" with no errors. On submit, the alert displays success message: "127.0.0.1:8000 says status updated".
Task_detail.html
<div class="deliv-box edit">
<form id="status-update-form" method='POST' action='{% url "task_edit" task.pk %}'>
{% csrf_token %}
{{task_form.status}}
<input id="status-update-btn" type="submit" value="Update Status" />
</form>
</div>
...
<script type="text/javascript">
var frm = $('#status-update-form');
frm.submit(function () {
console.log("script is connected")
console.log($('#status-update-btn').val())
$.ajax({
type: frm.attr('method'),
url: frm.attr('action'),
data: frm.serialize(),
success: function (data) {
$("#deliv-box edit").html(data);
alert("status updated");
},
error: function(data) {
alert("error");
}
});
return false;
});
</script>
forms.py
class TaskForm(forms.ModelForm):
class Meta:
model = Task
fields = "__all__"
views.py
class TaskDetail(ModelFormMixin, DetailView):
template_name='task_detail.html'
model = Task
form_class = TaskForm
def get_context_data(self, **kwargs):
context = super(TaskDetail, self).get_context_data(**kwargs)
context['task_form'] = self.get_form
return context
def update(request):
if request.method=='POST':
task_id = request.POST.get('id')
task = Task.objects.get(pk = task_id)
status_obj = request.POST.get('status')
task.status = status_obj
task.save()
return JsonResponse({'status':'updated...'})
else:
return JsonResponse({'status':'not updated'})
thank you.
A solution:
In the unlikely event that someone stumbles across this question and who is, like me, just trying to figure it out all by themselves, here is what I've learned about how this works: When a user wants to update a form, Django pre-populates the form with the existing data related to that instance. A user can then alter the data and re-submit the form.
Here, I was attempting to alter just one field of the exiting instance, but as I was only calling that one field, Django was assuming not, as I had hoped, that the other fields would remain the same, but that I intended the other fields to be submitted as blank. Where the fields are required one cannot return that field as blank. Therefore, Django was not able to validate the form and so the form did not get updated.
A solution that works is to call all the fields as hidden and show just the one you want to alter. This way Django can return the unaltered data and validate the form, and you get an update button on your detail view:
<form method="POST">
{% csrf_token %}
<h4> STATUS: </h4>
{% for field in form %}
{{ field.as_hidden }}
{% endfor %}
{{form.status}}
<button type="submit" class="btn btn-success">submit</button>
</form>
You are overriding the method update which does not exist, so it is never called.
You need to subclass UpdateView instead of the DetailView and the mixin.
class TaskUpdateView(UpdateView):
template_name='task_detail.html'
model = Task
form_class = TaskForm
# you can use the line below instead of defining form_class to generate a model form automatically
# fields = ('status', )
def form_valid(self, form):
post = form.save(commit=False)
# do anything here before you commit the save
post.save()
# or instead of two lines above, just do post = form.save()
return JsonResponse({'status':'updated...'})
Here is how you would add readonly (disabled) fields to your form:
class TaskForm(forms.ModelForm):
# override the default form field definitions for readonly fields
other_field = forms.CharField(disabled=True)
another_field = forms.IntegerField(disabled=True)
class Meta:
model = Task
fields = ("status", "other_field", "another_field")
# you could also just do:
# fields = '__all__'
I just want to show a Form on a HTML Page.
When I put it on the Page and look via Web Browser, there seems to show me the memory address instead of rendering the form..
[forms.py]
from django import forms
MODES=[('top10', 'Top 10 Songs'),
('last10', 'Last 10 Songs'),
('recentX', 'Recent X Songs')]
class chooseMode():
first_name = forms.CharField(required=True)
selectMode = forms.ChoiceField(label='Choose Mode', widget=forms.RadioSelect,choices=MODES)
[views.py]
def home(request):
modusFormular = chooseMode()
return render(request, 'home.html', {'modusForm' : modusFormular})
[home.html]
somewhere in body:
{{ modusForm }}
I expected the form to be shown on the page, but it shows me the following:
<mysongproject.forms.chooseMode object at 0x7fcdcae97710>
Your class needs to inherit from Form:
class chooseMode(forms.Form):
So basically i have form on my homepage that asks users to choose two cities : where they are now, and where they want to go. I display all the available options with ModelChoiceField() easily, but when i try to use user's choices to make arguments for url, i get NoReverseMatch. I did a little research and found out due to the fact that at the time when page is loaded, user hasn't chosen anything, so there are no arguments. After that, i took different approach - i tried to set /search/ as url for the form. There, i extracted user's choices and tried to redirect back to the main url with these two arguments. Error still persists
Traceback Url :
http://dpaste.com/34E3S2V
Here's my forms.py :
class RouteForm(forms.Form):
location = forms.ModelChoiceField(queryset=Location.objects.all())
destination = forms.ModelChoiceField(queryset=Destination.objects.all())
Here's my template :
<p> From where to where ? </p>
<form action="{% url 'listings:search' %}" method="POST">
{{ form.as_p }}
{% csrf_token %}
<input type="submit" value="Let's go!">
</form>
My urls.py :
urlpatterns = [
path('', views.index, name="index"),
path('<location>/<destination>', views.route, name="route"),
path('search/', views.search, name="search")
]
and views.py :
def index(request):
form = forms.RouteForm()
listings = Listing.objects.all()
context = {"listings" : listings, "form" : form }
def route(request, location, destination):
current_location = Location.objects.get(city=location)
future_destination = Destination.objects.get(city=destination)
context = {"current_location" : current_location, "future_destination" : future_destination}
return render(request, 'listings/route.html', context)
def search(request, location, destination):
chosen_location = Location.objects.get(pk=request.POST['location'])
chosen_destination = Destination.objects.get(pk=request.POST['destination'])
return HttpResponseRedirect(reverse('listings:route', args=[chosen_location, chosen_destination]))
What am i missing?
You need to show the full traceback in your question. Actually, the code you've shown wouldn't give that error; instead you would get a TypeError for the search view.
Nevertheless, you have quite a few things wrong here.
Firstly, you need to decide how you want to represent those fields in the URL. You can't just put a Location object in a URL. Do you want to use numeric IDs, or string slugs? Assuming you want to use slugs, your URL would be:
path('<slug:location>/<slug:destination>', views.route, name="route"),
Secondly, you shouldn't have location and destination as parameters to the search function. They aren't being passed in the URL, but you in the POST data.
Next, you need to actually use the Django form you've defined, and get the values from that form cleaned_data. Using the form - in particular calling its is_valid() method - ensures that the user actually chooses options from the fields. So the search function needs to look like this:
def search(request):
if request.method == 'POST':
form = RouteForm(request.POST)
if form.is_valid():
chosen_location = form.cleaned_data['location']
chosen_destination = form.cleaned_data['destination']
return HttpResponseRedirect(reverse('listings:route', args=[chosen_location.city, chosen_destination.city]))
else:
form = RouteForm()
return render(request, 'search.html', {'form': form})