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.

Asked By: azmirfakkri

||

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.

Answered By: azmirfakkri

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

Answered By: Genarito
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.