I know what above error means. Seems I can't handle form when it is posted. I can do it in function based views, but in class based views I am tad lost.
I am creating a comment app and here is forms.py content in comment app:
class CommentForm(forms.Form):
content_type = forms.CharField(widget=forms.HiddenInput)
object_id = forms.CharField(widget=forms.HiddenInput)
body = forms.CharField(widget=forms.Textarea)
then in DetailView of the blog app, I handled it this way:
class BlogDetail(DetailView):
model = Blog
template_name = 'blogs/blog_detail.html'
context_object_name = 'blog'
def get_object(self):
blog_slug = self.kwargs.get('blog_slug')
return get_object_or_404(Blog, slug=blog_slug)
def get_context_data(self, *args, **kwargs):
obj = self.get_object()
context = super().get_context_data(**kwargs)
context['comments'] = Comment.objects.filter_by_instance(obj)
""" comment form """
initial_data = {
'content_type': obj.get_content_type,
'object_id': obj.id
}
if self.request.method == 'POST':
form = CommentForm(self.request.POST, initial=initial_data)
if form.is_valid():
c_type = form.cleaned_data.get('content_type')
content_type = ContentType.objects.get(model=c_type)
object_id = form.cleaned_data.get('object_id')
body = form.cleaned_data.get('body')
new_comment, created = Comment.objects.get_or_create(
user=self.request.user,
content_type=content_type,
object_id=object_id,
body=body
)
else:
form = CommentForm(initial=initial_data)
context['comment_form'] = form
return context
albeit I passsed form = CommentForm(self.request.POST, initial=initial_data) but there sounds something is going wrong, Can anyone help? Thank you
edited:
class BlogDetail(DetailView, FormView):
model = Blog
template_name = 'blogs/blog_detail.html'
context_object_name = 'blog'
form_class = CommentForm
def get_object(self):
blog_slug = self.kwargs.get('blog_slug')
return get_object_or_404(Blog, slug=blog_slug)
def get_initial(self):
obj = self.get_object()
return {
'content_type': obj.get_content_type,
'object_id': obj.id
}
def form_valid(self, form):
c_type = form.cleaned_data.get('content_type')
content_type = ContentType.objects.get(model=c_type)
object_id = form.cleaned_data.get('object_id')
body = form.cleaned_data.get('body')
Comment.objects.create(
user=self.request.user,
content_type=content_type,
object_id=object_id,
body=body
)
edit 2:
Can anyone spot the error with this approach:
class BlogDetail(DetailView):
model = Blog
template_name = 'blogs/blog_detail.html'
context_object_name = 'blog'
form_class = CommentForm
def get_object(self):
blog_slug = self.kwargs.get('blog_slug')
return get_object_or_404(Blog, slug=blog_slug)
def get(self, request, *args, **kwargs):
obj = self.get_object()
initial_data = {
'content_type': obj.get_content_type,
'object_id': obj.id
}
print("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa", obj.get_content_type)
form = self.form_class(initial=initial_data)
return render(request, self.template_name, {'form': form})
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
c_type = form.cleaned_data.get('content_type')
content_type_2 = ContentType.objects.get(model=c_type)
object_id = form.cleaned_data.get('object_id')
body = form.cleaned_data.get('body')
Comment.objects.create(
user=request.user,
content_type=content_type_2,
object_id=object_id,
body=body,
)
return render(request, self.template_name, {'form': form})
Posts are handled by the post method of the class-based view:
class BlogDetail(DetailView):
# ...
def post(self, request, *args, **kwargs):
# all your form processing
Django ships with several views, that already provide various hooks into the form handling process, e.g. FormView, that you could leverage instead:
class BlogDetail(DetailView, FormView):
form_class = CommentForm
def form_valid(self, form):
c_type = form.cleaned_data.get('content_type')
# ...
def get_initial(self):
obj = self.get_object()
return {
'content_type': obj.get_content_type,
'object_id': obj.id
}
# ....
By default, the form is passed as "form" into the context.
To allow post requests to your view, write a function def post(self, request, *args, **kwargs) which will receive the post request. If you want to handle this as you would handle get, redirect it to the get function
def post(self, request, *args, **kwargs):
return self.get(request, *args, **kwargs)
You don't need the DetailView. You can simply use CreateView. I think you have everything overridden correctly to be able to ditch DetailView, except maybe get_form_kwargs().
However...
I usually approach this differently, cause it's confusing and very hacky. Instead, you add the form via get_context_data() to the DetailView and then in the template post to /blog/{id}/comment/create, where you have the CreateView. That makes things a lot simpler.
Related
I want to create a new post using PostCreateView and go to the details page of the new post in the next step, but I get this error:
(The view post.views.view didn't return an HttpResponse object. It returned None instead.)
views
class PostDetailView(View):
"""see detail post"""
def get(self, request, post_id, post_slug):
post = Post.objects.get(pk=post_id, slug=post_slug)
return render(request, "post/detail.html", {"post": post})
class PostCreateView(LoginRequiredMixin, View):
form_class = PostCreateUpdateForm
def get(self, request, *args, **kwargs):
form = self.form_class
return render(request, "post/create.html", {"form": form})
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
new_post = form.save(commit=False)
new_post.slug = slugify(form.cleaned_data["body"][:20])
new_post.user = request.user
new_post.save()
messages.success(request, "you created a new post", "success")
return redirect("post:post-detail", new_post.id, new_post.slug)
models
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
body = models.TextField()
slug = models.SlugField()
img = models.ImageField(upload_to="%Y/%m/%d/")
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
urls
app_name = 'post'
urlpatterns = [
path('', views.BlogView.as_view(), name="home"),
path('detail/<int:post_id>/<slug:post_slug>/', views.PostDetailView.as_view(), name="post-detail"),
path('delete/<int:post_id>/', views.PostDeleteView.as_view(), name="post-delete"),
path('update/<int:post_id>/', views.PostUpdateView.as_view(), name="post-update"),
path('create/', views.PostCreateView.as_view(), name="post-create"),
]
In case the form is not valid, you should rerender the template with the form, so:
class PostCreateView(LoginRequiredMixin, View):
form_class = PostCreateUpdateForm
def get(self, request, *args, **kwargs):
form = self.form_class
return render(request, "post/create.html", {"form": form})
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
new_post = form.save(commit=False)
new_post.slug = slugify(form.cleaned_data['body'][:20])
new_post.user = request.user
new_post.save()
messages.success(request, 'you created a new post', 'success')
return redirect('post:post-detail', new_post.id, new_post.slug)
return render(request, 'post/create.html', {'form': form})
But you are implementing a lot of boilerplate code here. What you here do is implementing a CreateView [Django-doc]:
from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic import CreateView
class PostCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
form_class = PostCreateUpdateForm
template_name = 'post/create.html'
success_message = 'you created a new post'
def form_valid(self, form):
form.instance.slug = slugify(form.cleaned_data['body'][:20])
form.instance.user = request.user
return super().form_valid()
def get_success_url(self):
return reverse('post:post-detail', args=(new_post.id, new_post.slug))
Your "post" method in PostCreateView only returns a response if the form is valid. If it isn't valid, it will return None, causing an error.
Modify that method so it looks like this:
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
new_post = form.save(commit=False)
new_post.slug = slugify(form.cleaned_data["body"][:20])
new_post.user = request.user
new_post.save()
messages.success(request, "you created a new post", "success")
return redirect("post:post-detail", new_post.id, new_post.slug)
return render(request, "post/create.html", {"form": form})
I have a update view in django project. I need to override the post method because I am using multiple modelform. I have already override createview. And it is working fine.
views.py:
class EmployeeUpdateView(LoginRequiredMixin, UpdateView):
"""
Update a created a employee
"""
login_url = '/authentication/login/'
template_name = 'employee/employee_update_form.html'
form_class = EmployeeAddModelForm
work_form_class = WorkExperienceForm
education_form_class = EducationForm
queryset = Employee.objects.all()
#success_url = reverse_lazy('employee:employee-list')
def get(self, request, *args, **kwargs):
id_ = self.kwargs.get("id")
employee_id = Employee.objects.get(id=id_)
work_info = WorkExperience.objects.get(employee=employee_id)
education_info = Education.objects.get(employee=employee_id)
return render(request, self.template_name, {
'form': self.form_class(instance=employee_id),
'work_form': self.work_form_class(instance=work_info),
'education_form': self.education_form_class(instance=education_info)
}
)
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
work_form = self.work_form_class(request.POST, prefix='work_form')
education_form = self.education_form_class(request.POST, prefix='education_form')
# Check form validation
if form.is_valid() and work_form.is_valid() and education_form.is_valid():
instance = form.save()
work = work_form.save(commit=False)
education = education_form.save(commit=False)
work.employee = instance
education.employee = instance
work.update()
education.update()
return redirect('employee:employee-list')
return render(request, self.template_name, {
'form': form,
'work_form': work_form,
'education_form': education_form
}
)
When I press update button of my form, error is giving showwing "This field already exist". It means when i update the form, it is posting data as a new form not as a update form.
I think my post method is not working. Where is the error in my post method?
You can write your own view like this instead of sub classing the generic view. Since you are working with more than one model here so it will be easy for you to write your own view.
class EmployeeUpdateView(LoginRequiredMixin, View):
.......
def post(self, request, *args, **kwargs):
id_ = self.kwargs.get("id")
employee_id = Employee.objects.get(id=id_)
work_info = WorkExperience.objects.get(employee=employee_id)
education_info = Education.objects.get(employee=employee_id)
form = self.form_class(request.POST, instance=employee_id)
work_form = self.work_form_class(request.POST, prefix='work_form', instance=work_info)
education_form = self.education_form_class(request.POST, prefix='education_form',instance=education_info)
# Check form validation
if form.is_valid() and work_form.is_valid() and education_form.is_valid():
instance = form.save()
work = work_form.save(commit=False)
education = education_form.save(commit=False)
work.employee = instance
education.employee = instance
work.save()
education.save()
return redirect('employee:employee-list')
return render(request, self.template_name, {
'form': form,
'work_form': work_form,
'education_form': education_form
}
)
i want to have default value in my form which should be current user's choice. For example, user chooses to give 5 for a movie and he should see 5 in form until he changes it.
views.py
class MovieDetailView(FormMixin, DetailView):
model = Movie
template_name = 'main/detail_movie.html'
context_object_name = 'movie'
form_class = RateForm
def get_context_data(self, **kwargs):
context = super(MovieDetailView, self).get_context_data(**kwargs)
context['form'] = RateForm(initial={'movie': self.object})
context['my_rate'] = Rate.objects.filter(
sender=self.request.user,
movie=self.get_object()).first()
return context
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
if request.method == 'POST':
if form.is_valid():
new_rate, _ = Rate.objects.update_or_create(
sender=self.request.user,
movie=self.object,
defaults={'choice': form.cleaned_data['choice']}
)
return JsonResponse({'rate': model_to_dict(new_rate)}, status=200)
else:
return self.form_invalid(form)
def get_success_url(self):
return reverse('detail_movie', kwargs={'slug': self.object.slug})
I was trying get_initial method but it still gives me '---' in form as default.
def get_initial(self):
return {'choice': Rate.objects.filter(movie=self.object, sender=self.request.user).first().choice }
Even something like this doesn't work
def get_initial(self):
return {'choice': 1 }
If you want to use FormMixin and get_initial method you need to use get_form method in get_context_data
class MovieDetailView(FormMixin, DetailView):
model = Movie
template_name = 'main/detail_movie.html'
context_object_name = 'movie'
form_class = RateForm
def get_initial(self):
initial = super(MovieDetailView, self).get_initial()
initial['movie'] = self.object # selected movie
rate = Rate.objects.filter( # attempt to get rate
movie=self.object,
sender=self.request.user
).first()
if rate is not None: # set rate choice only if it has been done before
initial['choice'] = rate.choice
return initial
def get_context_data(self, **kwargs):
context = super(MovieDetailView, self).get_context_data(**kwargs)
context['form'] = self.get_form() # use from mixin instead manual init
return context
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
if request.method == 'POST':
if form.is_valid():
new_rate, _ = Rate.objects.update_or_create(
sender=self.request.user,
movie=self.object,
defaults={'choice': form.cleaned_data['choice']}
)
return JsonResponse({'rate': model_to_dict(new_rate)}, status=200)
else:
return self.form_invalid(form)
def get_success_url(self):
return reverse('detail_movie', kwargs={'slug': self.object.slug})
Ok, i just had to input
'choice': Rate.objects.filter(movie=self.object, sender=self.request.user).first().choice
to my context['form'] where i already had initial:
def get_context_data(self, **kwargs):
context = super(MovieDetailView, self).get_context_data(**kwargs)
context['form'] = RateForm(
initial={
'movie': self.object,
'choice': Rate.objects.filter(movie=self.object, sender=self.request.user).first().choice
})
context['my_rate'] = Rate.objects.filter(
sender=self.request.user,
movie=self.get_object()).first()
return context
Put this in your form.
form = RateForm(initial={'choice': 1})
im only student so please bear with me. I already posted another topic about this but in function view. now i want to how do i convert this try exception to a class view and also add that comment form.
here's my views.py def
def BookDetail(request, id):
most_recent = Book.objects.order_by('-timestamp')[:3]
book= get_object_or_404(Book, id=id)
form = CommentForm(request.POST or None)
if request.method == "POST":
if form.is_valid():
form.instance.user = request.user
form.instance.post = book
form.save()
return redirect(reverse("book-detail", kwargs={
'id': book.pk
}))
if request.user.is_anonymous:
user_membership = None
else:
try:
user_membership = Customer.objects.get(user=request.user)
except Customer.DoesNotExist:
user_membership = None
context = {
'user_membership': user_membership,
'form': form,
'book': book,
'most_recent': most_recent,
}
return render(request, 'catalog/book_detail.html', context)
here is my new class view
class BookDetailView(NeverCacheMixin, generic.DetailView):
model = Book
UPDATE POST
here's my models.py..
class Book(models.Model):
slug = models.SlugField(unique=True, help_text="Enter BIC Code", null=True)
title = models.CharField(max_length=200) #more fields after this
timestamp = models.DateTimeField(default=timezone.now)
activeReference = models.ManyToManyField(Membership)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('book-detail', kwargs={'slug': self.slug})
#property
def get_comments(self):
return self.comments.all().order_by('-timestamp')
#property
def pages(self):
return self.page_set.all()
class Page(models.Model):
slug = models.SlugField(max_length=50)
book = models.ForeignKey(Book, on_delete=models.SET_NULL, null=True)
preview = models.FileField(upload_to='book_content', validators=[pdf_file_extension], help_text="PDF File Only")
def __str__(self):
return self.slug
def get_absolute_url(self):
return reverse('page-detail',
kwargs={
'book_slug': self.book.slug,
'page_slug': self.slug
})
here's my URL pattern
urlpatterns = [
path('', views.index, name='index'),
path('books/', views.BookListView.as_view(), name='book-list'),
path('book-detail/<slug>', views.BookDetailView.as_view(), name='book-detail'),
path('book-detail/<book_slug>/<page_slug>', views.PageDetailView.as_view(), name='page-detail'),
path('search/', views.Search, name='search'),
]
and my book_detail.html
{% for content in book.pages %}
Read
{% endfor %}
{% else %}
CBV are good when you need inheritance or need to deal with different HTTP methods to have the same route. If that is not the case, a FBV is a better choice. That being said, for what you are trying to do, you should just a FormView that will handle everything about a post and forms.
Something like:
class BookViewSet(FormView):
form = CommentForm
template_name = 'catalog/book_detail.html'
def form_valid(self, form):
self.send_mail(form.cleaned_data)
return super(BookViewSet, self).form_valid(form)
If you are handling forms, you should be using CreateView or FormView not DetailView. Here is an implementation example, as you can see is a little complex for someone new to Django:
from django.views.generic import CreateView
class BookDetailView(NeverCacheMixin, CreateView):
form_class = CommentForm
template_name = 'catalog/book_detail.html'
def dispatch(self, request, *args, **kwargs):
book_slug = self.kwargs.get('slug') # url variables are stored in self.kwargs
self.book = get_object_or_404(Book, slug=book_slug) # attach book to your view
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
# context data passed to template
kwargs['book'] = self.book
kwargs['most_recent'] = Book.objects.order_by('-timestamp')[:3]
if request.user.is_anonymous:
user_membership = None
else:
try:
user_membership = Customer.objects.get(user=self.request.user)
except Customer.DoesNotExist:
user_membership = None
kwargs['user_membership'] = user_membership
return super().get_context_data(**kwargs)
def form_valid(self, form):
instance = form.save(commit=False)
instance.user = self.request.user
instance.post = self.book
instance.save()
return redirect(self.book.get_absolute_url())
On my page, i need to display the post detail and a comment form for viewer to post comment. I created 2 generic views:
# views.py
class PostDetailView (DetailView):
model = Post
context_object_name = 'post'
template_name = 'post.html'
def get_context_data(self, **kwargs):
context = super(PostDetailView, self).get_context_data(**kwargs)
context['comment_form'] = CommentForm()
return context
class AddCommentView(FormView):
template_name = 'post.html'
form_class = CommentForm
success_url = '/'
def form_valid(self, form):
form.save()
return super(AddCommentView, self).form_valid(form)
def form_invalid(self, form):
return self.render_to_response(self.get_context_data(form=form))
detail = PostDetailView.as_view()
add_comment = AddCommentView.as_view()
# urls.py
....
url(r'^(?P<pk>\d+)/$', view='detail'),
url(r'^(?P<post_id>\d+)/add_comment/$', view='add_comment'),
....
Error would occur in the AddCommentView,since I haven't specified the post's id for the comment. How can I access the post_id in the AddCommentView?
self.kwargs['post_id'] or self.args[0] contains that value
Docs