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
Related
I'm building a generic blog and trying to enable users to make comments directly on the article page. I am trying to implement this by combining DetailView with CreateView.
The docs present 3 different solutions to this issue:
FormMixin + DetailView: this is the answer that Django docs advise against, but that is advised by most answers on SO that I could find
DetailView only + write the post method: "a better solution" according to Django docs
DetailView + FormView: "an alternative better solution", and the one I'm trying to implement.
The "alternative better" solution consists in making a DetailView for articles and a FormView for comments, but the docs state that "This approach can also be used with any other generic class-based views", which means that DetailView + CreateView should be possible.
I've gone through a number of SO items that reference this solution, but I am unable to implement any of them.
This SO question suggests mixing DetailView and CreateView. However, the explanation in that answer is incomplete.
Another SO question, among advice to use FormMixins, has this answer that is close, but different.
Other questions (1, 2, etc.) only address the FormMixin and DetailView + post methods.
Here's my implementation for now:
models.py:
class Article(models.Model):
slug = models.SlugField()
# title, body, author
def get_absolute_url(self):
return reverse("article_detail", kwargs={"slug": self.slug})
class Comment(models.Model):
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name="comments", to_field="slug")
body = models.TextField()
# ...
def get_absolute_url(self):
return reverse("article_detail", kwargs={"slug": self.article.slug})
views.py:
class ArticleDetailView(DetailView):
model = Article
template_name = "article_detail.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["form"] = CommentCreateView()
return context
class CommentCreateView(CreateView):
"""create comment"""
model = Comment
fields = ["body"]
template_name = "article_detail.html"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.article = Article.objects.filter(
slug=self.kwargs.get("slug")
).first()
self.object.author = self.request.user
self.object.save()
return super().form_valid(form)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return super().post(request, *args, **kwargs)
def get_success_url(self):
return reverse("article_detail", kwargs={"slug": self.object.article.slug})
class ArticleCommentView(View):
def get(self, request, *args, **kwargs):
view = ArticleDetailView.as_view()
return view(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
view = CommentCreateView.as_view()
return view(request, *args, **kwargs)
urls.py:
urlpatterns = [
# ...
path("article/<slug:slug>", ArticleCommentView.as_view(), name="article_detail"),
]
article_detail.html:
{% extends 'base.html' %}
{% block content %}
{{ article.title }}
{{ article.author }}
{{ article.body }}
{% include "comment_create.html" %}
<!-- list existing comments with {#% for comment in article.comments.all %#}, etc.-->
{% endblock %}
comment_create.html:
<form method="post" action="{% url 'article_detail' slug=article.slug %}">
{% csrf_token %}
<textarea name="{{ form.body.name }}">{{ form.body.value|default_if_none:'' }}</textarea>
</div>
<button type="submit">
Post Comment
</button>
</div>
</form>
I'm currently getting a
NoReverseMatch at /article/createview-cd7a7040-c4ca-4289-8f53-6676f27c3aa9
Reverse for 'editor_update' with keyword arguments '{'slug': ''}' not found. 1 pattern(s) tried: ['editor/(?P<slug>[-a-zA-Z0-9_]+)$']`
where editor_update is a path at editor/<slug> to an UpdateView. Can't understand how that is related to anything.
The article.slug in comment_create.html returns the correct slug, but form.body.name returns an empty string, if that helps.
Edited based on Abdul Aziz Barkat's comment below.
I am trying to allow users to save details of a workout for a specific exercise through submitting a form. My ExerciseDetailView displays the form how I'd like it to:
class ExerciseDetailView(DetailView):
model = Exercise
template_name = 'workouts/types.html'
def get_context_data(self, **kwargs):
context = super(ExerciseDetailView, self).get_context_data(**kwargs)
context['form'] = WorkoutModelForm
return context
But my problem is with saving the inputted data in the database. I have tried making both a FormView and a CreateView but am clearly missing something:
class ExerciseFormView(FormView):
form_class = WorkoutModelForm
success_url = 'workouts:exercise_detail'
def form_valid(self, form):
form.save()
return super(ExerciseFormView, self).form_valid(form)
Here is my referenced WorkoutModelForm:
class WorkoutModelForm(forms.ModelForm):
class Meta:
model = Workout
fields = ['weight', 'reps']
My template:
<form action="{% url 'workouts:workout' exercise.id %}" method="post">
{% csrf_token %}
{{ form }}
<button type="submit">Save</button>
</form>
Urls:
path('exercise/<int:pk>/detail/', ExerciseDetailView.as_view(), name='exercise_detail'),
path('exercise/<int:pk>/detail/', ExerciseFormView.as_view(), name='workout'),
And for context here is my Workout model which contains a get_absolute_url method:
class Workout(models.Model):
weight = models.FloatField(default=0)
reps = models.PositiveIntegerField(default=0)
created = models.DateField(auto_now_add=True)
updated = models.DateField(auto_now=True)
exercise = models.ForeignKey(Exercise, on_delete=models.CASCADE, default=None)
def get_absolute_url(self):
return reverse('exercise_detail', args=[str(self.pk)])
I am not receiving any errors, but when I submit the form my url remains the same, as I hoped, however the page just appears blank and the objects are not recorded. Can anybody please help me see what the problem is?
The problem is not your view, the Django logic will never trigger this view, the URLs are perfectly overlapping, so that means that for a URL, it will always trigger the first view (here the ExerciseDetailView), you should make the paths non-overlapping, for example with:
path('exercise/<int:pk>/detail/', ExerciseDetailView.as_view(), name='exercise_detail'),
path('exercise/<int:pk>/workout/', ExerciseFormView.as_view(), name='workout'),
Triggering the logic will however not be sufficient, since it will not link the Workout to the necessary exercise, you can alter the logic to:
from django.urls import reverse
class ExerciseFormView(CreateView):
form_class = WorkoutModelForm
def form_valid(self, form):
form.instance.exercise_id = self.kwargs['pk']
return super().form_valid(form)
def get_success_url(self):
return reverse('workouts:exercise_detail', kwargs={'pk': self.kwargs['pk']})
Need use CreateView
from django.views.generic.edit import CreateView
class ExerciseFormView(CreateView):
form_class = WorkoutModelForm
...
I created a ModelForm which renders correctly and displayed but whenever i try to submit the form I get a 405 error and the page doesnt redirect to success page.
Ive gone through the django 2.2 documentation trying many different things but nothing seems to work
My code is configured as such the template:
<form enctype="multipart/form-data" action="{% url 'order_thanks' %}"
method="post" novalidate>
{% csrf_token %}
{{ form|crispy }}
<input name="Submit" type="submit" class="btn btn-success" value="Git my
food!"></input>
The model:
from django.db import models
from django.forms import ModelForm, Textarea, Select,
CheckboxSelectMultiple, CheckboxSelectMultiple
from django import forms
BURGER_CHOICES = (("AFBB", "Aurion's Famous Beef Burger"), ("AIPB",
"Aurion's Infamous Pork Burger"), ("AULB", "Aurion's Undiscovered Lamb
Burger"), ("POG", "Pureed Otter Giblets"))
BUN_CHOICES = (("WHITE","White Bread"), ("RYE","Rye"), ("TPOODLE",
"Teacup Poodles"), ("AFOSSIL","Ammonite Fossils"))
TOPPING_CHOICES = (("CHEESE", "Cheese"), ("LETTUCE", "Lettuce"),
("TOMATOE", "Tomatoe"), ("ONION", "Onion"), ("WSHAVE", "Wood Shavings"))
SAUCES_CHOICES = (("OZTS", "Our Zesty Barbaque Sauce"), ("SEZBS",
"Someone Elses Zesty Barbaque Sauce"), ("VS", "Varmint Squeezings"))
EXTRAS_CHOICES = (("P", "Pinapple"), ("SG", "Soylent Green"), ("SB",
"Soylent Blue"), ("MWS", "More Wood Shavings"))
class Order(models.Model):
burger = models.CharField(max_length=50,choices=BURGER_CHOICES )
bun = models.CharField(max_length=50, choices=BUN_CHOICES)
toppings = models.CharField(max_length=60, choices=TOPPING_CHOICES)
sauces = models.CharField(max_length=60, choices=SAUCES_CHOICES)
extras = models.CharField(max_length=60, choices=EXTRAS_CHOICES)
# def get_absolute_url(self):
# return reverse('burger', kwargs={'pk': self.pk})
def __str__(self):
return self.burger
def get_absolute_url(self):
return reverse('order-thanks', kwargs={'pk': self.pk})
class OrderForm(ModelForm):
def __init__(self, *args, **kwargs):
super(OrderForm, self).__init__(*args, **kwargs)
self.fields['toppings'].widget = forms.CheckboxSelectMultiple()
for field_name in self.fields:
field = self.fields.get(field_name)
if field and isinstance(field , forms.TypedChoiceField):
field.choices = field.choices[1:]
self.fields['extras'].widget = forms.CheckboxSelectMultiple()
class Meta:
model = Order
fields = ['burger', 'bun', 'toppings', 'sauces', 'extras']
the view:
class OrderView(CreateView, FormView):
template_name = 'order_form.html'
form_class = OrderForm
success_url = 'order/thanks/'
def form_valid(self, form):
form.instance.created_by = self.request.user
return super().form_valid(form)
def get(self, request):
return Response(code=200)
class OrderThanksView(TemplateView):
template_name = 'order_thanks.html'
the urls:
urlpatterns = [
path('admin/', admin.site.urls),
path('', views.HomePage.as_view(),name='home'),
path('about/', views.AboutPage.as_view(),name='about'),
path('order/', views.OrderView.as_view(),name='order'),
path('order/thanks/',
views.OrderThanksView.as_view(),name='order_thanks'),
]
Appologies I dont know how to display the code correctly in the post.
I added some debugging to the code and it turns out that the form is not valid so the redirect doesnt happen?
===============
I got the redirect working by making the multiple choice checkboxes as blank=True and setting the action in the template to "{% url 'order' %}
There seems to be an issue with the form when you select multiple options with eh check-boxes. Any help would be appreciated.
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)
I am submitting a POST request via django form to my Django Rest Framework api.
Here is a snippet of my form:
<form action="{% url 'entry-list' %}" method="POST" class="form" role="form">
{% csrf_token %}
{{form.as_p}}
<div class = "form-group">
<button type="submit" class="save btn btn-default btn-block">Save</button>
</div>
views.py:
class entry_ViewSet(viewsets.ModelViewSet):
queryset = Entry.objects.all()
serializer_class= EntrySerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,IsOwnerOrReadOnly,)
def perform_create(self, serializer):
serializer.partial = True
serializer.save(created_by=self.request.user)
I am making a successful POST (and item is created in database), however once I save I go to the url /api/entry/ which shows my api w/Markdown. I'd like to have it go back to a specific url.
Is there a way to customize where the POST redirect to if successful?
Added Serializer:
class EntrySerializer(serializers.ModelSerializer):
created_by = serializers.ReadOnlyField(source='created_by.username')
class Meta:
model = Entry
fields = '__all__'
def __init__(self, *args, **kwargs):
super(EntrySerializer, self).__init__(*args, **kwargs)
for x in self.fields:
self.fields[x].required = False
viewsets.ModelViewSet has a method create that returns Response object. The response object is subtype of Django Response. Hence you can change behavior using HttpResponseRedirect into the create method. For example:
class entry_ViewSet(viewsets.ModelViewSet):
queryset = Entry.objects.all()
serializer_class= EntrySerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,IsOwnerOrReadOnly,)
def create(self, request, *args, **kwargs):
response = super(entry_ViewSet, self).create(request, *args, **kwargs)
# here may be placed additional operations for
# extracting id of the object and using reverse()
return HttpResponseRedirect(redirect_to='https://google.com')
def perform_create(self, serializer):
serializer.partial = True
serializer.save(created_by=self.request.user)