Search code examples
pythondjangomany-to-manydjango-generic-views

ValueError: <Registration: 749>" needs to have a value for field "id" before this many-to-many relationship can be used


I'm building a race registration application in django and I'm having trouble saving a many2many field of my model in a CreateView generic view. I am excluding the event field from the view because it allows you to select an event rather than having it automatically generated from the slug in the url. I was able to get the event object based on the URL using the slug in the get_context_data method. I have also tried form.instance.event = event in the form_valid method but it doesn't seem to be working here. I haven't worked with many2many fields before and I'm currently at a snag. Any help is greatly appreciated.

I am receiving a ValueError: "" needs to have a value for field "id" before this many-to-many relationship can be used.

views.py

class RegistrationCreateView(CreateView):
    model = Registration

    fields = ['car_year', 'car_manufacture', 'car_model', 'race_number', 'race_class']


    def get_context_data(self, *args, **kwargs):
        slug = self.kwargs['slug']
        event = Event.objects.get(slug=slug)

        context = super().get_context_data(**kwargs)
        context['slug'] = slug
        context['event'] = event
        return context

    def form_valid(self, form):
        form.instance.driver = self.request.user
        try:
            event = Event.objects.get(id=self.request.POST['event'])
        except:
            event = None
        print("test")
        form.instance.event.add(event)

        return super().form_valid(form)

urls.py

from django.urls import path
from . import views

app_name = "events"
urlpatterns = [
    path(
        route='add/',
        view=views.EventCreateView.as_view(),
        name='add'
    ),
    path(
        route='',
        view=views.EventListView.as_view(),
        name="list"
    ),
    path(
        route='<slug:slug>/',
        view=views.EventDetailView.as_view(),
        name="detail"
    ),
    path(
        route='<slug:slug>/update/',
        view=views.EventUpdateVIew.as_view(),
        name='update'
    ),
    path(
        route="<slug:slug>/register/",
        view=views.RegistrationCreateView.as_view(),
        name='register'
    ),
]

models.py

from django.db import models
from django.conf import settings

from autoslug import AutoSlugField
from model_utils.models import TimeStampedModel, TimeFramedModel
from localflavor.us.models import USStateField, USZipCodeField
from django.urls import reverse

class Event(TimeStampedModel, TimeFramedModel):
    organizer = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL)
    name = models.CharField("Event Name", max_length=255)
    location_name = models.CharField("Event Location", max_length=255)
    location_street_address = models.CharField("Event Address", max_length=100)
    location_state = USStateField("Event State")
    location_city = models.CharField("City", max_length=40)
    location_zip = USZipCodeField("Event Zipcode")
    description = models.TextField("Event Description")
    slug = AutoSlugField("Event Slug",
        unique=True, always_update=False, populate_from="name")
    registration_start = models.DateField("Pre-Registration Starts", blank=True)
    registration_end = models.DateField("Pre-Registration Ends", blank=True)

    def __str__(self):
        return self.name

    def address(self):
        full_location = f"""
        {self.location_name}
        {self.location_street_address}
        {self.location_city}, {self.location_state} {self.location_zip}
        """
        return(full_location)

    def get_absolute_url(self):
        return reverse('events:detail', kwargs={'slug': self.slug})


class Registration(TimeStampedModel):

    class RaceClass(models.TextChoices):
        STOCK_2WD = "S2", "Stock 2 Wheel Drive"
        STOCK_AWD = "SA", "Stock All Wheel Drive"
        PREP_2WD = "P2", "Prepared 2 Wheel Drive"
        PREP_AWD = "PA", "Prepared All Wheel Drive"
        MOD_2WD = "M2", "Modified 2 Wheel Drive"
        MOD_AWD = "MA", "Modified All Wheel Drive"
        CON_2WD = "C2", "Constructors 2 Wheel Drive"
        CON_AWD = "C4", "Constructurs 4 Wheel Drive"

    driver = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
    event = models.ManyToManyField(Event)
    car_year = models.PositiveIntegerField("Car Year")
    car_manufacture = models.CharField("Car Manufacture", max_length=15)
    car_model = models.CharField("Car Make", max_length=30)
    race_number = models.PositiveIntegerField("Race Number")
    race_class = models.CharField("Race Class", choices=RaceClass.choices, default=RaceClass.STOCK_2WD, max_length=50)

    def get_absolute_url(self):
        return reverse('events:list')

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

Solution

  • You need to save the form first, so the object you want to add many to many objects to is created first. Only after an object has been created, you can add many to many objects to it. So in your example, you would need to do something like this:

    def form_valid(self, form):
        form.instance.driver = self.request.user
        if form.is_valid():
            registration = form.save()  # returns the newly created object
    
        registration.event.add(event)
    
        return super().form_valid(form)