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
Related
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})
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).
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})
When I read the documentation about modelforms and widgets it looks like you can use any widget on any modelform but that there are certain default widgets used for form fields corresponding to modelform fields. I'm able to render a radio input using a form field but not with a modelform field.
I have tried many different things, but I just can't seem to render a RadioSelect widget of a modelform field which comes from a model field. Is this even possible?
Btw, my goal is to let the initial value of the radio input correspond with the current value of the model field(A boolean).
Attempt 1:
# views.py
class SettingsView(FormView):
template_name = 'settings.html'
success_url = 'settings/saved/'
form_class = NicknameForm
def post(self, request, *args, **kwargs):
profile = request.user.get_profile()
if request.POST['show_nickname'] == 'False':
profile.show_nickname = False
profile.save()
elif request.POST['show_nickname'] == 'True':
profile.show_nickname = True
profile.save()
return super(NicknameFormView, self).post(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""
To be able to use 'show_nickname_form' instead of plain 'form' in the template.
"""
context = super(NicknameFormView, self).get_context_data(**kwargs)
context["show_nickname_form"] = context.get('form')
return context
# models.py
from django.db import models
from django.contrib.auth.models import User
class Profile(models.Model):
user = models.OneToOneField(User,
unique=True,
verbose_name='user',
related_name='profile')
show_nickname = models.BooleanField(default=True)
# forms.py
from django import forms
from models import Profile
CHOICES = (('shows_nickname', 'Yes'), ('hides_nickname', 'No'))
class NicknameForm(forms.ModelForm):
class Meta:
model = Profile
fields = ('show_nickname',)
widgets = {
'show_nickname': forms.RadioSelect(attrs={'choices': CHOICES}),
}
Part from my template:
<form action='' method="post">
{{ show_nickname_form.as_ul }} {% csrf_token %}
<input type="submit" value="Save setttings">
</form>
The form that is rendered from {{ show_nickname_form.as_ul }}:
<li><label for="id_show_nickname_0">show nickname:</label>
<ul></ul>
</li>
<div style='display:none'><input type='hidden' name='csrfmiddlewaretoken' value='1BqD6HJbP5e01NVwLtmFBqhhu3Y1fiOw' /></div>`
Attempt 2:
# forms.py
from django import forms
from models import Profile
class NicknameForm(forms.ModelForm):
class Meta:
model = Profile
fields = ('show_nickname',)
widgets = {
'show_nickname': forms.RadioSelect(),
}
Attempt 3
# forms.py
CHOICES = ((True, 'On',),
(False, 'Off',))
class NicknameForm(ModelForm):
show_nickname = ChoiceField(widget=RadioSelect, choices=CHOICES, initial=True , label='')
class Meta:
model = Profile
fields = ('show_nickname',)
This renders the radio input fine but I need it to take the initial value of the corresponding model field show_nickname instead of the constant True.
I am using Django 1.4 btw.
You need to make it into a ChoiceField instead of a BooleanField with a choice for each and a RadioWidget in order for it to display radio buttons.
https://docs.djangoproject.com/en/dev/ref/forms/fields/#django.forms.ChoiceField
If you want to keep the boolean field, you will most likely have to do some hacking to create your own field/widget.
# views.py
class SettingsView(FormView):
template_name = 'settings.html'
success_url = 'settings/saved/'
form_class = NicknameForm
def get_form(self, form_class):
"""
Returns an instance of the form to be used in this view.
"""
form = super(SettingsView, self).get_form(form_class)
if 'show_nickname' in form.fields:
profile = self.request.user.get_profile()
form.fields['show_nickname'].initial = profile.show_nickname
return form
def post(self, request, *args, **kwargs):
profile = request.user.get_profile()
if request.POST['show_nickname'] == 'False':
profile.show_nickname = False
profile.save()
elif request.POST['show_nickname'] == 'True':
profile.show_nickname = True
profile.save()
return super(NicknameFormView, self).post(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""
To be able to use 'show_nickname_form' instead of plain 'form' in the template.
"""
context = super(NicknameFormView, self).get_context_data(**kwargs)
context["show_nickname_form"] = context.get('form')
return context
I have a model, say 'Article', with a field
published = models.BooleanField(default=True)
and a template with condition:
{% if user.is_staff %}
<li>form.published.label_tag</li>
<li>form.published</li>
{% else %}
<li>form.published.as_hidden</li>
{% endif %}
and I use class-based generic views to add and update for this model.
In this case it is still possible for regular non-staff and malicious user to replace the value of published field.
I think I have to move the condition to views level to prevent this issue, somithing like
class ArticleEdit(UpdateView):
model = Article
form_class = ArticleForm
def form_valid(self, form):
self.object = form.save(commit=False)
if self.request.user.is_staff:
''' How to let the staff change this value? '''
else:
''' How to set previous value? '''
self.object.save()
return HttpResponseRedirect(self.get_success_url())
assuming that I remove this hidden field from template.
I would consider defining two forms, one for staff and one for regular users. You can then override the get_form_class method to select the correct form. If you exclude the published field from the form for non-staff, then they won't be able to change the value.
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
exclude = ('published',)
class ArticleStaffForm(ArticleForm)
class Meta:
model = Article
exclude = ()
class ArticleEdit(UpdateView):
...
def get_form_class(self):
if self.request.user.is_staff:
return ArticleStaffForm
else:
return ArticleForm
you can do something like:
class MyForm(forms.Form):
def __init__(self, user, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
if not user.is_staff:
del self.fields['published']
and then pass the request.user object to the form when initialising it.
WARNING: Untested pseudo code. But this should give you an idea.