FormView not saving data in Django - django

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
...

Related

Django: Page not found (404) when posting form data

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

Django class based views form submit 405 method not allowed

I'm trying to make a email submit form in a django app. Coming from Flask though, I'm a bit confused as I'm trying to do this with class based views, but am pretty stuck.
I'm currently getting this error but unsure how to make it successfully post
Method Not Allowed (POST): /newsletter/
Method Not Allowed: /newsletter/
My models class has this
class Newsletter(models.Model):
email = models.CharField(max_length=200, unique=True)
My forms.py has this
from django import forms
class NewsletterForm(forms.Form):
email = forms.CharField(max_length=200)
def send_email(self):
# send email using the self.cleaned_data dictionary
pass
my urls file has this
path('newsletter/', views.NewsletterView.as_view(), name='newsletter'),
and my form submit in my html is like this
<form action="/newsletter/" method="post">{% csrf_token %}
<label for="email">Email: </label>
<input id="email" type="email" name="email_field" placeholder="email#example.com">
<input type="submit" value="Subscribe">
</form>
And here is the views function
from django.views import generic
from .models import Post
from blog.forms import NewsletterForm
class PostList(generic.ListView):
queryset = Post.objects.filter(status=1).order_by('-created_on')
template_name = 'index.html'
paginate_by = 3
class PostDetail(generic.DetailView):
model = Post
template_name = 'post_detail.html'
class NewsletterView(generic.TemplateView):
template_name = "newsletter.html"
form_class = NewsletterForm
success_url = '/thanks/'
def form_valid(self, form):
# This method is called when valid form data has been POSTed.
# It should return an HttpResponse.
form.send_email()
return super().form_valid(form)
You have to define a post method in your NewsletterView:
def post(self, request, *args, **kwargs):
...
Check this out ;) https://docs.djangoproject.com/en/3.0/topics/class-based-views/intro/#handling-forms-with-class-based-views

Overriding ModelForm with widgets

A user has a form where he can save sensitive data. I am capable of crypting this data and store it in the database using modelforms. However, if the user wants to modify this data, it appears in the TextInput from the form.
I've read this post, and this one. The answer seems to be there, but I can't manage to suceed in the implementation. Here is what I have:
Models
class CustomUser(AbstractUser):
api_key = models.CharField(max_length=256, default='null')
Forms
class APIForm(forms.ModelForm):
class Meta:
model = CustomUser
fields = ('api_key')
widgets = {
'api_key': TextInput(attrs={'size': 10, 'placeholder': 'Your key'}),
}
Html
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button type="submit">Save changes</button>
</form>
It seems that the form is not detecting the widget, since if I change the value of the attribute size, the form will not change.
Am I missing something?
Thanks in advance
UPDATE.
Here is my view code simplified:
Views
class KeyView(LoginRequiredMixin, UpdateView):
model = CustomUser
form_class = APIForm
template_name = 'account/api_form.html'
success_url = reverse_lazy('pages:home')
def get_object(self):
return self.request.user
def form_valid(self, form):
self.object = form.save(commit=False)
key=botcrypt.encrypt_val(self.object.api_key)
self.object.api_key =key.decode("utf-8")
self.object.save()
messages.success(self.request, 'key updated with success!')
return super().form_valid(form)
I'm using allauth for accounts, in case this information is important
You can try alernative for above method
api_key = forms.CharField(_(u'API Key'), required=False)
api_key.widget = forms.TextInput(attrs={'size': 10, 'title': 'API Key',})
or
api_key = forms.CharField(
_(u'API Key'),
required=False,
widget=forms.TextInput(attrs={'size': 10, 'title': 'API Key',})
)
So actually the solution was pretty simple, as expected....
All the code from Models, Forms and html was right.
I only had to empty the value of the key within the get_object from views:
def get_object(self):
self.request.user.api_key = ""
return self.request.user

405 error on submitting a modelform using class based views

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.

displaying other form inside DetailView in django

I have the detail view of "part" in views.py
class part_detail_view(DetailView):
model = part_list
context_object_name = 'part_detail'
template_name = 'part_detail.html'
def get_context_data(self, **kwargs):
context = super(part_detail_view, self).get_context_data(**kwargs)
context['my_list'] = populate_nav_bar()
return context
but inside this URL I also want to display another form "CreateView" of the stock model so that from the same page I can display the form to add stock of the part and add the stock. This was done easily in function based view but I am not sure how to do this in class based view.
You must create your form explicitly in forms.py:
# forms.py
class ParForm(forms.ModelForm):
class Meta:
model = Part # Your Part model
fields = '__all__'
Then just inject the form into the context in get_context_data():
# views.py
class PartDetailView(DetailView):
...
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['part_form'] = PartForm() # Your part form
return context
Then render the form in template:
...
<form method="post" action="{% url 'create_part' %}">
{{ part_form }}
<button type="submit">Submit</button>
</form>
...
where create_part is the url of your CreateView.