displaying other form inside DetailView in django - 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.

Related

How to update the user profile of a different user if you are logged in as the owner?

Currently, I am logged in as an owner and I want to update the fields of customers in the database. But the form does not update or show the details as a placeholder because the user model has extended the customer model and hence, the customer model does not have its own fields. How do I get the instance of the User of the Customer in the UpdateView?/How do I update the customer?
urls.py
urlpatterns = [
path('<int:pk>/update/',CustomerUpdateView.as_view()),
]
views.py
class CustomerUpdateView(OwnerAndLoginRequiredMixin, generic.UpdateView):
template_name = "customer_update.html"
form_class = CustomerModelForm
queryset = Customer.objects.all()
def get_success_url(self):
return "/customers"
models.py
class Customer(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
def __str__(self):
return self.user.username
class User(AbstractUser):
is_owner = models.BooleanField(default=True)
is_agent = models.BooleanField(default=False)
is_customer = models.BooleanField(default=False)
forms.py
class CustomerModelForm(forms.ModelForm):
class Meta:
model = User
fields = (
'email',
'username',
'first_name',
'last_name',
)
So at this point, I'd have to assume a few things here... Let's say you have a ListView to render a list of customers.
views.py file:
class CustomerListView(OwnerAndLoginRequiredMixin, generic.ListView):
template_name = "customer_update.html"
queryset = Customer.objects.all()
context_object_name = 'customers'
urls.py file:
urlpatterns = [
...
path('customers/', CustomerListView.as_view(), name='customers'),
path('update-customer/<int:pk>/', CustomerUpdateView.as_view(), name='update-customer'),
# You can pass whatever customer related info you wish via url, I'm just using id as a simply demo.
...
]
html file:
{% for customer in customers %}
# Displaying other related info per customer
# Link to click to update a particular customer profile: passing the customer pk via url
Update {{ customer.user.username|title }} Profile
{% endfor %}
Back to the views.py file:
class CustomerUpdateView(OwnerAndLoginRequiredMixin, generic.UpdateView):
template_name = "customer_update.html"
form_class = CustomerModelForm
# context_object_name = 'customer'
# queryset = Customer.objects.all() # not needed here
# You can use the get_object() on the class to grab the customer object by the pk passed via url
def get_object(self, queryset=None):
customer_pk = self.kwargs.get('pk', None)
return get_object_or_404(Customer, pk=customer_pk)
# Also, you could use the get_context_data() to set the form values before rendering on the page
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
customer = self.get_object() # -> returns a customer object
# creating a dictionary to use to initialize the form: accessing the user information on the customer
data = {
'username': customer.user.username,
'first_name': customer.user.first_name,
'last_name': customer.user.last_name,
'email': customer.user.email,
}
form = self.form_class(initial=data, instance=customer.user) # updated here too
context['form'] = form # -> updating the class view context dictionary
return context
def get_success_url(self):
return "/customers"
Now within the customer_update.html:
<form method="POST">
<div>
{{ form.username }}
{{ form.email }}
{{ form.first_name }}
{{ form.last_name }}
</div>
<input type="submit" value="Update"/>
</form>
Ideally, that should display the customer's information in the form.
UPDATES
To handle and save the form submission, you can use the post() on the update-view. You can add to the CustomerUpdateView:
# Use the post method to handle the form submission
def post(self, request, *arg, **kwargs):
# Should set the user instance on the form
customer = self.get_object()
form = self.form_class(request.POST, instance=customer.user) # updated here too
if form.is_valid():
form.save()
return redirect('to any path of your choice') # Redirect upon submission if necessary
else:
print(form.errors) # To see the field(s) preventing the form from being submitted
# Passing back the form to the template
return render(request, self.template_name, {'form': form})

Model Formset and regular ModelForm in same template?

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

FormView not saving data in 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
...

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

How do I pass an additional object to a Django form so I can render some fields in the template?

I have a form in Django where site visitors can submit "gear" to be included in a person's set of gear. Here's the URL to the change form:
# urls.py
path('person/<slug:slug>/gear/submit/', GearSubmitView.as_view(), name='forms/submit_gear'),
You can see that the person for whom the gear is being submitted is represented by a slug in the URL.
Here's the first part of the CreateView:
# views.py
class GearSubmitView(LoginRequiredMixin, CreateView):
"""Allows for third-party submissions for a pro's gear collection."""
template_name = 'forms/submit_gear.html'
form_class = GearSubmitForm
success_message = 'Success: Submission added.'
And the form:
# forms.py
class GearSubmitForm(forms.ModelForm):
class Meta:
model = PersonProduct
fields = ['product', 'version', 'setup_note', 'usage_start_date', 'evidence_link', 'evidence_text']
where PersonProduct is a junction table between my Person and Product models.
And the template:
# submit_gear.html
{% extends '_base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class="container">
<h2 id="content-header">Submit Gear For {{ person.full_name }}</h2>
{% crispy form %}
</div>
{% endblock content %}
where you can see what I'm trying to do. I want to insert the name of the person represented by the slug in the URL in the form template.
How can I do it?
You can override get_context_data method in your views.py as mentioned in FormMixin.
class GearSubmitView(LoginRequiredMixin, CreateView):
"""Allows for third-party submissions for a pro's gear collection."""
template_name = 'forms/submit_gear.html'
form_class = GearSubmitForm
success_message = 'Success: Submission added.'
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
person = Person.objects.filter(slug=self.kwargs.get("slug")).first()
data['full_name'] = person.full_name if person else ""
return data
You can change the variable name with full_name in the html file. You can also pass whole instance if you need, I just minimize the data sending from view to html. I didn't run the code block above but it should something like this.
Here you can use the get_context_data method inside of your createview it will look like :
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user'] = self.request.user
return context
and on your template you will add :
{{user}}
Hope it help you out !