Search code examples
pythondjangodjango-modelsdjango-viewsdjango-forms

Django form error using ManyToMany relationship - TypeError: Field 'id' expected a number but got <Profile: user1>


I am making a recommendation app where a user can recommend a movie/show/book to one or more users. The form is set up so that it displays a list of all users and you can check a box next to the users you want to send the recommendation to, but I am getting this error (Field 'id' expected a number but got <Profile: user1>) when I hit "Submit" on my form after checking the box next to the username and I'm not sure where I went wrong. Most of the other questions related to this are using ForeignKey rather than ManyToManyField in their model.

models.py:

from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    follows = models.ManyToManyField( 
        'self',
        related_name='followed_by',
        symmetrical=False,
        blank=True,
    )

    def __str__(self):
        return self.user.username
    
class Recommendation(models.Model):
    user = models.ForeignKey(
        User, related_name="recs", on_delete=models.DO_NOTHING
    )
    recipients = models.ManyToManyField(
        User, related_name="received_recs", symmetrical=False, blank=True
    )
    title = models.CharField(max_length=300)
    description = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return (
            f"{self.user} "
            f"({self.created_at:%y-%m-%d %H:%M}): "
            f"{self.title}"
        )

views.py:

from django.shortcuts import render, redirect
from .models import Profile, Recommendation
from .forms import RecForm

def dashboard(request):
  all_recs = Recommendation.objects.all()
  if request.method == "POST":
    form = RecForm(request.POST or None)
    if form.is_valid():
      rec = form.save(commit=False)
      rec.user = request.user
      rec.save()
      form.save_m2m()
      return redirect("watchthisshit:dashboard")
  form = RecForm()
  return render(request, "watchthisshit/dashboard.html", {
    "all_recs": all_recs,
    "form": form
  })

forms.py:

from django import forms
from django.db.models.base import Model
from .models import Profile, Recommendation

class RecForm(forms.ModelForm):
  title = forms.CharField(required=True)
  description = forms.CharField(widget=forms.widgets.Textarea(
    attrs={
      "placeholder": "You don't have to write a description, but it would be pretty cool if you did...",
      "class": "textarea is-success is-medium"
    }
  ), label="",)
  recipients = forms.ModelMultipleChoiceField(
    queryset=Profile.objects.all(),
    widget=forms.CheckboxSelectMultiple
  )

  class Meta:
    model = Recommendation
    fields = ["title", "description", "recipients"]
    exclude = ("user", )

I thought the problem might be with how it is getting passed to ModelMultipleChoiceField, so I tried doing the following in my forms.py:

recipients = forms.ModelMultipleChoiceField(
    queryset=Profile.objects.all().values_list('id', 'user'),
    widget=forms.CheckboxSelectMultiple
  )

However, that just returns a tuple of (1, 1) and I can't access the name. It also doesn't actually save anything.


Solution

  • Simply because in your Recommendation you set recipients related to the User object. But, in your form you are using the Profile to populate your select, a minor mistake.

    So, when you try to .save_m2m it breaks down, because the field expects an User not a Profile, a quick fix:

    forms.py

    class RecForm(forms.ModelForm):
        title = ...
        description = ...
        recipients = forms.ModelMultipleChoiceField(
            queryset=User.objects.all(), widget=forms.CheckboxSelectMultiple
        )
    
        class Meta:
            ...
    

    A little improvement

    So that an User cannot mark himself as recipient. We can override the Form __init__ method and by providing an initial value we can exclude the current user from the QuerySet.

    forms.py

    class RecForm(forms.ModelForm):
        title = ...
        description = ...
        recipients = forms.ModelMultipleChoiceField(
            queryset=None, widget=forms.CheckboxSelectMultiple
        )
    
        class Meta:
            ...
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            current_user_id = self.initial.get('user', None)
            self.fields["recipients"].queryset = User.objects.exclude(pk=current_user_id)
    

    views.py

    def dashboard(request):
        all_recs = Recommendation.objects.all()
    
        if request.method == "POST":
            ...
        form = RecForm(initial={"user": request.user.id})
        return render(
            request, "watchthisshit/dashboard.html", {"all_recs": all_recs, "form": form}
        )