How to get template variables by using FormView? - python

I am currently following Mozilla's Django tutorial (https://developer.mozilla.org/en-US/docs/Learn/Server-side/Django/Forms). The tutorial mostly shows how to create form using functions. I am trying to make the same function view work by using a generic class view (FormView). I am able to make most of the code to work except for 2 things. First one is that I can't seem to be able to save the due date. And, second one, is that I don't know how to access the model fields in my template using template variables.
Here is my form model from the forms.py file.
class RenewBookModelForm(ModelForm):
def clean_due_back(self):
data = self.cleaned_data['due_back']
# Check if a date is not in the past.
if data < datetime.date.today():
raise ValidationError(ugettext_lazy(
'Invalid date - renewal in past'))
# Check if a date is in the allowed range (+4 weeks from today).
if data > datetime.date.today() + datetime.timedelta(weeks=4):
raise ValidationError(ugettext_lazy(
'Invalid date - renewal more than 4 weeks ahead'))
# Remember to always return the cleaned data.
return data
class Meta:
model = BookInstance
fields = ['due_back']
labels = {'due_back': ugettext_lazy('New renewal date')}
help_texts = {'due_back': ugettext_lazy(
'Enter a date between now and 4 weeks (default 3).')}
The form model implemented as a function:
#permission_required('catalog.can_mark_returned')
def renew_book_lirarian(request, pk):
book_instance = get_object_or_404(BookInstance, pk=pk)
# If this is a POST request then process the Form data
if request.method == 'POST':
# Create a form instance and populate it with data from the request (binding):
form = RenewBookModelForm(request.POST)
# Chech if the form is valid
if form.is_valid():
# process that data in form.cleaned_data as required (here we just write to the model due_back field)
book_instance.due_back = form.cleaned_data['due_back']
book_instance.save()
# redirect to a new URL
return HttpResponseRedirect(reverse('all-borrowed'))
# If this is a GET (or any other method) create the default form.
else:
proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
form = RenewBookModelForm(initial={'due_back': proposed_renewal_date})
context = {
'form': form,
'book_instance': book_instance
}
return render(request, 'catalog/book_renew_librarian.html', context=context)
This is my class-based view from my views.py file:
class RenewBookLibrarian(LoginRequiredMixin, PermissionRequiredMixin, generic.FormView):
"""Generic class-based view for forms."""
template_name = 'catalog/book_renew_librarian.html'
permission_required = 'catalog.can_mark_returned'
form_class = RenewBookModelForm
success_url = reverse_lazy('all-borrowed')
def get_initial(self):
proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
initial = {'due_back': proposed_renewal_date}
return initial
And finally this is my template file where I wish to access the model fields:
{% extends 'base_generic.html' %}
{% block content %}
<h1>Renew: {{ book_instance.book.title }}</h1>
<p>Borrower: {{ book_instance.borrower }}</p>
<p {% if book_instance.is_overdue %} class="text-danger" {% endif %}>Due date: {{ book_instance.due_back }}</p>
<form action="" method="POST">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" value="submit">
</form>
{% endblock %}
The book_instance variable in the template is not working, hence I would like to know how I can display fields from my BookInstance model.

To add book_instance to the template context, you can override get_context_data.
In the FormView, instead of checking if form.is_valid(), you override the form_valid method (see the class based view docs for basic forms).
class RenewBookLibrarian(LoginRequiredMixin, PermissionRequiredMixin, generic.FormView):
...
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['book_instance'] = get_object_or_404(BookInstance, pk=self.kwargs['pk'])
def form_valid(self, form):
book_instance = get_object_or_404(BookInstance, pk=self.kwargs['pk'])
book_instance.due_date = form.cleaned_data['due_date']
book_instance.save()
return super().form_valid(form) # will redirect to the success url

Related

Rendering list of forms in Django

I am trying to create a basic personality test in Django as a proof-of-concept at work. I'm new to Django (and python in general), coming at it from a C# .NET background.
I am trying to make a list of form objects (populated with information pulled from question objects stored in the database), then display them in the HTML.
This is only partly working; I can render the form attributes individually in a for loop (by calling, for example, question.pk) but nothing renders with the standard Django {{ form }} tag, and trying to submit the list of forms breaks the whole thing.
I'm pretty sure it's an issue with handling a bunch of form objects populated inside one larger html , but I'm not sure how to go about resolving it.
I've done some research into formsets, but I can't find any way to pre-populate the form items with information from the database.
Thanks in advance!
DISCQuestionForm in forms.py:
class DISCQuestionForm(forms.Form):
# create new form object from database question object
def __init__(
self,
pk,
disc_query,
dom_answer,
infl_answer,
stead_answer,
con_answer,
):
super().__init__()
self.pk = pk
self.disc_query = disc_query
self.dom_answer = dom_answer
self.infl_answer = infl_answer
self.stead_answer = stead_answer
self.con_answer = con_answer
self.disc_response = forms.DecimalField(
max_value=4,
widget=forms.NumberInput
)
disc_create method in views.py
# Create a new DISC assessment for current user
def disc_create(request, pk):
profile = User.objects.get(pk=pk)
user = int(profile.pk)
name = profile.name
rawquestionset = DISCQuestion.objects.all()
discformset = []
for item in rawquestionset:
question = DISCQuestionForm(
pk=item.pk,
disc_query=item.disc_query,
dom_answer=item.dom_answer,
infl_answer=item.infl_answer,
stead_answer=item.stead_answer,
con_answer=item.con_answer,
)
discformset.append(question)
if request.method == 'POST':
questionset = discformset[request.POST]
if questionset.is_valid():
dom = 0
infl = 0
stead = 0
con = 0
for discquestion in questionset:
if discquestion.disc_response == discquestion.dom_answer:
dom += 1
if discquestion.disc_response == discquestion.infl_answer:
infl += 1
if discquestion.disc_response == discquestion.stead_answer:
stead += 1
if discquestion.disc_response == discquestion.con_answer:
con += 1
disctest = DISCTest(
user=user,
name=name,
dom=dom,
infl=infl,
stead=stead,
con=con,
)
disctest.save()
else:
questionset = discformset
context = {
"pk": user,
"name": name,
"discquestionset": questionset
}
return render(request, "disc_create.html", context)
DISCTest and DISCQuestion models in models.py:
class DISCTest(models.Model):
user = models.ForeignKey('User', on_delete=models.CASCADE)
name = user.name
created_on = models.DateTimeField(auto_now_add=True)
dom = models.DecimalField(max_digits=3, decimal_places=0)
infl = models.DecimalField(max_digits=3, decimal_places=0)
stead = models.DecimalField(max_digits=3, decimal_places=0)
con = models.DecimalField(max_digits=3, decimal_places=0)
class DISCQuestion(models.Model):
disc_query = models.TextField()
disc_response = models.DecimalField(max_digits=1, decimal_places=0, null=True)
dom_answer = models.DecimalField(max_digits=1, decimal_places=0)
infl_answer = models.DecimalField(max_digits=1, decimal_places=0)
stead_answer = models.DecimalField(max_digits=1, decimal_places=0)
con_answer = models.DecimalField(max_digits=1, decimal_places=0)
and finally disc_create.html in templates:
{% extends "base.html" %}
{% block page_content %}
<div class="col-md-8 offset-md-2">
<h1>Take your DISC assessment</h1>
<hr>
<h3>Insert instructions here</h3>
<hr>
<form action="/assessment/create/{{pk}}/" method="post">
{% csrf_token %}
<div>
{% for question in discquestionset %}
<p>{{question.pk}}</p>
<p>{{ question.disc_query }}</p>
{{ form }}
{% endfor %}
</div>
<button type="submit">Submit</button>
</form>
</div>
{% endblock %}
Your DiscQuestionForm has no fields. disc_response is defined as an attribute of the form but for Django it isn't a field because it isn't added to self.fields. And form isn't defined in your template in your for loop, only question (which is the form) so {{ question }} would print the form if it had any fields.
But then the problem is that each of your question form fields would all have the same "name" attributes because they are not prefixed to make them unique.
You should read this document carefully to understand ModelForm and modelformset. Basically you need:
class DISCQuestionForm(forms.ModelForm):
class Meta:
model = DISCQuestion
def __init__(...):
...
Use the modelformset_factory to create a proper ModelFormSet that you can initialise with the request.POST when submitted.
DISCQuestionFormSet = modelformset_factory(DISCQuestionForm, form = DISCQuestionForm) # note DISCQuestionForm not needed if you don't customise anything in your form.
and in your view:
formset = DISCQuestFormSet(request.POST or None)
then in your template you can loop through the forms in the formset:
{% for form in formset %}{{ form }}{% endfor %}

Pre-populating a child models django create form with a parent's ID

I have followed the guidelines from This answer in order to pass Parent pk to the child creation page. At the moment though it is not working and I am seeing the following log.
[14/Jul/2017 13:15:37] "POST /catalog/productstatus/2/create/ HTTP/1.1" 200 4001
I'm not sure what I'm doing wrong, here is the code I currently have.
Models
Models.py
class Product(models.Model):
serial_number = models.CharField(unique=True, max_length=15)
class ProductStatus(models.Model):
serial_number = models.ForeignKey('Product', on_delete=models.CASCADE, null=True)
status = models.CharField(max_length=20, blank=True, default='Stock', help_text='Products status')
date = models.DateTimeField(auto_now_add=True)
View
class ProductStatusCreate(CreateView):
model = ProductStatus
template_name = 'catalog/productstatus_create.html'
form_class = ProductStatusModelForm
def form_valid(self, form):
productstatus = form.save(commit=False)
product_id = form.data['product_id']
product = get_object_or_404(Product, id=product_id)
productstatus.product = product
return super(ProductStatusCreate, self).form_valid(form)
def get_context_data(self, **kwargs):
context = super(ProductStatusCreate, self).get_context_data(**kwargs)
context['s_id'] = self.kwargs['product_id']
return context
def get_success_url(self):
if 'product_id' in self.kwargs:
product = self.kwargs['product_id']
else:
product = self.object.product.pk
return reverse_lazy('product_detail', kwargs={'pk': product})
Forms
class ProductStatusModelForm(forms.ModelForm):
class Meta:
model = ProductStatus
fields = ['status',]
def __init__(self, *args, **kwargs):
self.fields["product"] = forms.CharField(widget=forms.HiddenInput())
super(ProductStatusModelForm, self).__init__( *args, **kwargs)
templates/myapp/product_detail.html
New
urls.py
urlpatterns += [
url(r'^productstatus/(?P<product_id>\d+)/create/$', views.ProductStatusCreate.as_view(), name='productstatus_create'),
]
productstatus_create.html
{% extends "base_generic.html" %}
{% block content %}
<h2>New Product Status</h2>
</br>
<form action="" method="post">
{% csrf_token %}
<table>
<input type=hidden id="id_product" name="product" value="{{ s_id }}">
{{ form }}
</table>
<input type="submit" value="Submit" />
</form>
</br>
{% endblock %}
When looking at the page's source the value does get populated but when I submit the form nothing happens.
Why do you have views.ProductInstanceCreate.as_view() in your urls.py but the view you show is called ProductStatusCreate? Are you sure you are using the right view?
You are creating a 'product' hidden field in your form, but not providing a value for it anywhere. Your template output then has two product fields, and the latter (blank) is taken, so returns an error saying it is required.
None of this outputting the product ID to the template in order to read it back in is necessary - you always have the ID available to you in the URL kwargs.
You can get rid of your get_context_data, and the extra field code in the Form and template. Your form_valid can be something like:
def form_valid(self, form):
product = get_object_or_404(Product, id=self.kwargs['product_id'])
form.instance.product = product
return super().form_valid(form)
And product_id will always be in self.kwargs, so your get_success_url can be shorter too:
def get_success_url(self):
product = self.kwargs['product_id']
return reverse('product_detail', kwargs={'pk': product})

Issue with Django form POST method

I am having a problem with the following view and form. The form loads correctly however when I edit any of the fields it does not save. After a bit of debugging I think it is due to one of two things: either request.method == "POST" is evaluating to false, or form.is_valid() is evaluating to false. So potentially something wrong with my template or my clean() method? I've searched previous questions and can't find anything that helps. I've also checked my clean() method against the Django docs and think it is OK.
views.py
#login_required
def edit_transaction(request, pk):
transaction = get_object_or_404(Transaction, pk=pk)
if request.method == "POST":
form = TransactionForm(request.POST, instance=transaction)
if form.is_valid():
transaction = form.save(commit=False)
transaction.updated = timezone.now()
transaction.save()
return redirect('view_transaction_detail', pk=transaction.pk)
else:
form = TransactionForm(request=request, instance=transaction)
return render(request, 'budget/new_transaction.html', {'form': form})
forms.py
class TransactionForm(forms.ModelForm):
class Meta:
model = Transaction
fields = ('title', 'transaction_type', 'category', 'budgeted_amount', 'actual_amount', 'date', 'comments',)
#new_category field to allow you to add a new category
new_category = forms.CharField(max_length=30, required=False, label="New Category Title")
def __init__(self, request, *args, **kwargs):
super(TransactionForm, self).__init__(*args, **kwargs)
#category is now not a required field because you will use category OR new_category
self.fields['category'].required=False
#set to allow use of self.request.user to set user for category
self.request = request
def clean(self):
category = self.cleaned_data.get('category')
new_category = self.cleaned_data.get('new_category')
if not category and not new_category:
# raise an error if neither a category is selected nor a new category is entered
raise forms.ValidationError('Category or New category field is required')
elif not category:
# create category from new_category
category, created = Category.objects.get_or_create(title=new_category, defaults={'user': self.request.user})
self.cleaned_data['category'] = category
return super(TransactionForm, self).clean()
template
{% extends 'budget/base.html' %}
{% block content %}
<h2>New transaction</h2>
<h4>To add a new category, leave Category blank and enter your new category in the New Category Title field</h4>
<form method="POST" class="post-form">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="save btn btn-default">Save</button>
</form>
{% endblock %}
update following answer - accessing request through kwargs
def __init__(self, *args, **kwargs):
super(TransactionForm, self).__init__(*args, **kwargs)
self.fields['category'].required=False
self.request = kwargs.pop('request', None)
As I mentioned on your last question, since you've changed the signature of the form's init method you need to pass the request both times you instantiate it. You're only doing so when it is not POST; so, when it is a POST, Python takes the data that you passing and assigns it to the request argument, leaving the data itself blank.
form = TransactionForm(request, data=request.POST, instance=transaction)
Note it is precisely for this reason that is is a bad idea to change the signature; instead, pass request as a keyword argument and inside the method get it from kwargs.

Getting a 'This field is required` error when updating a model instance

I have an edit view for when a user wants to edit a Post:
def edit(request, id):
post = get_object_or_404(Post, id=id)
edit_form = PostForm(request.POST or None, instance=post)
if edit_form.is_valid():
instance = edit_form.save(commit=False)
instance.save(update_fields=['content'])
return HttpResponseRedirect('/')
else:
print(edit_form.errors)
edit_form = PostForm(instance=post)
context = {
'edit_form': edit_form,
'form_post': post
}
return render(request, 'edit.html', context)
When a user edits a Post, I only want them to be able to edit 1 field (content), so i've only rendered that form field in my template (pre-populated with the previous post.content. The other fields are just fields of the object (not a form/can't be edited).
...
<form method="post" action="" enctype="multipart/form-data">{% csrf_token %}
<h1>{{ form_post.title }}</h1>
<p>{{ edit_form.content}}</p>
<p>{{ form_post.category }}</p>
</form>
...
and here is my Post model:
class Post(models.Model):
...
title = models.TextField(max_length=76)
content = models.TextField(null=False, default='')
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES, default='1')
When the edit form is submitted, form_errors returns this:
<ul class="errorlist">
<li>title<ul class="errorlist"><li>This field is required.</li></ul></li>
<li>category<ul class="errorlist"><li>This field is required.</li></ul</li>
</ul>
Why is this happening? Doesn't:
instance = edit_form.save(commit=False)
instance.save(update_fields=['content'])
keep the fields from the orignal Post and just change the content field?
If you only want some of the fields to be editable, you should set fields in your model form. If you use PostForm in another view and cannot edit fields, then create a new form.
class EditPostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['content']
You can subclass PostForm if you prefer:
class EditPostForm(PostForm):
class Meta(PostForm.Meta):
fields = ['content']
Then update your edit view to use EditPostForm instead of PostForm.

Pass database value in form (django)

My form.py:
class BannerForm(forms.ModelForm):
name = forms.CharField(max_length=32)
#Affiliazione = forms.CharField(disabled = True, initial='red') #in original question
#affiliation = forms.ModelChoiceField(Affiliation.objects.all(),
#widget=forms.HiddenInput(), initial=Affiliation.objects.get(id=1)) #in original question
Affiliazione = forms.CharField(disabled = True, required=False) #added after first answer
affiliation = forms.ModelChoiceField(Affiliation.objects.all(),
widget=forms.HiddenInput()) #added after first answer
The 'Affiliazione' field display 'red' but it isn't saved because Disabled controls cannot be successful. The 'affiliation' field actually pass the data but is hidden. They together give what I want (a disabled field that pass data so the user can see the value but can't change it).
The problem is I don't like to hardcode that values ('red' and 'id=1'). I have the 'Options' class in models where I choose the value to pass but I don't know how... I think it's a silly question, sorry, but someone can help me?
My models.py:
class Options(models.Model):
new_affiliation = models.ForeignKey('Affiliation')
class Affiliation(models.Model):
name = models.CharField(max_length=32, unique=True)
def __str__(self):
return self.name
class Banner(models.Model):
name = models.CharField(max_length=32, unique=True)
affiliation = models.ForeignKey(Affiliation)
Edit. My View.py:
def add_banner(request):
# A HTTP POST?
if request.method == 'POST':
form = BannerForm(request.POST)
print('form is post') #control
# Have we been provided with a valid form?
if form.is_valid():
print('form is valid') #control
# Save the new banner to the database.
banner = form.save(commit=False)
#some irrilevant code here
form.save(commit=True)
print('form salvato') #control
# Now call the homepage() view.
# The user will be shown the homepage.
return homepage(request)
else:
# The supplied form contained errors - just print them to the terminal
print (form.errors)
else:
# If the request was not a POST, display the form to enter details
#form = BannerForm(request.POST) #in original question
#initial_value = 'red' #added after first answer
#init = Affiliation.objects.get(id=1) #added after first answer
form = BannerForm(request.POST or None, initial={
'affiliation': Campaign_Options.new_regent_affiliation}) #added after first answer
# Bad form (or form details), no form supplied...
# Render the form with error messages (if any).
print ('fine')
return render(request, 'core/add_banner.html', {'form': form})
My add_banner.html:
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% for field in form.visible_fields %}
{{ field.errors }}
{{ field.label }}
{{ field }}
{{ field.help_text }}
<br />
{% endfor %}
Even if I don't quite get the intention of your form, but for the sake of answering your question, you could pass the initial value from your views when you initialize the form, this will make the value flexible:
def your_view(request):
# get the string for affilizione by applying your logic here
# initial_value = 'red'
form = BannerForm(request.POST or None, initial={'Affiliazione': initial_value})

Categories