Have a master-detail objects with a one-to-many relationship:
from django.db import models
class Master(models.Model):
field1 = models.CharField(max_length=100)
field2 = models.IntegerField()
class Detail(models.Model):
field3 = models.IntegerField()
field4 = models.IntegerField()
field5 = models.IntegerField()
master = models.ForeignKey(Master, on_delete=models.CASCADE)
For details, I have a ModelForm and an inline formset:
from django import forms
from .models import Master, Detail
class MasterForm(forms.Form):
field1 = forms.CharField(label='Field 1', max_length=100)
field2 = forms.IntegerField(label='Field 2')
class DetailsForm(forms.ModelForm):
class Meta:
model = Detail
exclude = ()
DetailsFormset = forms.inlineformset_factory(
Master, Detail, form=DetailsForm, extra=1)
I have a template view:
class MasterDetailsView(TemplateView):
template_name = 'app/master_detailsview.html'
def post(self, request, *args, **kwargs):
print('IN POST!')
details_formset = DetailsFormset(request.POST)
if details_formset.is_valid():
print('FORMSET VALID!')
Master.objects.get(pk=self.kwargs['pk']).save()
details_formset.save()
else:
print('ERRORS!')
print(details_formset.errors)
return HttpResponseRedirect(self.request.path_info)
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(**kwargs)
pk = self.kwargs['pk']
master_instance = Master.objects.get(pk=pk)
context['master_instance'] = master_instance
if self.request.POST:
context['details_formset'] = DetailsFormset(self.request.POST, instance=master_instance)
else:
context['details_formset'] = DetailsFormset(instance=master_instance)
return context
and the template:
{% extends 'base.html' %}
{% block contents %}
<table class="table table-bordered">
<tr>
<th>Field 1</th>
<th>Field 2</th>
</tr>
<tr>
<td>{{ master_instance.field1 }}</td>
<td>{{ master_instance.field2 }}</td>
</tr>
</table>
<hr/ >
<form action="" method="post">
{% csrf_token %}
{{ details_formset.as_p }}
<input type="submit" value="Save" />
</form>
<hr/ >
{% endblock %}
The error I get in the console:
[{'master': ['The inline value did not match the parent instance.']}]
I suppose my view is not right. I have tried getting the master record and saving it before the details formset but same error. I am not using CreateView because this is a learning project.
Try adding {{details_formset.management_form}} right under the csrf_token in templates
Related
The Priority model has three values, for each of them values I'm returning an inlineform which allows the user to set a score for each priority & then save with the Project.
This is what it currently looks like: Current view
My problem is: how can I automatically show all the priority values and allow the user to enter the score but not have to pick the Priority. Is it possible to show it like the image below?
What I'm trying to do.
Views.py
class ProjectCreateview(LoginRequiredMixin, CreateView):
model = Project
form_class = ProjectCreationForm
login_url = "/login/"
success_url = '/'
def get_context_data(self, **kwargs):
PriorityChildFormset = inlineformset_factory(
Project, ProjectPriority, fields=('project', 'priority', 'score'), can_delete=False, extra=Priority.objects.count(),
)
data = super().get_context_data(**kwargs)
if self.request.POST:
data['priorities'] = PriorityChildFormset(self.request.POST)
else:
data['priorities'] = PriorityChildFormset()
return data
def form_valid(self, form):
context = self.get_context_data()
prioritycriteria = context["priorities"]
form.instance.creator = self.request.user
self.object = form.save()
prioritycriteria.instance = self.object
if prioritycriteria.is_valid():
prioritycriteria.save()
return HttpResponseRedirect(self.get_success_url())
Models.py
class Priority(models.Model):
title = models.CharField(verbose_name="Priority Name", max_length=250)
def __str__(self):
return self.title
class Project(models.Model):
name = models.CharField(verbose_name="Name", max_length=100)
details = models.TextField(verbose_name="Details/Description", blank=False)
creator = models.ForeignKey(User, on_delete=models.CASCADE)
class ProjectPriority(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE)
priority = models.ForeignKey(Priority, on_delete=models.CASCADE)
score = models.CharField(max_length=1000, choices=priority_choices)
class Meta:
verbose_name = "Priority"
verbose_name_plural = "Priorities"
Template
<table class="table table-light">
<tbody>
{{ priorities.management_form }}
{% for priority in priorities.forms %}
<tr>
{% for field in priority.visible_fields %}
<td>
{{ field.errors.as_ul }}
{{ field }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
You can do this by using initial data with your formset (see the Django documentation here).
In your views.py code you can add this line to generate some initial values for the priority fields:
initial = [{'priority': priority} for priority in Priority.objects.all()]
And then pass it to your formset:
data['priorities'] = PriorityChildFormset(initial=initial)
Note that initial expects a list of the same length as the formset you created, which is defined by the extra parameter. This works because both of these parameters have been defined using the same base queryset (i.e. Priority.objects). If a filter were applied - it would need to apply in both places.
Changing how it displays
In addition, if you want to prevent the priority fields from being changed by the user using the dropdown menu, you can pass a widgets keyword argument to inlineformset_factory to set a disabled attribute on the <select> element that gets generated e.g.:
from django import forms
...
widgets={'priority': forms.Select(attrs={'disabled': True})}
If you want the field to show only text - this is more difficult. The string representation of the related object that we want is buried in the field choices that are used to generate each <option>. If you want to do this, you can dig out each field manually:
{% for priority in priorities.forms %}
<tr>
<td>
{{ priority.priority.errors.as_ul }}
{% for value, name in priority.priority.field.choices %}
{% if value == priority.priority.value %}
{{ name }}
{% endif %}
{% endfor %}
{{ priority.priority.as_hidden }}
</td>
<td>
{{ priority.score.errors.as_ul }}
{{ priority.score }}
</td>
</tr>
{% endfor %}
Using Class Based Views, ModelForms, and Inlline Formsets. I’m making a recipe application in Django. Each user has their own OneToOne RecipeBook object, which in turn can hold as many recipes as needed, as each Recipe has a ForeignKey relationship to the RecipeBook object. There are also Ingredient and Direction objects that each have a FK relationship to the Recipe object.
The good news is that I can create a Recipe object using my CreateView, with as many associated Ingredient and Direction objects as I want. The Ingredient/Direction objects should be unique to each Recipe object (and by extension, each User). However, when I create a Recipe object, and then I try to create a new Recipe object, its Ingredient and Direction fields are already populated on the new object, form the old object. So if I had just created a Recipe with 3 Ingredient/Direction fields all set to '1', and then go to create another Recipe, the new Recipe object will have all blank fields, but will have 3 Ingredient/Direction objects all set to 1. This will happen to each user that is logged in. I want to make it so these objects are all staying together.
I think the issue to this is that either my get_context_data or my form_valid methods are saving the Ingredient/Direction objects globally, when I just want each Ingredient/Direction object to only be associated with the specific recipe object. I’ve tried messing with the init function of my Forms, I’ve tried querying for the object before/while its being created, and it seems like no matter what I do I’m just running in circles. I’d appreciate any help/resources anyone can point me towards!
My Models:
class RecipeBook(models.Model):
"""Each user has a single associated RecipeBook object, linked in this OneToOne field"""
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
class Recipe(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4())
recipebook = models.ForeignKey(RecipeBook, related_name='recipe_set', on_delete=models.CASCADE)
title = models.CharField(max_length=150, help_text='Title of the recipe')
description = models.TextField(help_text='Description of the recipe', blank=True)
# image = models.ImageField(height_field=, width_field=, help_text='Image of the recipe', blank=True)
servings = models.PositiveSmallIntegerField(help_text='The amount of servings the recipe will yield', default=0, blank=True)
prep_time = models.PositiveSmallIntegerField(help_text='The preparation time', default=0, blank=True)
cook_time = models.PositiveSmallIntegerField(help_text='The cooking time', default=0, blank=True)
url = models.URLField(blank=True)
TIME_UNITS = (
('m', 'Minutes'),
('h', 'Hours')
)
def get_absolute_url(self):
return reverse('recipe_book:recipe-detail', args=[str(self.id)])
def __str__(self):
return self.title
class Ingredient(models.Model):
recipe = models.ForeignKey(Recipe, related_name='ingredient_set', on_delete=models.CASCADE)
name = models.CharField(max_length=100)
amount = models.CharField(max_length=20, blank=True)
def __str__(self):
return self.name
class Direction(models.Model):
recipe = models.ForeignKey(Recipe, related_name='direction_set', on_delete=models.CASCADE)
step_instructions = models.TextField(help_text='Write the instructions of the step here')
My Forms:
class AddRecipeForm(ModelForm):
recipe = forms.ModelChoiceField(queryset=Recipe.objects.all())
class Meta:
model = Recipe
fields = ['title', 'description', 'servings', 'prep_time', 'cook_time', 'url']
class AddIngredientForm(ModelForm):
class Meta:
model = Ingredient
fields = ['name', 'amount']
# def __init__(self, *args, **kwargs):
# self.recipe = kwargs.pop('recipe')
# super(AddIngredientForm, self).__init__(*args, **kwargs)
#
# if not self.instance:
# self.fields['name'].initial = self.recipe.default_name
# self.fields['amount'].widget = forms.TextInput(required=False)
#
# def save(self, *args, **kwargs):
# self.instance.recipe = self.recipe
# ingredient = super(AddIngredientForm, self).save(*args, **kwargs)
# return ingredient
IngredientFormset = inlineformset_factory(Recipe, Ingredient, form=AddIngredientForm, extra=1, can_delete=True)
class AddDirectionForm(ModelForm):
class Meta:
model = Direction
fields = ['step_instructions']
# def __init__(self, *args, **kwargs):
# self.recipe = kwargs.pop('recipe')
# super(AddDirectionForm, self).__init__(*args, **kwargs)
#
# if not self.instance:
# self.fields['step_instructions'].initial = self.recipe.default_step_instructions
#
# def save(self, *args, **kwargs):
# self.instance.recipe = self.recipe
# direction = super(AddDirectionForm, self).save(*args, **kwargs)
# return direction
DirectionFormset = inlineformset_factory(Recipe, Direction, form=AddDirectionForm, extra=1, can_delete=True)
My View:
class RecipeListView(LoginRequiredMixin, generic.ListView):
model = models.Recipe
context_object_name = 'recipes'
# Using this method ensures that the only recipes that are displayed are the ones associated with each user
def get_queryset(self):
return models.Recipe.objects.filter(recipebook=self.request.user.recipebook)
class RecipeDetailView(LoginRequiredMixin, generic.DetailView):
model = models.Recipe
fields = ['title', 'description', 'servings', 'prep_time', 'cook_time', 'url']
context_object_name = 'recipe'
def get_queryset(self):
return models.Recipe.objects.filter(recipebook=self.request.user.recipebook)
# Classes used to actually create full recipe objects
class RecipeCreate(LoginRequiredMixin, CreateView):
model = models.Recipe
fields = ['title', 'description', 'servings', 'prep_time', 'cook_time', 'url']
def get_queryset(self):
return models.Recipe.objects.filter(recipebook=self.request.user.recipebook)
def get_context_data(self, **kwargs):
data = super(RecipeCreate, self).get_context_data(**kwargs)
#user = self.request.user
if self.request.POST:
data['ingredients'] = IngredientFormset(self.request.POST)
#queryset=models.Recipe.objects.filter(recipebook=self.request.user.recipebook))
data['directions'] = DirectionFormset(self.request.POST)
#queryset=models.Recipe.objects.filter(recipebook=self.request.user.recipebook))
else:
data['ingredients'] = IngredientFormset()
#queryset=models.Recipe.objects.filter(self.kwargs['id']))
data['directions'] = DirectionFormset()
#queryset=models.Recipe.objects.filter(self.kwargs['id']))
return data
def form_valid(self, form):
form.instance.recipebook = self.request.user.recipebook
context = self.get_context_data()
ingredients = context['ingredients']
directions = context['directions']
# self.object is the object being created
self.object = form.save()
if ingredients.is_valid():
ingredients.instance = self.object
ingredients.save()
if directions.is_valid():
directions.instance = self.object
directions.save()
return super(RecipeCreate, self).form_valid(form)
class RecipeUpdate(LoginRequiredMixin, UpdateView):
model = models.Recipe
fields = ['title', 'description', 'servings', 'prep_time', 'cook_time', 'url']
def get_context_data(self, **kwargs):
data = super(RecipeUpdate, self).get_context_data(**kwargs)
if self.request.POST:
data['ingredients'] = IngredientFormset(self.request.POST, instance=self.object)
#queryset=models.Recipe.objects.filter(recipebook=self.request.user.recipebook))
data['directions'] = DirectionFormset(self.request.POST, instance=self.object)
#queryset=models.Recipe.objects.filter(recipebook=self.request.user.recipebook))
else:
data['ingredients'] = IngredientFormset(instance=self.object)
#queryset=models.Recipe.objects.filter(recipebook=self.request.user.recipebook))
data['directions'] = DirectionFormset(instance=self.object)
#queryset=models.Recipe.objects.filter(recipebook=self.request.user.recipebook))
return data
def form_valid(self, form):
form.instance.recipebook = self.request.user.recipebook
context = self.get_context_data()
ingredients = context['ingredients']
directions = context['directions']
self.object = form.save()
if ingredients.is_valid():
ingredients.instance = self.object
ingredients.save()
if directions.is_valid():
directions.instance = self.object
directions.save()
return super(RecipeUpdate, self).form_valid(form)
My Template:
{% extends 'base-recipe.html' %}
{# https://simpleit.rocks/python/django/dynamic-add-form-with-add-button-in-django-modelformset-template/ #}
{% block content %}
<div class="container">
<div class="card">
<div class="card-header">Create Recipe</div>
<div class="card-body">
<form action="" method="POST"> {% csrf_token %}
{# table for the Recipe object, manually rendering it for more control #}
<table class="table">
<tr>
<td>{{ form.title.label_tag }}</td>
<td>{{ form.title }}</td>
</tr>
<tr>
<td>{{ form.description.label_tag }}</td>
<td>{{ form.description }}</td>
</tr>
<tr>
<td>{{ form.servings.label_tag }}</td>
<td>{{ form.servings }}</td>
</tr>
<tr>
<td>{{ form.prep_time.label_tag }}</td>
<td>{{ form.prep_time }}</td>
</tr>
<tr>
<td>{{ form.cook_time.label_tag }}</td>
<td>{{ form.cook_time }}</td>
</tr>
<tr>
<td>{{ form.url.label_tag }}</td>
<td>{{ form.url }}</td>
</tr>
</table>
{# table for the ingredient(s) object(s) #}
<table class="table">
{{ ingredients.management_form }}
{% for form in ingredients.forms %}
{% if forloop.first %}
<thead>
<tr>
{% for field in form.visible_fields %}
<th>{{ field.label|capfirst }}</th>
{% endfor %}
</tr>
</thead>
{% endif %}
<tr class="{% cycle row1 row2 %} formset_row-{{ ingredients.prefix }}">
{% for field in form.visible_fields %}
<td>
{# include the hidden fields in the form #}
{% if forloop.first %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% endif %}
{{ field.errors.as_ul }}
{{ field }}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
<table class="table">
{{ directions.management_form }}
{% for form in directions.forms %}
{% if forloop.first %}
<thead>
<tr>
{% for field in form.visible_fields %}
<th>{{ field.label|capfirst }}</th>
{% endfor %}
</tr>
</thead>
{% endif %}
<!--<tr class="{% cycle row1 row2 %} formset_row-{{ directions.prefix }}">-->
<tr class="formset_row-{{ directions.prefix }}">
{% for field in form.visible_fields %}
<td>
{# include the hidden fields #}
{% if forloop.first %}
{% for field in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% endif %}
{{ field.errors.as_ul }}
{{ field }}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
<input class="btn btn-primary" type="submit" value="Submit">
<a class="btn btn-danger" href="{% url 'recipe_book:index' %}">Back to the recipe list</a>
</form>
</div>
</div>
</div>
{% load static %}
<script src="{% static 'js/jquery.formsets.js' %}"></script>
<script type="text/javascript">
$('.formset_row-{{ ingredients.prefix }}').formset({
addText: 'Add Another Ingredient',
deleteText: 'Remove',
prefix: '{{ ingredients.prefix }}',
});
$('.formset_row-{{ directions.prefix }}').formset({
addText: 'Add another',
deleteText: 'Remove',
prefix: '{{ directions.prefix }}',
});
</script>
{% endblock %}
For anyone who also has this issue, here's the fix, from the Django forum's user KenWhitesell:
You can chase this down through the source code if you really want to
understand what’s going on, but the Reader’s Digest version is that an
inline formset is created under the assumption that the formset is
linked to an existing instance. If one isn’t supplied, it selects one
from the database.
The fix, for me, was in the CreateView's get_context_data() method: since we don't want the inline_formset to be querying for any objects on a CreateView, you have to explicitly tell it not to with a queryset parameter on the GET request like this:
def get_context_data(self, **kwargs):
data = super(RecipeCreate, self).get_context_data(**kwargs)
if self.request.POST:
data['ingredients'] = IngredientFormset(self.request.POST)
data['directions'] = DirectionFormset(self.request.POST)
else:
data['ingredients'] = IngredientFormset(queryset=models.Ingredient.objects.none())
data['directions'] = DirectionFormset(queryset=models.Direction.objects.none())
return data
Here's a link to our forum post discussing the issue: https://forum.djangoproject.com/t/my-forms-are-being-populated-with-the-data-that-was-entered-in-the-last-createview/6278/12
i am trying to create a student attendance sheet in django using django modelformset_factory...but when i save the formset it thows me the id is not valid here is my implementation
i have two models one StudentAttendance and StudentClass:
1: the StudentAttendance model is responsible for stroring students
attendance data here is the example
class StudentAttendance(models.Model):
classroom_id = models.ForeignKey(ClassRoom, on_delete=models.CASCADE, related_name='student_attendance')
attendance_date = models.DateField()
student_id = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='student_attendance')
status = models.CharField(max_length=20, choices=ATTENDANCE_CHOICES)
comment = models.CharField(max_length=150, blank=True)
#signed_by = models.ForeignKey(Teacher, on_delete=models.CASCADE)
date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return str(self.student_id)
2: the StudentClass model is a submodel that maps a student to his respective class
class StudentClass(models.Model):
"""
This is a bridge table to link a student to a class
when you add a student to a class we update the selected class capacity
"""
main_class = models.ForeignKey(ClassRoom, on_delete=models.CASCADE, related_name='class_student')
academic_year = models.ForeignKey(AcademicYear, on_delete=models.CASCADE)
student_id = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='student_class')
#property
def is_current_class(self):
if self.academic_year.is_current_session:
return True
return False
def __str__(self):
return str(self.student_id)
So my forms.py implementation is:
class StudentsAttendanceForm(forms.ModelForm):
class Meta:
model = StudentAttendance
fields = ('status', 'comment')
#exclude = [
#'siqned_by',
#]
On my views.py:
def student_attendance_manager(request):
"""
this function is responsible for querying the attendance parameters and present the student multiple attendance form
"""
if request.method == "POST":
# get the class name , the attendance date and present the attendance form
class_name = get_object_or_404(ClassRoom, pk=request.POST['class_name']) # class name
attendance_date = request.POST['date_field'] # date
# get the students in the class which is current active
student = StudentClass.objects.filter(main_class=request.POST['class_name'])
# modelform creation
AttendanceFormSet = modelformset_factory(StudentAttendance, form=StudentsAttendanceForm, extra=0)
# initiate the form and pass in the required parameters ie: classroom_id, attendance_date
list_formset = AttendanceFormSet(queryset=student)
# initialise the class_name and attendance date
#for form_inst in list_formset:
#form_inst.fields['classroom_id'].initial = class_name
#form_inst.fields['attendance_date'].initial = attendance_date
template = 'attendance/students_attendance_form.html'
context = {
'class_name':class_name,
'attendance_form': list_formset,
}
return JsonResponse({'html_form': render_to_string(template, context, request=request)})
template = 'attendance/students_attendance_manager.html'
class_date_selector_form = ClassroomDateQueryForm(request.GET or None)
context = {
'choice_form':class_date_selector_form
}
return render(request, template, context)
when the User Posts the form to be submited this is how i handle the form:
def student_attendance_register(request):
if request.method == "POST":
students = StudentClass.objects.filter(main_class=request.GET['class_id'])
StudentsAttendanceFormSet = modelformset_factory(StudentAttendance, form=StudentsAttendanceForm, extra=0)
list_formset = StudentsAttendanceFormSet(request.POST, queryset=students)
if list_formset.is_valid():
list_formset.save()
return HttpResponse('valid')
else:
return HttpResponse(list_formset.errors)
on my template i display the form in a table and this is my implementation:
form.html:
<form class="js-mark-attendance" method="post" action="{% url 'attendance:student_attendance_register' %}?class_id={{ class_name.id }}">
{% csrf_token %}
<table class="table-striped table table-bordered" id="Student_attendance_table">
<thead>
<tr>
<th>#</th>
<th>Admission Number</th>
<th>Name</th>
<th>Status</th>
<th>Comment</th>
</tr>
</thead>
<tbody>
{{ attendance_form.management_form }}
{% for form_inst in attendance_form %}
{% for hidden in form_inst.hidden_fields %}
{{ hidden }}
{% endfor %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ form_inst.instance.student_id.admission_number }}</td>
<td>{{ form_inst.instance.student_id }}</td>
<td>{{ form_inst.status }}</td>
<td> {{ form_inst.comment }}</td>
{{ form_inst.classroom_id.as_hidden }}
{{ form_inst.attendance_date.as_hidden }}
{{ form_inst.student_id.as_hidden }}
</tr>
{% endfor %}
</tbody>
</table>
<div class="row">
<div class="col-md-12 d-flex justify-content-center">
<input type="submit" value="Mark Attendance" class="btn btn-success">
</div>
</div>
</form>
and this is the error that django throws after the user has clicked submit button:
id
Select a valid choice. That choice is not one of the available choices.
so my question is ... how can i handle this post request form and or if their is an alternative way of doing my task:
any leads will be much upreciated
oops....I found my problem was the wrong usage of modelsfomset_factory...
I'm getting this error using Django 3 when trying to either 1) create a new object (CreateView), or 2) edit an existing one (UpdateView). Using CBVs and ModelForms.
I explored similar questions on Stackoverflow. Very often, it's about the template missing pk reference, but I triple-checked that and it doesn't seem to be the case.
Any idea?
Here are my codes:
URLs
path('availability/<int:pk>/list', views.AvailabilityListView.as_view(), name='availability_list'),
path('availability/new/', views.AvailabilityCreateView.as_view(), name='availability_new'),
path('availability/<int:pk>/edit/', views.AvailabilityUpdateView.as_view(), name='availability_edit'),
Views
class AvailabilityUpdateView(UpdateView):
template_name = 'crm/availability_form.html'
form_class = AvailabilityForm
model = Availability
class AvailabilityUpdateView(UpdateView):
template_name = 'crm/availability_form.html'
form_class = AvailabilityForm
model = Availability
class AvailabilityListView(TemplateView):
template_name = 'crm/availability_list.html'
def get_context_data(self, **kwargs):
user = self.request.user
kwargs['availabilities'] = Availability.objects.filter(staff__manager=Manager.objects.get(user=user)).filter(staff=self.kwargs['pk'])
return super().get_context_data(**kwargs)
Forms
class AvailabilityForm(forms.ModelForm):
class Meta():
model = Availability
exclude = []
Models
class Availability(models.Model):
staff = models.ForeignKey(Staff, on_delete=models.CASCADE)
start = models.DateTimeField()
end = models.DateTimeField()
class Meta:
verbose_name_plural = "Availabilities"
def get_absolute_url(self):
return reverse('staff_list')
Template
{% block body_block %}
<div class="container">
<div class="heading">
<h1>Availability</h1>
<a class = 'btn btn-primary' href="{% url 'availability_new' %}">+Create new availability</a>
</div>
<hr/>
{% if availabilities %}
<table class="table">
<tbody>
{% for availability in availabilities %}
<tr>
<td>{{availability.pk}}</td>
<td>{{availability.staff}}</td>
<td>{{availability.start}}</td>
<td>{{availability.end}}</td>
<td>
Edit
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class='empty'>Staff does not have any availability</p>
{% endif %}
</div>
{% endblock %}
My aim is to use Two models in One template. I have tried various ways around this and have had no success. Originally I had 2 views, 2 models and Two forms. After searching I found people using inlineformsets. So I dropped One of the Views and set-up the inlineformset.
This is currently where I am up to and seem to be hitting a wall.
The template renders to the browser and the 'object_list' part displays the database content as desired and the 'form' part renders the form and validates/saves the data correctly. The issue is with the 'formset'. No fields are rendered (I would expect to see a dropdown as the field is a foreignkey) and when the 'submit' button is pressed I get:
AttributeError at /settings/
'NoneType' object has no attribute 'save'
Any help in finding the error or pointers on alternative solutions would be greatly appreciated.
The Code:
models.py
from django.db import models
class RevisionSettings(models.Model):
global_revision_type = models.CharField(max_length = 5, unique=True, blank = True)
global_revision_description = models.CharField(max_length = 300, unique=True, blank = True)
class Meta:
ordering = ["global_revision_type"]
def __unicode__(self):
return u'%s %s' % (self.global_revision_type, self.global_revision_description)
class RevisionDefaultType(models.Model):
defaultrevisiontype = models.ForeignKey(RevisionSettings)
class Meta:
ordering = ["defaultrevisiontype"]
def __unicode__(self):
return unicode(self.defaultrevisiontype)
views.py
class RevisionSettingsView(CreateView):
template_name = 'settings/revisionsettings_view.html'
model = RevisionSettings
form_class = SettingsForm
success_url = reverse_lazy('globalsettings')
success_message = 'Successfully added your new revision type'
def get(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = SettingsFormSet(instance = RevisionSettings)
return self.render_to_response(
self.get_context_data(form=form,
formset=formset))
def post(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = SettingsFormSet(self.request.POST)
if 'rev_settings_form_1' in self.request.POST:
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
elif 'rev_settings_form_2' in self.request.POST:
if formset.is_valid():
return self.formset_valid(formset)
else:
return self.form_invalid(formset)
def form_valid(self, form):
self.object = form.save()
self.object.save()
return HttpResponseRedirect(self.get_success_url())
def formset_valid(self, formset):
self.object.save()
formset.instance = self.object
formset.save()
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self, form, formset):
return self.render_to_response(self.get_context_data(form=form,formset=formset))
def get_context_data(self, **kwargs):
kwargs['object_list'] = RevisionSettings.objects.order_by('global_revision_type')
return super(RevisionSettingsView, self).get_context_data(**kwargs)
forms.py
from django import forms
from django.forms.models import inlineformset_factory
from .models import RevisionSettings, RevisionDefaultType
class SettingsForm(forms.ModelForm):
class Meta:
model = RevisionSettings
class DefaultSettingsForm(forms.ModelForm):
class Meta:
model = RevisionDefaultType
SettingsFormSet = inlineformset_factory(RevisionSettings, RevisionDefaultType)
revisionsettings_view.html
(I have removed most of the HTML styling to keep the information to the point)
{% extends 'base_private.html' %}
{% block content %}
{% for object in object_list %}
<tr>
<td align="center">{{ object.global_revision_type }}</td>
<td align="center">{{ object.global_revision_description }}</td>
<td align="center"><span class="glyphicon glyphicon-remove-circle"></span></td>
</tr>
{% endfor %}
<form action = '{{ action }}' method = 'POST' class="form-horizontal" role="form">
{% csrf_token %}
<tr>
<td align="center">
{{ formset.management_form }}
{% for form in formset %}
{{ form.id }}
{{ form.defaultrevisiontype.label_tag }}
{{ form.defaultrevisiontype }}
{% endfor %}
</td>
</tr>
<span class="input-group-addon">
<input type = 'submit' name = 'rev_settings_form_2' value = 'Update Default Revision Type' class = 'btn btn-success'>
</span>
<td align="center">{{ form.global_revision_type }}{{ form.global_revision_type.errors }}</td>
<td align="center">{{ form.global_revision_description }}{{ form.global_revision_description.errors }}</td>
</tr>
<span class="input-group-addon">
<input type = 'submit' name = 'rev_settings_form_1' value = 'Add Revision Type' class = 'btn btn-success'>
</span>
</form>
{% endblock %}
Formsets are overkill for two forms. This is actually not too hard but poorly documented. You can make both forms the same form type, just give a prefix.
def parent_apply(request):
if request.method == 'POST':
parent_form = SignupForm(request.POST, prefix="parent")
student_form = StudentApplyForm(request.POST, prefix="student")
if parent_form.is_valid() and student_form.is_valid():
parent = parent_form.save()
student = student_form.save(parent)
else: messages.error(request, "Please correct the errors marked in red.")
else:
parent_form = SignupForm(prefix="parent")
student_form = StudentApplyForm(prefix="student")
return render_to_response('template_path/forms.html', { 'parent_form':parent_form, 'student_form':student_form }, context_instance=RequestContext(request))
The forms are just regular Django forms, no special settings required. You can change the order on which they validate and save one even if the other did not validate if you choose.
In your HTML Template, wrap both forms in the same tag and they will submit at the same time. If you want your forms to go to different view functions, specify two different elements.
Thanks for all the help. The pointers really helped me come to this solution. The main change was to 'def get' as shown below. I dropped the formset and passed the forms this way.
def get(self, request, *args, **kwargs):
form = self.settings_form_class
formset = self.default_form_class
return self.render_to_response(self.get_context_data(form = form, formset = formset))
I was unaware this was possible! Thanks again.