Search code examples
pythondjangomany-to-many

Django Generic View form_valid Cannot set values on a ManyToManyField which specifies an intermediary model


I have three Tables in a many-to-many-relationship, a Product table, a Server table and an intemediary table to link the two objects together.

Each Server can have many products, and each product can be associated with more than one server.

Here are my models

#/myapp/models.py

class Server(TimeStampedModel):
    name = models.CharField(max_length=35)
    description = models.CharField(max_length=200)
    products = models.ManyToManyField('Product', through='ServerProduct',
                                      related_name='products')

class ServerProduct(TimeStampedModel):
    server = models.ForeignKey('Server', on_delete=models.CASCADE)
    product = models.ForeignKey('Product', on_delete=models.CASCADE)


class Product(TimeStampedModel):
    name = models.CharField(max_length=200)
    price = models.DecimalField(decimal_places=2, max_digits=11)
    servers = models.ManyToManyField(
        'Server', through='ServerProduct', related_name='servers')

In my create view I'm pointing to a form to allow users to create a Server, and select it's corresponding products...

Inside form_valid() I'm trying to link each Product to the new Server

#/myapp/views.py

class ServerCreateView(SuccessMessageMixin, CreateView):
        model = Server
        form_class = ServerForm
        ....

        def form_valid(self, form):
            server = form.save(False)
            server.save()
            for product in form.cleaned_data['products']:
                ServerProduct.objects.create(server=server, product=product)
            return super(ServerCreateView, self).form_valid(form)

My form looks as follows..

class ServerForm(BlankToRequiredMixin):

    class Meta:
        model = Server
        fields = '__all__'
        widgets = {
            'name': forms.TextInput(attrs={'autofocus': 'autofocus'}),
        }

However when I submit the form django returns the following error:

Cannot set values on a ManyToManyField which specifies an intermediary model. Use reports.ServerProduct's Manager instead.

In place of ServerProduct.objects.create(server=server, product=product) I've also tried the following (after reading the documentation here) but this returns the same error

prod = ServerProduct(server=server, product=product)
prod.save()

Any idea how I could solve this? (Preferably still using the Generic create view)

EDIT: Full Traceback

Environment:


Request Method: POST
Request URL: http://localhost:8000/server-create/

Django Version: 1.9.7
Python Version: 3.4.2
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django_extensions',
 'auditlog',
 'rest_framework',
 'reports.apps.ReportsConfig']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'auditlog.middleware.AuditlogMiddleware']



Traceback:

File "/home/jwe/piesup2/venv/lib/python3.4/site-packages/django/core/handlers/base.py" in get_response
  149.                     response = self.process_exception_by_middleware(e, request)

File "/home/jwe/piesup2/venv/lib/python3.4/site-packages/django/core/handlers/base.py" in get_response
  147.                     response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/home/jwe/piesup2/venv/lib/python3.4/site-packages/django/views/generic/base.py" in view
  68.             return self.dispatch(request, *args, **kwargs)

File "/home/jwe/piesup2/venv/lib/python3.4/site-packages/django/views/generic/base.py" in dispatch
  88.         return handler(request, *args, **kwargs)

File "/home/jwe/piesup2/venv/lib/python3.4/site-packages/django/views/generic/edit.py" in post
  256.         return super(BaseCreateView, self).post(request, *args, **kwargs)

File "/home/jwe/piesup2/venv/lib/python3.4/site-packages/django/views/generic/edit.py" in post
  222.             return self.form_valid(form)

File "/home/jwe/piesup2/reports/views.py" in form_valid
  182.         return super(ServerCreateView, self).form_valid(form)

File "/home/jwe/piesup2/venv/lib/python3.4/site-packages/django/contrib/messages/views.py" in form_valid
  11.         response = super(SuccessMessageMixin, self).form_valid(form)

File "/home/jwe/piesup2/venv/lib/python3.4/site-packages/django/views/generic/edit.py" in form_valid
  201.         self.object = form.save()

File "/home/jwe/piesup2/venv/lib/python3.4/site-packages/django/forms/models.py" in save
  452.             self._save_m2m()

File "/home/jwe/piesup2/venv/lib/python3.4/site-packages/django/forms/models.py" in _save_m2m
  434.                 f.save_form_data(self.instance, cleaned_data[f.name])

File "/home/jwe/piesup2/venv/lib/python3.4/site-packages/django/db/models/fields/related.py" in save_form_data
  1618.         setattr(instance, self.attname, data)

File "/home/jwe/piesup2/venv/lib/python3.4/site-packages/django/db/models/fields/related_descriptors.py" in __set__
  481.         manager.set(value)

File "/home/jwe/piesup2/venv/lib/python3.4/site-packages/django/db/models/fields/related_descriptors.py" in set
  882.                     (opts.app_label, opts.object_name)

Exception Type: AttributeError at /server-create/
Exception Value: Cannot set values on a ManyToManyField which specifies an intermediary model. Use reports.ServerProduct's Manager instead.

Solution

  • The traceback shows that the error is occurring when you call super() in the form_valid method.

    File "/home/jwe/piesup2/reports/views.py" in form_valid
       182.         return super(ServerCreateView, self).form_valid(form)
    

    You are already saving the form in your form_valid method, so there is no need to call super(). Just redirect to the success url instead.

        def form_valid(self, form):
            server = form.save(False)
            server.save()
            for product in form.cleaned_data['products']:
                ServerProduct.objects.create(server=server, product=product)
    
            return HttpResponseRedirect(self.get_success_url())
    

    Remember to add the import:

    from django http import HttpResponseRedirect