I have a formset, but I can't figure out how to prepopulate it with data from the database.
Truth be told, I have a model Library and a model Book that has a foreign key to Library. I want someone to be able to edit all of the book in one library (at a time).
html
{{ book_form.management_form }}
{% for form in book_form.forms %}
{{ form.as_p }}
{% endfor %}
views.py
class LibraryUpdateView(LoginRequiredMixin, UpdateView):
model = Library
form_class = LibraryChangeForm
template_name = 'libraries/library_update.html'
def get_context_data(self, *args, **kwargs):
context = super(LibraryUpdateView, self).get_context_data(*args, **kwargs)
if self.request.POST:
context['book_form'] = BookFormSet(self.request.POST)
else:
context['book_form'] = BookFormSet()
return context
forms
class LibraryChangeForm(forms.ModelForm):
class Meta:
exclude = ['updated']
BookFormSet = inlineformset_factory(Library, Book, exclude = () )
How do I prepopulate the html page to show the data in the formset. In my loop there are 3 loops for {{form.as_p}}. When I run print(BookFormsSet()) I see the following
<input type="hidden" name="perdateinfo_set-TOTAL_FORMS" value="3"...
So it looks like it is generating three blank forms.
When I run print(PerDateInfoFormSet(qs,qs2)) for qs = Library.objects.filter(library_id='slkdfj') and qs2=Book.objects.filter(library_id='slkdfj') It tells me too many values to unpack (expected 2)
Can I pass in a query set? I imagine that I will be passing in a qs.first() to get the library and then multiple (qs2) query sets to get all of the books in the library.
You need to pass instance argument to the formset:
def get_context_data(self, *args, **kwargs):
context = super(LibraryUpdateView, self).get_context_data(*args, **kwargs)
if self.request.POST:
context['book_form'] = BookFormSet(self.request.POST, instance=self.object)
else:
context['book_form'] = BookFormSet(instance=self.object)
return context
Related
Im trying to add a field called, interested_fields inside my personalInfo model which users can choose from and the choices themselves come from another models' objects with the help of ManyToMany relation between the two models. Here are my models.py codes(I simplified my personal model by removing some other fields like name, age, etc in order to make it more readable for you):
class Field(models.Model):
id = models.AutoField(primary_key=True)
slug = models.CharField(max_length=16, default='default')
title = CharField(max_length=32)
class PersonalInfo(models.Model):
id = models.AutoField(primary_key=True)
interested_fields = models.ManyToManyField(Field, blank=True)
then, I created a ModelForm like this:
class InterestedFieldsForm(forms.ModelForm):
interested_fields = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple, choices=Field.objects.all(), required=False)
class Meta:
model = PersonalInfo
fields = ['interested_fields']
and created a get and post functions inside my views like this:
class PersonalView(View):
template_name = 'reg/personal.html'
def get(self, request, *args, **kwargs):
context = {}
context['fields'] = Field.objects.all()
return render(request, self.template_name, context=context)
def post(self, request, *args, **kwargs):
user = request.user
if request.method == 'POST':
form = InterestedFieldsForm(request.POST)
if form.is_valid():
profile = form.save(commit=False)
profile.user = request.user
profile.save()
else:
form = InterestedFieldsForm()
return render(request, 'reg/done.html', context={'form': form})
and finally in template, inside the form I added this for loop:
{% for field in fields %}
<label class="containerq ant-col ant-col-md-6 ant-col-xs-8" >
<span>
<input type="checkbox" name="interested_fields" {% if field.slug in user.personalInfo.interested_fields %} checked="checked" {% endif %} value="{{field.title}}">
<span style="margin-left:7px" class="checkmark"></span>
</span>
<span>{{field.title}}</span>
</label>
{% endfor %}
when I submit the form it gives me this error:
cannot unpack non-iterable Field object
Im new to django so I really dont know what am I doing wrong. thank you for your answers
You should use a ModelMultipleChoiceField
interested_fields = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Field.objects.all(), required=False).
I got two models:
Project:
class Project(Model):
name = CharField(max_length=50)
members = ManyToManyField("accounts.User", through='ProjectUser')
organization = ForeignKey(Organization, related_name="projects", on_delete=CASCADE)
def __str__(self):
return self.name
and Task:
class Task(Model):
task = CharField(max_length=100)
project = ForeignKey(Project, on_delete=CASCADE)
class Meta:
db_table = 'task'
I got a UpdateView class:
class ProjectUpdateView(UpdateView):
form_class = ProjectUpdateForm
template_name = 'projects/project_edit.html'
success_url = reverse_lazy('projects:list')
How can I allow a user to add tasks (through an inline formset) on the same page as where they'd edit a Project instance?
E.g one consolidated form where the user can edit the Project name, and add / remove Task instances, all in one place
Form/Formset:
First, create a form and a formset for your Task model
class TaskForm(ModelForm):
class Meta:
model = Task
fields = ['task']
def __init__(self, *args, **kwargs):
super(TaskForm, self).__init__(*args, **kwargs)
class TaskBaseFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(TaskBaseFormSet, self).__init__(*args, **kwargs)
TaskFormset = inlineformset_factory(
Project, # parent_model
Task, # model
form=TaskForm,
formset=TaskBaseFormSet
)
Or maybe all that you need to do to create a TaskFormset if you dont need a TaskForm class is this
TaskFormset = inlineformset_factory(Project, Task, fields=('task',))
View:
I see you're using a UpdateView class for your view, so you can do this to get a TaskFormset in your context_data, so now you can use the TaskFormset in the template that you declared in the 'template_name' property of your UpdateView class
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.POST:
context['task_formset'] = forms.TaskFormset(self.request.POST)
else:
context['task_formset'] = forms.TaskFormset()
return context
# In the form_valid method of your UpdateView class you can validate the data
# and assign the Project instance to all the tasks that were create by the formsets
def form_valid(self, form):
task_formset = context['task_formset']
# you can validate formset data like this
if not task_formset.is_valid():
return self.form_invalid(form)
project = form.save()
# Here you assign the Project instance to the Tasks
task_formset.instance = project
task_formset.save()
return super().form_valid(form)
Template:
Now all that you need to do is to print the management_form and each form from the formset using a loop as you can see in the code below
<form method="post">
<!-- Your ProjectUpdateForm form here... -->
{{ task_formset.management_form }}
<table>
{% for form in task_formset %}
{{ form }}
{% endfor %}
</table>
</form>
Hope this can help! There are some links to the official Django documentation that you may find useful:
https://docs.djangoproject.com/en/3.1/topics/forms/formsets/#using-a-formset-in-views-and-templates
https://docs.djangoproject.com/en/3.1/topics/forms/modelforms/#inline-formsets
https://docs.djangoproject.com/en/3.1/ref/forms/models/#inlineformset-factory
I am trying to create a form to submit a blog post on an author detail page, so that the blog post will automatically use the current author as its "blog_author" foreign key. I'm aware that this approach isn't "secure" - it's a project site, and I'm trying to learn a new design pattern.
The Django docs recommended using 1 parent view and 2 subviews to handle get and post respectively (https://docs.djangoproject.com/en/3.0/topics/class-based-views/mixins/).
The page renders fine with the get, but the post gives me an error reading "Page not found (404) - no blog post found matching the query." The exception is raised by my parent view (blog.views.AuthorDetail), but there is no traceback.
Edit: Form should have been a ModelForm from the beginning
Here are my views:
class BlogAuthorDetailView(generic.DetailView):
model = BlogAuthor
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = BlogSubmitForm()
return context
class BlogSubmit(SingleObjectMixin, FormView):
template_name = 'blogauthor_detail.html'
form_class = BlogSubmitForm
model = BlogPost
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
#Should I be overriding form_valid() to use the line above? Not sure if I'm doing my data
#handling in the right place
return super().post(request, *args, **kwargs)
def form_valid(self, form):
blogpost = form.save(commit=False)
blogpost.blog_author = self.object
blogpost.save()
return redirect('blog_author-detail', pk=self.object.id)
class AuthorDetail(View):
def get(self, request, *args, **kwargs):
view = BlogAuthorDetailView.as_view()
return view(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
view = BlogSubmit.as_view()
return view(request, *args, **kwargs)
URLs:
urlpatterns = [
path('', views.index, name='index'),
path('blogs/', views.BlogPostListView.as_view(), name='blogs'),
path('blog/<int:pk>', views.BlogPostDetailView.as_view(), name='blogpost-detail'),
path('bloggers/', views.BlogAuthorListView.as_view(), name='bloggers'),
path('blogger/<int:pk>', views.AuthorDetail.as_view(), name='blog_author-detail'),
path('blog/<int:pk>/create', views.BlogCommentCreate.as_view(), name='comment_create')
]
the template:
{% extends "base_generic.html" %}
{% block content %}
<h1>Title: {{ blogauthor.title }}</h1>
<p><strong>Author:</strong> {{ blogauthor }}</p>
<p><strong>Biography:</strong> {{ blogauthor.biography }}</p>
<p><strong>User:</strong> {{ blogauthor.user }}</p>
<p><strong>Posts:</strong>
{% for blog in blogauthor.blogpost_set.all %}
<p> {{ blog.title }} </p>
{% endfor %} </p>
<form action="" method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" value="Submit">
</form>
<div style="margin-left:20px;margin-top:20px">
<h4>Comments: Coming Soon!</h4>
{% endblock %}
Model:
class BlogPost(models.Model):
date_created = models.DateField(blank=False, default = date.today)
blog_author = models.ForeignKey('BlogAuthor', on_delete = models.SET_NULL, null=True)
title = models.TextField(max_length=70)
content = models.TextField(max_length=400, null=False)
class Meta:
ordering = ['date_created']
def get_absolute_url(self):
"""Returns the url to access a particular blog post instance."""
return reverse('blogpost-detail', args=[str(self.id)])
def __str__(self):
return self.title
And the forms.py:
class BlogSubmitForm(forms.Form):
title = forms.CharField()
content = forms.CharField(widget=forms.Textarea(attrs={'cols': 40, 'rows': 8}))
date_created = forms.DateField()
At this point, I suspect that the problem is related to my redirect() call in the form_valid override.
The things I have tried include:
Changing the form’s action from blank to the same URL as in my URL paths (possible I did this wrong)
Changing the code in form_valid() to read form.instance.blog_author = self.object (same exact error message, so I don’t think it’s this)
Fiddling with the form_valid()’s redirect call, including: using self.object instead or a URL, using a hardcoded url, getting rid of the second argument, and changing the 2nd arg to pk=, slug=.
Adding a get_success_url override (don’t really know why this would work)
edit: one of the excepted post calls that showed up in my local server went to blog/blogger/4, which is the url I want. Not sure what the issue is.
This is confusing on how you are using the template. Anyway, I think the simplest solution here is to get the BlogAuthor data from request.user and that is most logical, otherwise, anyone can post anything from another user as long as they can predict their primary key(which is a security hole). Here is how you can try:
from django.contrib.auth.mixins import LoginRequiredMixin
class BlogSubmit(LoginRequiredMixin, CreateView):
template_name = 'blogauthor_detail.html'
form_class = BlogSubmitForm
model = BlogPost
def get_success_url(self):
return reverse('blog_author-detail', pk=self.object.id)
def form_valid(self, form):
form.blog_author = self.request.user.blogauthor # assuming BlogAuthor has OneToOne relation with User
return super(BlogSubmit, self).form_valid(form)
Update
Purpose of FormView is to collect data from Forms, where CreateView is to store and create a new instance. Anyway, you need to change your code like this to make it work:
class BlogSubmit(LoginRequiredMixin, SingleObjectMixin, FormView):
template_name = 'blogauthor_detail.html'
form_class = BlogSubmitForm
model = BlogAuthor
def get_success_url(self):
return reverse('blog_author-detail', pk=self.object.id)
def form_valid(self, form):
self.object = self.get_object()
form.blog_author = self.object
form.save()
return super(BlogSubmit, self).form_valid(form)
Also update the form:
class BlogSubmitForm(forms.ModelForm):
class Meta:
model = BlogPost
fields = ['title', 'date_created', 'content']
FYI, to make SingleObjectMixin work, you need to change the model from BlogPost to BlogAuthor
I have a website where user have 2 model for their profile, user_detail and user_location. I tried to serve 2 model form on one page with one submit. The problem is when the data from those model form does not save in to the database.
I confirmed that self.request.POST in the post method returns the correct data.
I tried :
Django ModelForm not saving data to database - Does not work
Django ModelForm not saving data - Does not work
The following code if for admins.
Here is my view :
class UpdateProfile(LoginRequiredMixin, UpdateView):
template_name = 'account/user_profile.html'
fields = '__all__'
model = models.UserProfile
user_detail_form_class = forms.UserDetailForm
user_location_form_class = forms.UserLocationForm
def get_context_data(self, **kwargs):
user_profile = get_object_or_404(models.UserProfile, pk=self.kwargs.get(self.pk_url_kwarg))
context = super(UpdateProfile, self).get_context_data(**kwargs)
if 'user_detail_form' not in context:
context['user_detail_form'] = self.user_detail_form_class(instance=user_profile.user_detail)
if 'user_location_form' not in context:
context['user_location_form'] = self.user_location_form_class(instance=user_profile.user_location)
return context
def get(self, request, *args, **kwargs):
super(UpdateProfile, self).get(request, *args, **kwargs)
return self.render_to_response(self.get_context_data())
def post(self, request, *args, **kwargs):
user_detail_form = self.user_detail_form_class(request.POST)
user_location_form = self.user_location_form_class(request.POST)
if user_detail_form.is_valid() and user_location_form.is_valid():
user_detail_form.save()
user_location_form.save()
return redirect(self.get_success_url())
else:
return self.render_to_response(self.get_context_data())
def get_success_url(self):
return reverse('account:admin_client_list')
def dispatch(self, request, *args, **kwargs):
if not request.user.groups.filter(name__in=['Admin']).exists():
return errors.render_403(request)
return super(UpdateProfile, self).dispatch(request, *args, **kwargs)
Here is my template :
{% extends 'base.html' %}
{% block content %}
<form method='POST' action="">{% csrf_token %}
{{ user_detail_form }}
{{ user_location_form }}
<input type="submit" value="Submit">
</form>
{% endblock %}
Here is the form :
class UserDetailForm(forms.ModelForm):
class Meta:
model = models.UserDetail
fields = '__all__'
class UserLocationForm(forms.ModelForm):
class Meta:
model = models.UserLocation
fields = '__all__'
You need to pass the instance parameter when you are creating the ModelForm in the post method. Sample code:
user_profile = get_object_or_404(models.UserProfile, pk=self.kwargs.get(self.pk_url_kwarg))
user_detail_form = self.user_detail_form_class(request.POST, instance=user_profile.user_detail)
user_location_form = self.user_location_form_class(request.POST, instance=user_profile.user_location)
So I have a model:
class MyThing(models.Model):
my_field = OneToOneField(SomeOtherModel)
... other fields
A form:
class MyThingForm(forms.ModelForm):
class Meta:
model = MyThing
A view:
class MyThingView(views.TemplateView):
template_name = 'thing.html'
def get(self, request, *args, **kwargs):
form = MyThingForm()
return render(self.template_name, {'form': form})
def post(self, request, *args, **kwargs):
... retrieve some_instance
request.POST['my_field'] = some_instance
form = MyThingForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(...somewhere else)
return render(self.template_name, {'form': form})
my thing.html template:
{% for field in form %}
{{ field }}
{% endfor %}
What my problem is:
I need to hide the my_field field when rendering the template (but from backend), that implying that when I do the for on form in the template, it shouldn't have the my_field field in the fields set already
This is a creation form, that means that I don't have an existing instance
In the backend the my_field is required, so when doing POST I retrieve the instance for my_field from somewhere, doesn't matter where, and add it to the data for the form in sight. After this the form should be valid and can be saved to database
So the basic question is : How do I make a required field, hidden but saveable?
It very usual use case look at this doc.
In summary you can exclude the field from form and save it after retrieving it from somewhere.
Update code as
class MyThingForm(forms.ModelForm):
class Meta:
model = MyThing
exclude = ['my_field', ]
class MyThingView(views.TemplateView):
...
def post(self, request, *args, **kwargs):
form = MyThingForm(request.POST)
#retrieved_my_field = retrieve the field
if form.is_valid():
inst = form.save(commit=False)
inst.my_field = retrieved_my_field
inst.save()
return HttpResponseRedirect(...somewhere else)
return render(self.template_name, {'form': form})