Django: Disabled Dropdown doesn't send information back to POST - python

I have a dropdown in a modelform and the user should not be able to change the selected value.
I found that a disabled does exactly do what I need. However there is an oddness to this:
The first time when the form opens (GET) the value is selected and the user can't change the value. which is great:
But as soon as there is a validation error with an unrelated field and the POST sends the user back to the same form, the previous information is lost. The disabled foreignkey-dropdown no longer contains any value and is very irritating.
I did some research and found something on stackoverflow and seems when a foreignkey-dropdown widget is disabled, no data is sent back at all. While the validation can be overriden to not throw any errors for the dropdown field as the third answer here explains. However if ANY OTHER unrelated field throws an error then the data is lost, because the disabled dropdown had never sent any data to POST in first place.
It is a tricky situation.
Is there a way to pass in the data within the view to the request.POST ? or what do you suggest? I could use a readonly instead ofdisabled and that would work, however the dropdown can be changed by the user, which is also irritating.
Any ideas? Many Thanks
edit:
Small correction: The data is not completely lost. Rather the select is set wrongly to the initial dummy value.
<select id="id_form-0-deal_type" name="form-0-deal_type" disabled="disabled">
<option selected="selected" value="">---------</option>
<option value="1">deal 1</option>
<option value="2">deal 2</option>
</select>
UPDATE:
The solution from Francis looks very promising. So I have tried his second suggestion and added a hidden inputfield in the html and pass in the correct value into the POST.
The problem is now how to proceed. I have tried to add the missing entry in the formset's form's querydict like this (in order to set the correct dropdown value)
formset.forms[0].data['form-0-deal_type'] = formset.forms[0].data['form-0-hiddenfield']
But it says This QueryDict instance is immutable
The only other way to do it is setting it through Initials with regular formsets. Unfortunally I am using modelformsets, which doesn't support initials for existing forms.
If there is no other solution, I start refactoring my modelformset into a regular formset. Still open for ideas...
Final Update + Solution:
There is no need to refactor modelformset into regular fomsets. In fact I highly discourage doing that, since it brings other problems with itself. modelformsets handle everything for you and fill the missing parts.
The actual problem is the fact that QueryDict are immutable, but this can be easily solved by copying them:
formset = deal_formset(request.POST, queryset=formset_query)
if formset.is_valid():
pass
else:
new_post = request.POST.copy()
deal_types = dict()
for k,v in new_post.items():
if k.startswith('hidden'):
deal_types[k[7:]]= v
for k,v in deal_types.iteritems():
new_post[k] = v
formset = deal_formset(new_post, queryset=formset_query)
This plus the solution of Francis:
{{ formset.management_form }}
{% for fs in formset %}
{{ fs.id }}
<input type="hidden" name="hidden-{{ fs.prefix }}-deal_type" value="{{fs.deal_type.value}}" />
{{fs.deal_type}}
{% endfor %}
{% endif %}
just works wonders... enjoy :)

Its not a django thing, its an HTML thing. Disabled form elements are not sent by the form.
[The Element] cannot receive user input nor will its value be submitted with the form.
http://www.w3.org/TR/html401/interact/forms.html#h-17.12.1 & http://www.w3schools.com/tags/att_input_disabled.asp
you could use readonly if its on a text/textarea
http://www.w3schools.com/tags/att_input_readonly.asp
something else you could do, is show the value plaintext, and submit it as a hidden field....
{{ form.field_name.label_tag }}
{{ form.field_name.value }}
<input type="hidden" name="field_name" value="{{form.field_name.value}}" />
its not very elegant, but it could get you there.
You could also take it a step further and write some JS that looks for disabled elements and adds an input with that element's name and value after.
some sample JQuery:
//Untested, but you get the gist
$(':disabled').each(
function()
{
$(this).after('<input type="hidden" name="' + $(this).attr('name') + '" value="' + $(this).val() + '" />');
}
);

Well, you could set the element with hidden property in the template, using formsets in the view to build the form:
{{form.field.as_hidden}}
and inside the view, if the problem is the data loss, you could always set an initial value for the field that suits your model structure, since it's a foreign key. Of course, you will have to validate the form before commiting it, and if the form is not valid, you can render it with initial values on the fields that must be always filled.

I think this is a HTML issue rather than Django, disabled form fields don't post their values back so you're losing the value.
Would it be possible to rebind the value to the field if validation fails? You could try something like
if form.is_valid(): # All validation rules pass
#save form, redirect, etc.
else:
form.disabled_field = my_value
return render(request, 'contact.html', {'form': form,})
Obviously you'll need to replace the field name and value with the correct data from your model.

Related

How do use the input of an HTML <select> tag as a value to get Django Models data, that has that particular input as one of it's data?

I'm using an HTML <select> tag to let the user select the title of a project, where the project is a QuerySet in a Django Model. So I want to use that value to display other data concerned with that project to the user.
The name of the HTML <select> is select_project. In my views.py, I use the request.POST['select_project']and store it in a variable selected_project. In order to get other data in the QuerySet, I used Project.objects.filter(title=selected_project) and stored it in a variable displayed_project which I then used in a View where the user sees all the information about that QuerySet.
enter image description here
But when the user goes to the view, he gets nothing. Only an empty <QuerySet[]>. Please could you help suggest a way around this?
<select class="select_project" name="select_project">
{%for project in projects%}
<option value="" name='select_project'>{{project.title}}</option>
{%endfor%}
Views.py
def overview(request):
if request.method=='POST':
selected_project=request.POST['select_project']
displayed_project=Project.objects.filter(title=selected_project)
return render (request, 'core/overview.html', {
'displayed_project':displayed_project
})
Thank you!
Try to fill the value and name in tag with the value of every project id or title. For example:
<select class="select_project" name="select_project">
{%for project in projects%}
<option value="{{project.id}}" name='{{project.title}}'>{{project.title}}</option>
{%endfor%}
If there will be a problem again, please write here
I've resolved the issue. So I did not know that my View gets the value of the <select> and not actually it's content. So I then added the project.id to the value of the <select> tag. With that done, and after getting the POST data, I filtered the QuerySet using the ID like so:
Project.objects.filter(pk=selected_project)
and at the end, I got the data in the Overview page.
Thanks!

validate_on_submit() fails when RadioButton choices are dynamically generated

I am creating quiz-like web application for learning languages using Flask, Jinja, WTForms, SqlAlchemy etc. Once an user completes such a language course by successfully going through all levels stored in JSON file I want the app offer him a practice mode, where the user will answer randomly selected levels.
When I run the app, I can see radio buttons generated with values from random level as I want, but when I choose any answer and submit it, form.validate_on_submit() returns False and form.errors returns {'practiceform': [u'Not a valid choice']}. When I hard-code value to currentLevel variable, it works properly.
views.py
#user_blueprint.route('/courses/<course>/quiz/practice',methods=['GET','POST'])
#login_required
def practice(course):
courseClass = class_for_name("project.models", course.capitalize())
courses = courseClass.query.filter_by(email=current_user.email).first()
maxLevel = courseClass.query.filter_by(email=current_user.email).first().get_maxLevel()
currentLevel = randint(0, maxLevel-1) # If this value is hard-coded or fetched from db, it works correctly
dic = generateQuestion(course, currentLevel)
display = dic["display"]
correct = dic["correct"]
options = dic["options"]
form = PracticeForm(request.form)
form.practiceform.choices = [(option, option) for option in options]
if form.validate_on_submit():
practiceForm = form.practiceform.data
if ((practiceForm == correct) and courses):
# Do something
flash("Nice job", 'success')
return redirect(url_for('user.practice', course=course))
else:
# Do something else
flash("Wrong answer", 'danger')
return redirect(url_for('user.practice', course=course))
return render_template('courses/practice.html', form=form, display=display)
forms.py
class PracticeForm(Form):
practiceform = RadioField('practice')
practice.html
{% extends "_base.html" %}
{% block content %}
<form action='' method='POST' role='form'>
<p>
<!-- Tried put form.csrf, form.csrf_token, form.hidden_tag() here -->
{{ form.practiceform() }}
</p>
<input type="submit" value="submit" />
</form>
{% endblock %}
So what am I missing there? What makes difference between lets say hardcoded level 25, which works properly or if the number 25 is randomly generated within randint?
My guess is that option is a int, bug WTForms get a str from request.form.
When data comes back from requests it is treated as a string by WTForms unless you specify a type explicitly with the coerce kwarg of the wtforms.fields.*Field constructor:
practiceform = RadioField('practice', coerce=int)
So I found that randint() caused the problem because the practice(course) method was called on both GET and POST actions which led to having two different integers -> two different forms most of the time. So I refactored the code. kept the practice(course) method for GET action and created a new method which handles POST action and this solved the problem.

Thwarting form double-submission through server side tokens (Django)

I am trying to implement a server-side check to prevent users from double-submitting my forms (Django web app).
One technique I'm trying is:
1) When the form is created, save a unique ID in the session, plus pass the unique ID value into the template as well.
2) When the form is submitted, pop the unique ID from the session, and compare it to the same unique ID retrieved from the form.
3) If the values are the same, allow processing, otherwise not.
These SO answers contributed in me formulating this.
Here's a quick look at my generalized code:
def my_view(request):
if request.method == 'POST':
secret_key_from_form = request.POST.get('sk','0')
secret_key_from_session = request.session.pop('secret_key','1')
if secret_key_from_form != secret_key_from_session:
return render(request,"404.html",{})
else:
# process the form normally
form = MyForm(request.POST,request.FILES)
if form.is_valid():
# do something
else:
# do something else
else:
f = MyForm()
secret_key = uuid.uuid4()
request.session["secret_key"] = secret_key
request.session.modified = True
return render(request,"my_form.html",{'form':f,'sk':secret_key})
And here's a sample template:
<form action="{% url 'my_view' %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
<input type="hidden" name="sk" value="{{ sk }}">
{{ form.my_data }}
<button type="submit">OK</button>
</form>
This set up has failed to stop double-submission.
I.e., one can go on a click frenzy and still end up submitting tons of copies. Moreover, if I print secret_key_from_form and secret_key_from_session, I see them being printed multiple times, even though secret_key_from_session should have popped after the first attempt.
What doesn't this work? And how do I fix it?
UPDATE: when I use redis cache to save the value of the special key, this arrangement works perfectly. Therefore, it seems the culprit is me being unable to update request.session values (even with trying request.session.modified=True). I'm open to suggestions vis-a-vis what could be going wrong.
Note that this question specifically deals with a server-side solution to double-submission - my JS measures are separate and exclusive to this question.
You might just need request.session.modified = True. If you want to make sure that the session is deleting you can use del too.
secret_key_from_session = request.session.get('secret_key','1')
del request.session['secret_key']
request.session.modified = True
I couldn't figure out what caused the problem, but via substituting Redis cache for every request.session call, I was able to get my desired results. I'm open to suggestions.

Changing Id of django form in for loop

I'm stuck in my code. Need help.
This is my front end. I am rendering forms stored in "form_list".
The problem is that the forms stored are of same type and thus produce input fields with same "id" and same "name".
This is my view:-
#login_required
def VideoLinkView(request):
"""view to save the video links """
current_form_list = []
current_form = None
if request.method == 'GET':
vl = VideoLink.objects.filter(company=CompanyModel.objects.get(owner=request.user))
for link in vl:
current_form = VideoLinkForm(link.__dict__)
current_form_list.append(current_form)
return render(request, "premium/video_link.html", context={'form_list':current_form_list})
This is my html template :-
{% for form in form_list %}
<div class="form-group">
<label for="id_video_link">Video Link:</label>
{{ form.video_link }}
</div>
{% endfor %}
How can I create different "id" and different "name" in each iteration of for loop's input tag, automatically without having knowledge of no form stored in form_list.
I tried {{ forloop.counter}} it didn't worked, perhaps I made some mistake. Also, raw python don't work in template.
Thanks in Advance.
The way you are creating your forms is wrong in two ways. Firstly, the first positional argument is for the values submitted by the user; passing this arg triggers validation, among other things. If you are passing values for display to prepopulate the form, you must use the initial kwarg:
current_form = VideoLinkForm(initial={...dict_of_values...})
However, even that is not correct for your use case here. link is a model instance; you should use the instance kwarg:
current_form = VideoLinkForm(instance=link)
Now, to solve the problem you asked, you could just pass a prefix as well as I originally recommended:
for i, link in enumerate(vl):
current_form = VideoLinkForm(instance=link, prefix="link{}".format(i))
However, now that you have shown all the details, we can see that this is not the best approach. You have a queryset; so you should simply use a model formset.
from django.forms import modelformset_factory
VideoLinkFormSet = modelformset_factory(VideoLink, form=VideoLinkForm, queryset=vl)
current_form_list = VideoLinkFormSet()

Django ModelChoiceField allow objects creation

Django's ModelChoiceField is the default form field used for foreign keys when deriving a form from a model using ModelForm. Upon validation, the field will check that selected value does exist in the corresponding related table, and raise a ValidationError if it is not the case.
I'm creating a form for a Document model that has a type field, a foreign key to a Type model which does only contain a name attribute. Here is the code of models.py for clarity
class Type(models.Model):
name = models.CharField(max_length=32)
class Document(models.Model):
title = models.CharField(max_length=256)
type = models.ForeignKey(Type, related_name='related_documents')
Instead of a standard select control, I'm using selectize.js to provide auto-completion to the users. Moreover, selectize provides a "create" option, that allows to enter a value that does not exist yet into the select.
I would like to extend the ModelChoiceField in order to create a new Type object when the selected value does not exist (the new value will be assigned to name field, this should be an option of the field for reusability). If possible, I would like the object to not be inserted into DB until save() is called on the validated form (to prevent that multiple failed validation create multiple rows in db). What would be a good way to do so in Django? I tried to look into the documentation and the source code, tried to override ModelChoiceField and tried to build this behavior basted on a TextField but I'm not sure if there isn't a simpler way to do it.
I looked into the following questions but couldn't find the answer.
Django ModelChoiceField has no plus button
Django: override RelatedFieldWidgetWrapper
How to set initial value in a dynamic Django ModelChoiceField
I would like to keep the process of adding new types as simple as possible - i.e.: do not use a pop-up attached to a '+' button. User should be able to type the value, and the value gets created if it doesn't exist.
Thanks
This seems like it'd be easier without using a ModelForm. You could create a list of all of your Type objects then pass that to the template as a context variable. Then, you could use that list to construct a <select> element. Then use jquery and selectize to add the necessary attributes to the form.
#views.py
...
types = Type.objects.all()
...
#template.html
...
<form action="" method="POST">{% csrf_token %}
<input type='text' name='title'>
<select name='type' id='id_type'>
{% for type in types %}
<option value="{{type.id}}">{{type.name}}</option>
{% endfor %}
</select>
<input type='submit'>
</form>
<script type='text/javascript'>
$('#id_type').selectize({
create: true
});
</script>
...
Then when you get a form submission, you can process it in a simple view function:
if request.method == POST:
title_post = request.POST.get('title','')
type_post = request.POST.get('type',None)
if type_post is not None:
try:
#If an existing Type was selected, we'll have an ID to lookup
type = Type.objects.get(id=type_post)
except:
#If that lookup failed, we've got a user-inputted name to use
type = Type.objects.create(name=type_post)
new_doc = Document.objects.create(title=title_post, type=type)

Categories