danf
danf

Reputation: 11

Django Generic Views ManyToMany with "through" model

I'm having some trouble with my first Django project, which must be a common/easy to solve one! As a bit of background, I am building an application which is used to track therapy appointments. Most of the data structure is fairly simple, apart from this one instance, where a ManyToMany is required, with an intemediary (through) model. As you can see in my models.py below, there are three models which are relevant to the problem I'm having. The Contact model is used for storing contact details of customers. The Case model is to handle the concept of a piece of work/job. Any particular Case can have multiple Sessions, and so on. There are often scenarios where a Case may have two or more Contacts who will split the bill between them. Hence the need for the ManyToMany with the intemediary model to store the percentage of the bill for which that particular contact will be paying. models.py

class Contact(models.Model):
    first_name = models.CharField(max_length=200)
    last_name = models.CharField(max_length=200)
class Case(models.Model):
    invoicees = models.ManyToManyField(Contact, through='Invoicees_Members', through_fields=('case','contact'),null=True, blank=True)
class Invoicees_Members(models.Model):
     contact = models.ForeignKey(Contact, on_delete=models.CASCADE)
     case = models.ForeignKey(Case, on_delete=models.CASCADE)
     invoice_percentage = models.IntegerField(validators = [MinValueValidator(1), MaxValueValidator(100)],null=True, blank=True)

I've done quite a lot of searching around on stackoverflow and other sites on how to handle saving form submissions. The most common solution seems to be what I've tried to implement below. You will notice that I'm trying to use Generic Class-Based Views (CreateView in this case).

views.py

class CaseCreate(CreateView):
    model = Case
    success_url = '/cases/'
    fields = '__all__'
    def form_valid(self, form):
        self.instance = form.save(commit=False)
        for contact in form.cleaned_data['invoicees']:
            invoicee = Invoicees_Members()
            invoicee.case = self.instance
            invoicee.contact = contact
            invoicee.save()
        return super(ModelFormMixin, self).form_valid(form)

Unfortunately, a form submission results in the following error: "Exception Value: save() prohibited to prevent data loss due to unsaved related object 'case'". My assuumption is that for some reason the form.save(commit=False) is not coming back with an ID to use for the Invoicees_Members model save...

Any thoughts? It must be something trivial that I've got wrong here. P.S. I've tried using self.object in place of self.instance and encounter the same error.

Error & Stacktrace:

Traceback:

File "C:\Python27\lib\site-packages\django\core\handlers\base.py" in get_response
  149.                     response = self.process_exception_by_middleware(e, request)

File "C:\Python27\lib\site-packages\django\core\handlers\base.py" in get_response
  147.                     response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "C:\Python27\lib\site-packages\django\views\generic\base.py" in view
  68.             return self.dispatch(request, *args, **kwargs)

File "C:\Python27\lib\site-packages\django\views\generic\base.py" in dispatch
  88.         return handler(request, *args, **kwargs)

File "C:\Python27\lib\site-packages\django\views\generic\edit.py" in post
  256.         return super(BaseCreateView, self).post(request, *args, **kwargs)

File "C:\Python27\lib\site-packages\django\views\generic\edit.py" in post
  222.             return self.form_valid(form)

File "C:\Users\danie\Documents\django-projects\office_management\officeman\views.py" in form_valid
  40.           invoicee.save()

File "C:\Python27\lib\site-packages\django\db\models\base.py" in save
  651.                         "unsaved related object '%s'." % field.name

Exception Type: ValueError at /cases/add
Exception Value: save() prohibited to prevent data loss due to unsaved related object 'case'.

Upvotes: 1

Views: 933

Answers (2)

Nuhil Mehdy
Nuhil Mehdy

Reputation: 2454

May be problem with the saving sequence caused by ManyToMany relationship. Its better to have a read from the offical doc first ...

Every time you save a form using commit=False, Django adds a save_m2m() method to your ModelForm subclass. After you’ve manually saved the instance produced by the form, you can invoke save_m2m() to save the many-to-many form data.

Example:

# Create a form instance with POST data.
>>> f = AuthorForm(request.POST)

# Create, but don't save the new author instance.
>>> new_author = f.save(commit=False)

# Modify the author in some way.
>>> new_author.some_field = 'some_value'

# Save the new instance.
>>> new_author.save()

# Now, save the many-to-many data for the form.
>>> f.save_m2m()

Read the full documentation of save method with an example Here

Upvotes: 1

Mounir
Mounir

Reputation: 11726

You must save case model instance before saving the invoice instance. This error make sense because Django is not able to know the ID for the case instance to be able to save it on invoice model as a foreign key. self.instance = form.save(commit=True). And check this link there is an explanation about this error on Django documentation.

Upvotes: 0

Related Questions