Multiple select Django keeps throwing not a valid value
Question:
I want to be able to select multiple categories when a blog post is created. I can do this without a problem on Django admin but I can’t do it using a bootstrap form.
I have a bootstrap form that a user will be able to see the list of categories available in the database. I can show the values in the dropdown menu and I am able to select multiple categories, but when I hit post, it keeps saying {category} is not a valid value.
I have tried save_m2m()
but it didn’t work either. A post can have multiple categories and a category can have multiple post.
I can’t figure out if it’s in my model or form or the html file itself.
models.py
class Category(models.Model):
category_name = models.CharField(max_length=255)
slug = models.SlugField(blank=True, unique=True)
date_created = models.DateTimeField(auto_now_add=True, verbose_name='date created')
date_updated = models.DateTimeField(auto_now=True, verbose_name='date updated')
class Meta:
verbose_name_plural = 'categories'
def __str__(self):
return self.category_name
class BlogPost(models.Model):
STATUS = (
('draft', 'Draft'),
('published', 'Published'),
)
title = models.CharField(max_length=500, null=False, blank=False)
body = models.TextField(max_length=5000, null=False, blank=False)
image = models.ImageField(upload_to=upload_location, null=False, blank=False)
date_published = models.DateTimeField(auto_now_add=True, verbose_name='date published')
date_updated = models.DateTimeField(auto_now=True, verbose_name='date updated')
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
slug = models.SlugField(blank=True, unique=True)
category = models.ManyToManyField(Category)
status = models.CharField(max_length=10, choices=STATUS, default='draft')
def __str__(self):
return self.title
forms.py
class CreateBlogPostForm(forms.ModelForm):
class Meta:
model = BlogPost
fields = ['title', 'body', 'image', 'category', 'status']
views.py
def create_blog_view(request):
context = {}
user = request.user
categories = Category.objects.all
statuses = BlogPost.STATUS
if not user.is_authenticated:
return redirect('login')
form = CreateBlogPostForm(request.POST or None, request.FILES or None)
if form.is_valid():
obj = form.save(commit=False)
author = Account.objects.filter(username=user.username).first()
obj.author = author
obj.save()
form = CreateBlogPostForm()
context['form'] = form
context['categories'] = categories
context['statuses'] = statuses
return render(request, 'my_simple_blog/create-blog.html', context)
create-blog.html
<form class="create-form" method="post" enctype="multipart/form-data">
{% csrf_token %}
<!-- title -->
<!-- body -->
<!-- image -->
<!-- category -->
<div class="form-group">
<label for="id_category">Category</label>
<select multiple class="form-control" id="id_category" name="category">
{% for category in categories %}
<option value="{{ category.id }}">{{ category }}</option>
{% endfor %}
</select>
</div>
<!-- status -->
<div class="form-group">
<label for="id_status">Status</label>
<select class="form-control" id="id_status" name="status">
{% for val, name in statuses %}
<option value="{{ val }}">{{ name }}</option>
{% endfor %}
</select>
</div>
{% for field in form %}
<p>{% for error in field.errors %}
<p style="color: red;">{{ error }}</p>
{% endfor %}
</p>
{% endfor %}
{% if form.non_field_errors %}
<div style="color:red;">
<p>{{ form.non_field_errors }}</p>
</div>
{% endif %}
<!-- Submit btn -->
<button class="submit-button btn btn-lg btn-primary btn-block" type="submit">POST</button>
</form>
UPDATE:
I got it working to a point where I can select multiple and create the post by sending category id as the value of the multi select option.
But this selection is not reflected in Django Admin.
Answers:
The solution turns out to be very simple, django-crispy-forms.
Once I’ve added the required settings in settings.py
I updated my create-blog.html
:
{% load crispy_forms_filters %}
<form class="create-form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form | crispy }}
{% for field in form %}
<p>{% for error in field.errors %}
<p style="color: red;">{{ error }}</p>
{% endfor %}
</p>
{% endfor %}
{% if form.non_field_errors %}
<div style="color:red;">
<p>{{ form.non_field_errors }}</p>
</div>
{% endif %}
<!-- Submit btn -->
<button class="submit-button btn btn-lg btn-primary btn-block" type="submit">POST</button>
</form>
Make sure to load crispy_forms_filters at the top of the file.
In my views.py
I have to use save_m2m
since I’ve used obj.save(commit=False)
.
def create_blog_view(request):
context = {}
user = request.user
categories = Category.objects.all
statuses = BlogPost.STATUS
if not user.is_authenticated:
return redirect('login')
form = CreateBlogPostForm(request.POST or None, request.FILES or None)
if form.is_valid():
obj = form.save(commit=False)
author = Account.objects.filter(username=user.username).first()
obj.author = author
obj.save()
form.save_m2m()
form = CreateBlogPostForm()
context['form'] = form
context['categories'] = categories
context['statuses'] = statuses
return render(request, 'my_simple_blog/create-blog.html', context)
And you should be able to do multiple select and the choices will be reflected in Django Admin.
In my case I was setting a sliced QuerySet in my custom form:
class MyModelAdminForm(forms.ModelForm):
"""Custom form to prevent the huge list of ManyToMany choices"""
class Meta:
model = MyModel
exclude = []
def __init__(self, *args, **kwargs):
super(MyModelAdminForm, self).__init__(*args, **kwargs)
# Keeps only the first 100 choices
self.fields['list_of_choices'].queryset = Choice.objects.all()[:100]
The problem is that Django raises an exception when applying a filter on a slice while saving the form. So I had to change my code to use a "pure" queryset as proposed in this answer:
class MyModelAdminForm(forms.ModelForm):
"""Custom form to prevent the huge list of ManyToMany choices"""
class Meta:
model = MyModel
exclude = []
def __init__(self, *args, **kwargs):
super(MyModelAdminForm, self).__init__(*args, **kwargs)
# Keeps only the first 100 choices
ids = [elem.pk for elem in Choice.objects.all()[:100]]
self.fields['list_of_choices'].queryset = Choice.objects.filter(pk__in=ids)
It’s slower as it makes two queries to the DB, but at least it works with Django Forms and the admin panel works again
I want to be able to select multiple categories when a blog post is created. I can do this without a problem on Django admin but I can’t do it using a bootstrap form.
I have a bootstrap form that a user will be able to see the list of categories available in the database. I can show the values in the dropdown menu and I am able to select multiple categories, but when I hit post, it keeps saying {category} is not a valid value.
I have tried save_m2m()
but it didn’t work either. A post can have multiple categories and a category can have multiple post.
I can’t figure out if it’s in my model or form or the html file itself.
models.py
class Category(models.Model):
category_name = models.CharField(max_length=255)
slug = models.SlugField(blank=True, unique=True)
date_created = models.DateTimeField(auto_now_add=True, verbose_name='date created')
date_updated = models.DateTimeField(auto_now=True, verbose_name='date updated')
class Meta:
verbose_name_plural = 'categories'
def __str__(self):
return self.category_name
class BlogPost(models.Model):
STATUS = (
('draft', 'Draft'),
('published', 'Published'),
)
title = models.CharField(max_length=500, null=False, blank=False)
body = models.TextField(max_length=5000, null=False, blank=False)
image = models.ImageField(upload_to=upload_location, null=False, blank=False)
date_published = models.DateTimeField(auto_now_add=True, verbose_name='date published')
date_updated = models.DateTimeField(auto_now=True, verbose_name='date updated')
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
slug = models.SlugField(blank=True, unique=True)
category = models.ManyToManyField(Category)
status = models.CharField(max_length=10, choices=STATUS, default='draft')
def __str__(self):
return self.title
forms.py
class CreateBlogPostForm(forms.ModelForm):
class Meta:
model = BlogPost
fields = ['title', 'body', 'image', 'category', 'status']
views.py
def create_blog_view(request):
context = {}
user = request.user
categories = Category.objects.all
statuses = BlogPost.STATUS
if not user.is_authenticated:
return redirect('login')
form = CreateBlogPostForm(request.POST or None, request.FILES or None)
if form.is_valid():
obj = form.save(commit=False)
author = Account.objects.filter(username=user.username).first()
obj.author = author
obj.save()
form = CreateBlogPostForm()
context['form'] = form
context['categories'] = categories
context['statuses'] = statuses
return render(request, 'my_simple_blog/create-blog.html', context)
create-blog.html
<form class="create-form" method="post" enctype="multipart/form-data">
{% csrf_token %}
<!-- title -->
<!-- body -->
<!-- image -->
<!-- category -->
<div class="form-group">
<label for="id_category">Category</label>
<select multiple class="form-control" id="id_category" name="category">
{% for category in categories %}
<option value="{{ category.id }}">{{ category }}</option>
{% endfor %}
</select>
</div>
<!-- status -->
<div class="form-group">
<label for="id_status">Status</label>
<select class="form-control" id="id_status" name="status">
{% for val, name in statuses %}
<option value="{{ val }}">{{ name }}</option>
{% endfor %}
</select>
</div>
{% for field in form %}
<p>{% for error in field.errors %}
<p style="color: red;">{{ error }}</p>
{% endfor %}
</p>
{% endfor %}
{% if form.non_field_errors %}
<div style="color:red;">
<p>{{ form.non_field_errors }}</p>
</div>
{% endif %}
<!-- Submit btn -->
<button class="submit-button btn btn-lg btn-primary btn-block" type="submit">POST</button>
</form>
UPDATE:
I got it working to a point where I can select multiple and create the post by sending category id as the value of the multi select option.
But this selection is not reflected in Django Admin.
The solution turns out to be very simple, django-crispy-forms.
Once I’ve added the required settings in settings.py
I updated my create-blog.html
:
{% load crispy_forms_filters %}
<form class="create-form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form | crispy }}
{% for field in form %}
<p>{% for error in field.errors %}
<p style="color: red;">{{ error }}</p>
{% endfor %}
</p>
{% endfor %}
{% if form.non_field_errors %}
<div style="color:red;">
<p>{{ form.non_field_errors }}</p>
</div>
{% endif %}
<!-- Submit btn -->
<button class="submit-button btn btn-lg btn-primary btn-block" type="submit">POST</button>
</form>
Make sure to load crispy_forms_filters at the top of the file.
In my views.py
I have to use save_m2m
since I’ve used obj.save(commit=False)
.
def create_blog_view(request):
context = {}
user = request.user
categories = Category.objects.all
statuses = BlogPost.STATUS
if not user.is_authenticated:
return redirect('login')
form = CreateBlogPostForm(request.POST or None, request.FILES or None)
if form.is_valid():
obj = form.save(commit=False)
author = Account.objects.filter(username=user.username).first()
obj.author = author
obj.save()
form.save_m2m()
form = CreateBlogPostForm()
context['form'] = form
context['categories'] = categories
context['statuses'] = statuses
return render(request, 'my_simple_blog/create-blog.html', context)
And you should be able to do multiple select and the choices will be reflected in Django Admin.
In my case I was setting a sliced QuerySet in my custom form:
class MyModelAdminForm(forms.ModelForm):
"""Custom form to prevent the huge list of ManyToMany choices"""
class Meta:
model = MyModel
exclude = []
def __init__(self, *args, **kwargs):
super(MyModelAdminForm, self).__init__(*args, **kwargs)
# Keeps only the first 100 choices
self.fields['list_of_choices'].queryset = Choice.objects.all()[:100]
The problem is that Django raises an exception when applying a filter on a slice while saving the form. So I had to change my code to use a "pure" queryset as proposed in this answer:
class MyModelAdminForm(forms.ModelForm):
"""Custom form to prevent the huge list of ManyToMany choices"""
class Meta:
model = MyModel
exclude = []
def __init__(self, *args, **kwargs):
super(MyModelAdminForm, self).__init__(*args, **kwargs)
# Keeps only the first 100 choices
ids = [elem.pk for elem in Choice.objects.all()[:100]]
self.fields['list_of_choices'].queryset = Choice.objects.filter(pk__in=ids)
It’s slower as it makes two queries to the DB, but at least it works with Django Forms and the admin panel works again