Search code examples
pythondjangodjango-viewsdjango-formsmany-to-many

Django Value Error: Cannot Assign Queryset must be instance, ManyToMany Relationship


I have built a model,OpeningDays, to manage a ManyToMany relationship (opening_days field in my BookingManagement model), since I wanted to add some additional data to each instance. I am using the 'through' approach as you can see from my models.py. Within my form, I have a CheckboxSelectMultiple, and for each day which is checked I want to create a model instance. However, I am getting the following error message:

ValueError at /listings/createlisting/openingdays/b8s43t Cannot assign "<QuerySet [<DaysOfWeek: Monday>, <DaysOfWeek: Tuesday>, <DaysOfWeek: Wednesday>]>": "OpeningDays.daysofweek" must be a "DaysOfWeek" instance.

The exception is being raise on this line of my code:

 if form.is_valid(): 

In order to try and solve this I have attempted to iterate through the queryset in my view with a for loop. However this hasn't worked either and I get the same error message. Any ideas how I can fix this?

My models.py:

class DaysOfWeek(models.Model):
    day = models.CharField(max_length=13)

    def __str__(self):
        return str(self.day)


class BookingManagement(models.Model):


    listing = models.ForeignKey(Listing, verbose_name="Listing", on_delete=models.CASCADE)
    opening_days = models.ManyToManyField(DaysOfWeek, verbose_name="Opening Days", through="OpeningDays")
    booking_slots_per_day = models.IntegerField(verbose_name="Available booking slots per day", blank=True)
    max_bookings_per_time_slot = models.PositiveIntegerField(verbose_name="Maximum bookings per time slot", blank=False)
    standard_booking_duration = models.PositiveIntegerField(verbose_name="Standard booking duration in minutes", blank=False)
    min_booking_duration = models.PositiveIntegerField(verbose_name="Minimum booking duration in minutes", blank=True)
    max_booking_duration = models.PositiveIntegerField(verbose_name="Maximum booking duration in minutes", blank=True)
    booking_duration_increment = models.PositiveIntegerField(verbose_name="Booking duration increment", blank=True)
    min_people_per_booking = models.PositiveSmallIntegerField(verbose_name="Minimum number of people per booking", blank=False)
    max_people_per_booking = models.PositiveSmallIntegerField(verbose_name="Maximum number of people per booking", blank=False)
    turnaround_time = models.PositiveSmallIntegerField(verbose_name="Turn around time between activities", blank=True)
    time_slot_frequency = models.PositiveSmallIntegerField(verbose_name="Time slot frequency", blank=False)
    max_people_per_time_slot = models.PositiveSmallIntegerField(verbose_name="Maximum number of people who can book per time slot", blank=False)
    max_people_per_day = models.PositiveSmallIntegerField(verbose_name="Maximum number of people who can book per day", blank=True)
    min_adults_per_booking = models.PositiveSmallIntegerField(verbose_name="Minimum number of adults per booking", blank=True)
    max_children_per_adult = models.PositiveSmallIntegerField(verbose_name="Maximum number of children per adult", blank=True)
    min_age = models.PositiveSmallIntegerField(verbose_name="Minimum age", blank=False)
    max_age = models.PositiveSmallIntegerField(verbose_name="Maximum age", blank=True)
    child_max_age = models.PositiveSmallIntegerField(verbose_name="Maximum age for child", blank=True)


class OpeningDays(models.Model):
    bookingmanagement = models.ForeignKey(BookingManagement, on_delete=models.CASCADE)
    daysofweek = models.ForeignKey(DaysOfWeek, on_delete=models.CASCADE, null=True)
    opening_time = models.TimeField(verbose_name="Opening Time", blank=True, null=True)
    closing_time = models.TimeField(verbose_name="Closing Time", blank=True, null=True)

    class Meta:
        unique_together = [['bookingmanagement', 'daysofweek']]

My forms.py:

class OpeningDaysForm(forms.ModelForm):
    """A form to complete the opening days of a listing. """

    daysofweek = forms.ModelMultipleChoiceField(queryset=DaysOfWeek.objects.all(), widget=forms.CheckboxSelectMultiple(attrs={'name': 'daysofweek', 'id': 'daysofweek'}))

    class Meta:

        model = OpeningDays
        fields = ['daysofweek']

My views.py:

class OpeningDaysView(NextUrlMixin, View):
    form_class = OpeningDaysForm
    template_name = 'listings/createlistingopeningdays.html'
    default_next = '/'
    

    def get(self, request, *args, **kwargs):
        form = self.form_class()
        context = {
            'form': form
        }
        return render(request, self.template_name, context=context)

    def post(self, request, *args, **kwargs):
        form = self.form_class(request.POST)
        listing = get_object_or_404(Listing, slug=self.kwargs['slug'])
        bookingmanagement = get_object_or_404(BookingManagement, listing=listing)
        if form.is_valid():
            open_days = request.POST.getlist('daysofweek')
            for day in open_days:
                opendays = OpeningDays(
                    bookingmanagement = bookingmanagement,
                    daysofweek = day
                )
                opendays.save()

            slug = listing.slug
            return redirect(reverse('listings:create_opening_hours', kwargs={'slug': slug}))
            # return redirect(self.get_next_url())

        else:
            print("Form Validation Error")
            context = {
                'form': form
            }
            return render(request, self.template_name, context)

And my html template:

{% extends "base.html" %}
{% block content %}

    <h1>Opening Days</h1>

    <div class="container">
        <form method="POST" element="multipart/form-data">
            {% csrf_token %}

            <div class="mb-3">
                <label for="title">Opening Days</label>
                {{form.daysofweek}}
            </div>

            <div class='mb-5'>
                <button type='submit' class='btn btn-primary'>Next</button>
            </div>

        </form>
    </div>
{% endblock %}

Solution

  • You currently have the daysofweek attribute as a ForeignKey relationship, which means that your OpeningDays can only ever be related to one DaysOfWeek.

    Your form field is normalizing to a QuerySet of DaysOfWeeks, which the ModelForm then tries to assign to the daysofweek field on an OpeningDays model. This won't work, because the FK relationship requires a single instance.

    The quick fix would be to use a regular Form instead of a ModelForm, especially since you aren't using the the rest of the ModelForm functionality for object creation, and you don't appear to be actually populating the form in your get from an existing OpeningDays.

    So, to fix your current problem, you'd just change your form to be:

    class OpeningDaysForm(forms.Form):
    """A form to complete the opening days of a listing. """
    
    daysofweek = forms.ModelMultipleChoiceField(queryset=DaysOfWeek.objects.all(), widget=forms.CheckboxSelectMultiple(attrs={'name': 'daysofweek', 'id': 'daysofweek'}))
    

    It does look like maybe you don't quite understand what the form is doing for you, since you are manually pulling data from the POST. After calling is_valid, you can access the normalized Queryset by using the cleaned_data dictionary on the form (Django 4 docs). You're doing double work by manually getting the list from the POST.