I need form, where user can select tours based on selected tag. I have implemented it via view, custom html form and some AJAX stuff:
HTML form:
<form id="add-destination" method="post">
{% csrf_token %}
<select name="tag_id" id="tag_id">
<option value=''>---</option>
{% for tag in tags %}
<option value={{ tag.id }}>{{ tag.name }}</option>
{% endfor %}
</select></br>
<select multiple="multiple" name="tours" id="tours">
</select></br>
<button type="submit">Submit</button>
</form>
View to get tours by tag
def get_tours_by_tag(request):
tag_id = Tag.objects.get(id=request.GET.get('tag_id', None))
tours = Inbound.objects.filter(tags=tag_id)
return JsonResponse({"status": "ok", "tours": serializers.serialize("json", list(tours))})
JS:
$(function(){
$('#tag_id').change(function(e){
$('#tours').html('');
e.preventDefault();
$.ajax({
type: 'GET',
data: $(e.currentTarget).serialize(),
url: "/ru/tour/get_tours/",
success: function(data){
var tour_data = JSON.parse(data.tours);
if(data.status=='ok'){
$.each(tour_data, function(i, item) {
$('#tours').append('<option value='+item['pk']+'>'+item['fields']['title']+'</option>');
});
}else{
console.log("Not okay!")
}
}
});
})
});
This way works fine, but I know that it's not right way to solve such problem. And I want to do it by using power of django forms.
So I have model Tours:
class Tour(models.Model):
title = models.CharField(max_length=255)
tags = models.ManyToManyField(Tag)
And form:
class FiveDestinationForm(forms.Form):
tags = forms.ModelChoiceField(queryset=Tag.objects.all())
tours = forms.ModelMultipleChoiceField(queryset=????)
fields = ('tag', 'tours')
I found some close enough questions and tried to solve my problem by overriding __init__ method in form, as it was suggested:
class MyForm(forms.Form):
tags = forms.ModelChoiceField(queryset=Tag.objects.all())
tours = forms.ModelMultipleChoiceField(queryset=Tour.objects.none())
def __init__(self, *args, **kwargs):
qs = kwargs.pop('tours', None)
super(MyForm, self).__init__(*args, **kwargs)
self.fields['tours'].queryset = qs
fields = ('tag', 'tours')
But I get following error:
'NoneType' object has no attribute 'iterator'
So my question is how to properly set queryset for ModelMultipleChoiceField based on what user chosed in ModelChoiceField ?
The error you're seeing is most probably because the Form does not receive a 'tours' argument (it will receive a dictionary named data instead, containing the fields' data in an unvalidated form) and thus the qs variable will be None.
You should probably do something like this:
class MyForm(forms.Form):
tag = forms.ModelChoiceField(queryset=Tag.objects.all())
tours = forms.ModelMultipleChoiceField(queryset=Tour.objects.none())
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
if kwargs['data']:
tag_id = kwargs['data'].get('tag', None)
if tag_id:
self.fields['tours'].queryset = Tour.objects.filter(tag_id=tag_id)
Note that we bypass django's form mechanics a bit here and need to parse the tag_id ourselves. It's not a big deal, though.
From a UX vantage point, this is a rather hard problem to get right without the use of ajax. So I wouldn't completely dismiss your solution either.
Related
This is how my form looks:
class TestForm(forms.ModelForm):
class Meta:
model = Restaurant
fields = [
'title',
'content',
]
def clean(self, *args, **kwargs):
title = self.cleaned_data.get("title")
content = self.cleaned_data.get("content")
error_dict = {}
if len(title) < 3:
error_dict['title'] = ValidationError("testerror1")
if len(content) < 3:
error_dict['content'] = ValidationError('testerror2')
if error_dict:
raise ValidationError(error_dict)
If I try to submit the form with empty title and content it shows two error messages (testerror1, testerror2), they appear above each field label and looks like this:
<ul class="errorlist">
<li>test2</li>
</ul>
But I want to hide each of them if client click on input, so I tried with Jquery:
$("#my_form_id").find('input, textarea').click(function() {
$(this).closest('ul').hide();
})
Without success (it doesn't found any <ul> element.
My question is, is there a way to set different id for each error? So that I can manage each one separately.
You can put a unique identifier on each error message, but it is a bit fiddly and I'm not sure how 'safe' I would consider it.
What I think is a better alternative for what you want is iterating over the fields in the form, and rendering the field and the error messages one at a time. The end of this post describes how to do that. If what you really want is to put identifiers on the error messages without looping through the fields in the template... well, read on.
The Hard Way
In order to get more than a simple text message rendered for each error message (without breaking out the form in the template), you need to provide an alternative ErrorList class to the ModelForm. The ErrorList class is what performs the rendering of the errors in HTML, so by creating and using a subclass you can change what gets rendered - including adding a special code from the ValidationError itself.
from django.forms.utils import ErrorList
from django.utils.html import format_html, format_html_join
# This overrides the ErrorList class to provide the additional rendering
# features you want - in this example it only overrides it for the `ul` output
class ErrorListDerivative(ErrorList):
def as_ul(self):
if not self.data:
return ''
# Key part 1: the UL is now being rendered with a class for
# each of the errors which includes the error code from the
# ValidationError. You can then locate the UL by looking for that class.
return format_html(
'<ul class="{{}} {}">{{}}</ul>'.format(' '.join(('errorcode{}'.format(e.code) for e in self.data))),
self.error_class,
# Key Part 2: This adds the code from the validation error to the individual LIs
format_html_join('', '<li class="errorforcode{}">{}</li>', ((e.code, e.message) for e in self.data))
)
Now having created an ErrorList that renders things the way you want, it needs to be used by the TestForm.
class TestForm(forms.ModelForm):
# This __init__ is what makes the ModelForm use the custom ErrorList class you've created.
# The BaseForm from which ModelForm is derived (a few layers of inheritence deep) has an `error_class` argument to receive the class used to render errors. This just injects your custom class.
def __init__(self, *args, **kwargs):
kwargs_new = {'error_class': ErrorListDerivative}
kwargs_new.update(kwargs)
super().__init__(*args, **kwargs_new)
class Meta:
model = Restaurant
fields = [
'title',
'content',
]
Then, inside your TestForm clean function, you pass the additional code to the ValidationErrors
def clean(self, *args, **kwargs):
title = self.cleaned_data.get("title")
content = self.cleaned_data.get("content")
error_dict = {}
# Key Part 3: Here we're including the custom error code in the
# ValidationError, which will be rendered out
if len(title) < 3:
error_dict['title'] = ValidationError("testerror1", code='title')
if len(content) < 3:
error_dict['content'] = ValidationError('testerror2', code='content')
if error_dict:
# Must admit, not sure if adding a code here will do anything at all
raise ValidationError(error_dict, code='3')
Once you have done that, the HTML output should look something like:
<form id="my_form_id" method="post" novalidate="">
<label for="id_title">Title:</label>
<ul class="errorlist errorcodetitle">
<li class="errorforcodetitle">testerror1</li>
</ul><input type="text" name="title" value="ao" maxlength="100" required="" id="id_title">
<label for="id_content">Content:</label>
<ul class="errorlist errorcodecontent">
<li class="errorforcodecontent">testerror2</li>
</ul><input type="text" name="content" value="ao" maxlength="100" required="" id="id_content">
<button type="submit">Submit</button>
</form>
With the class now on those ULs, you can use the name of the field to locate the relevant UL and hide it.
$("#my_form_id").find('input, textarea').click(function(evt) {
$('.errorcode' + this.name).hide();
})
The Idiomatic Way
If you don't want to go down that rabbit hole, an alternative is to do something more like the example in the django docs for 'looping over the form's fields' (https://docs.djangoproject.com/en/3.0/topics/forms/#looping-over-the-form-s-fields)
It doesn't give you the custom classes (or ids, whatever you end up adding) on the error messages, but it is much more idiomatic.
Something like the following...
{% for field in form %}
<div class="fieldWrapper">
<div class="errorcode{{field.html_name}}">
{{ field.errors }}
</div>
{{ field.label_tag }} {{ field }}
{% if field.help_text %}
<p class="help">{{ field.help_text|safe }}</p>
{% endif %}
</div>
{% endfor %}
Then you can use the same jquery as was described above:
$("#my_form_id").find('input, textarea').click(function(evt) {
$('.errorcode' + this.name).hide();
})
I found multiple answers to this same questions but unfortunately, I can't seem to figure it out :(
The form has a drop-down list for the 'subcategory' field in my model 'PhysicalPart', the values of the 'subcategory' field are updated dynamically upon the form creation (using a 'category' parameter).
Unfortunately, I can't get the drop-down to show all subcategories AND have the one from the database selected at the same time. I can't seem to retrieve the 'short_description' value either from the database.
It used to work before I learned about UpdateView class and decided to use it instead...
Any insight on how-to workaround my problem would be appreciated!
forms.py
class PartForm(forms.ModelForm):
subcategory = forms.ChoiceField(choices=[])
class Meta:
model = PhysicalPart
fields = ['subcategory', 'short_description']
views.py
class PartUpdate(UpdateView):
model = PhysicalPart
template_name = 'part_update.html'
form_class = PartForm
def post(self, request, *args, **kwargs):
# Load model instance
self.object = self.get_object()
# Load form
form = super(PartUpdate, self).get_form(self.form_class)
# Populating subcategory choices
form.fields['subcategory'].choices = SubcategoryFilter[self.object.category]
# Check if form valid and save data
if form.is_valid():
form.save()
return redirect('part-list')
# Update context before rendering
context = self.get_context_data(**kwargs)
context['part_id'] = self.object.pk
context['part_category'] = self.object.category
context['manufacturing_list'] = self.object.manufacturing.all()
return render(request, self.template_name, context)
html
<form action="{% url 'part-update' pk=part_id category=part_category %}" method="post" style="display: inline">
{% csrf_token %}
<div class="form">
<p class="font-weight-bold">Type</br>
{{ form.subcategory }}
</p>
</div>
<div class="form">
<p class="font-weight-bold">Short Description</br>
{{ form.short_description }}
</p>
</div>
<button type="submit" class="btn btn-primary">Save</button>
</form>
<form action="{% url 'part-list' %}" style="display: inline">
<button type="submit" class="btn btn-danger">Cancel</button>
</form>
My problem was that I did not differentiate the "GET" versus the "POST" calls in the UpdateView class, I was trying to do everything in the post() method. It took me a while to figure it out but now I think it's clear.
I originally used the get() method but I realize that get_context_data() was better suited as it automatically loads most of the context (eg. the instance and the form), instead of having to do everything from scratch in the get() method.
Scrubbing through the code of the UpdateView class here, it also seemed necessary to add ModelFormMixin into the declaration of the PartUpdate class so that the get_context_data() method automatically loads the form associated to the target model/instance (else it looks like it won't do it).
Here is my updated views.py code:
class PartUpdate(UpdateView, ModelFormMixin):
model = PhysicalPart
template_name = 'part_update.html'
form_class = PartForm
success_url = reverse_lazy('part-list')
def get_context_data(self, **kwargs):
# Load context from GET request
context = super(PartUpdate, self).get_context_data(**kwargs)
# Get id from PhysicalPart instance
context['part_id'] = self.object.id
# Get category from PhysicalPart instance
context['part_category'] = self.object.category
# Add choices to form 'subcategory' field
context['form'].fields['subcategory'].choices = SubcategoryFilter[self.object.category]
# Return context to be used in form view
return context
def post(self, request, *args, **kwargs):
# Get instance of PhysicalPart
self.object = self.get_object()
# Load form
form = self.get_form()
# Add choices to form 'subcategory' field
form.fields['subcategory'].choices = SubcategoryFilter[self.object.category]
# Check if form is valid and save PhysicalPart instance
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
From my understanding you are trying to edit an instance. This is how you do it in Django, it should autopopulate your inputs with the proper values :
my_record = MyModel.objects.get(id=XXX)
form = MyModelForm(instance=my_record)
More details on this answer : how to edit model data using django forms
If your models are properly done (with relations) you shouldn't need to provide the choices for the Select.
class form(forms.ModelForm):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
u = User.objects.get(user=user)
super(form, self).__init__(*args, **kwargs)
self.fields['phone'].queryset = Phone.objects.filter(user=u)
class Meta:
model = MyModel
fields = ["name", "description", , "phone"]
This form pre-populates the field phones with phones that belong to the currently logged in user. I got the template to display using {{ form.as_p }}, but I dont know how to manually display the fields with the pre-populated phone names to be chosen, in an html template.
I tried the snippet below as part of the bigger form, but it did not work.
<select multiple="multiple" id="id_phone" name="phone" required>
{% for p in phone %}
<option value="{{ p.id }}">{{ p.name }}</option>
{% endfor %}
</select>
Also, how can I allow a user to choose multiple phones at once with this pre-population method. I tried to use ModelMultipleChoiceField but it did not work.
There are two things that you need in this situation
Set an initial value for the field
Set the field widget to be able to select multiple values
Luckily, Django already has a widget for that!
The following code will achieve what you want:
class form(forms.ModelForm):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
u = User.objects.get(user=user)
kwargs["initial"].update({
"phone": Phone.objects.filter(user=u)
})
super(form, self).__init__(*args, **kwargs)
class Meta:
model = MyModel
widgets = {
"phone": SelectMultiple
}
fields = ["name", "description", "phone"]
With these two pieces in place, the template becomes easy!
{{ form.phone.errors }} {# shows on page any errors during clean #}
{{ form.phone }} {# renders the widget #}
If you do not want to use the built in widget that Django provides, look into creating your own
For other ways to set initial values checkout this post
I am setting up a simple html page, the page captures the information that the user entered and based on the information that the user entered makes a new page. The problem is that I cant get back the information entered by the user at the backed and I dont understand where I am going wrong.
My views file is setup like this:
def suggestion(request):
if request.method == 'POST':
form = BusinessName(request.POST)
if form.is_valid():
data=form.cleaned_data
context = insert_function_here(data)
return render( request,'mainpage.html', context)
else:
form = BusinessName()
context = {'form':form}
return render( request,'mainpage.html', context)
My forms.py is setup like this:
class BusinessName(forms.Form):
business_name = forms.CharField(widget = forms.HiddenInput(), required = False)
The relevant part of my html is set up like this:
<form id="user_input_form" method="post" action="http://127.0.0.1:8000/textinsighters/suggestion">
Enter Your Business Name : <input type="text" list="browsers" name="browser" id="user_input">
{% csrf_token %}
{{ form }}
<datalist id="browsers">
<option value="Internet Explorer">
<option value="Firefox">
<option value="Chrome">
<option value="Opera">
<option value="Safari">
</datalist>
<button onclick="myFunction()">Submittt</button>
</form>
<script>
function myFunction() {
document.getElementById("id_business_name").value = document.getElementById("user_input").value;
document.getElementById("user_input_form").submit();
}
</script>
I want an auto-completing list so thats why I am creating a form in html. I get the user input, set the value of the Django form field to the value that the user entered and submit it. I should get something back but the variable 'data' in views doesnt contain the user input.
Thanks
You are using a forms.HiddenInput() as the widget and then add the form field yourself. This doesn't work that way. What if you change the field class to TextInput:
class BusinessName(forms.Form):
business_name = forms.CharField(widget = forms.TextInput())
If you're goal is to add custom attributes to the widget, then this can be done by providing an attrs dictionary:
class BusinessName(forms.Form):
business_name = forms.CharField(widget = forms.TextInput(attrs={
'list': 'browser'
}))
Or you could have a look at the django-widget-tweaks package to add attributes in the template.
Is it not possible to iterate over User objects using User.objects.all()??
I am trying to do the same but to no avail
I have a form;
class AddMemberForm(Form):
user = forms.ChoiceField(choices=User.objects.all(),
initial='Choose a User',
)
And I am trying to render it through a template. Corresponding part of views.py below;
class StationHome(View):
def get(self, request, pk):
station = Station.objects.get(pk=pk)
channels = Channel.objects.filter(station=station)
members = station.members.all()
form1 = AddMemberForm()
return render(request,
"home_station.html",
{"form1":form1,
"station":station,
"channels":channels,
"members":members,
},
)
Finally the corresponding part of the corresponding template,
<form method="post" action="{% url 'add_member' station.pk %}">
{% csrf_token %}
{{ form1 }}
</form>
But I am unable to access the URL due to this form. I get a TypeError at corresponding URL 'User' object is not iterable error.
Someone please help out.
Use the ModelChoiceField instead of the simple ChoiceField:
user = forms.ModelChoiceField(queryset=User.objects.all(),
empty_label="(Choose a User)")
UPDATE: You can change the queryset in the form's constructor. For example if you want to exclude already added members from the form:
class AddMemberForm(Form):
...
def __init__(self, *args, **kwargs):
station = kwargs.pop('station')
super(AddMemberForm, self).__init__(*args, **kwargs)
if station:
self.fields['user'].queryset = User.objects.exclude(
id__in=station.members.all())
And then create the form with the station argument:
form1 = AddMemberForm(station=station)