Ryan Castner
Ryan Castner

Reputation: 1054

Django Form's save_m2m() with save(commit=False) not working -- Requesting model field to have a value

I am using commit=False to save my form because I want to include some non-form data (the form's author) in the table of data before saving it. When you set commit=False in Django there is a side effect mentioned in the docs:

Another side effect of using commit=False is seen when your model has a many-to-many relation with another model. If your model has a many-to-many relation and you specify commit=False when you save a form, Django cannot immediately save the form data for the many-to-many relation. This is because it isn’t possible to save many-to-many data for an instance until the instance exists in the database.

So I try to use the save_m2m() method as described in the docs here but to no avail, I am getting the error below.

views.py

class MinutesCreate(LoginRequiredMixin, View):
    @method_decorator(permission_required('pd.add_agenda'))
    def dispatch(self, *args, **kwargs):
        return super(MinutesCreate, self).dispatch(*args, **kwargs)

    def get(self, request, **kwargs):
        minutes_form = MinutesForm()
        return render(request, "pd/minutes_form.html", {"form": minutes_form, "kwargs": kwargs})

    def post(self, request, **kwargs):
        minutes_form = MinutesForm(request.POST)
        if minutes_form.is_valid():
            minutes = minutes_form.save(commit=False)
            minutes.author = request.user
            minutes.save()
            minutes_form.save_m2m()
            return redirect('pd:agenda_list')
        return render(request, 'pd/minutes_form.html', {'form': minutes_form})

forms.py

class MinutesForm(BetterModelForm):

    def __init__(self, *args, **kwargs):
        super(MinutesForm, self).__init__(*args, **kwargs)
        self.fields['participants'].required = False

    participants = UserModelMultipleChoiceField(queryset=UserProfile.objects.all(), widget=forms.CheckboxSelectMultiple())

    class Meta:
        model = Minutes

        fieldsets = (
            ("Minutes", {"fields": ["participants","minutes"]}),
        )

models.py

class Minutes(models.Model):
    agenda = models.OneToOneField(
        Agenda,
        on_delete=models.CASCADE,
        primary_key=True,
    )
    published = models.DateTimeField(verbose_name='Minutes Published', auto_now_add=True)
    edited = models.DateTimeField(verbose_name='Last Modified', auto_now=True)
    author = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="minutes_author", db_index=True, blank=True)
    participants = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="participants")
    minutes = models.TextField(blank=True)

stacktrace

Environment:


Request Method: POST
Request URL: http://localhost:8000/committee/agenda/2016/6/22/minutes/add

Django Version: 1.8
Python Version: 2.7.11
Installed Applications:
('flat',
 'django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'pdpauth',
 'pd',
 'bootstrap3',
 'recurrence',
 'mail_templated',
 'django_navtag',
 'debug_toolbar')
Installed Middleware:
(u'debug_toolbar.middleware.DebugToolbarMiddleware',
 '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',
 'rollbar.contrib.django.middleware.RollbarNotifierMiddleware')


Traceback:
File "/Users/ryancastner/Code/pdpsite/venv/lib/python2.7/site-packages/django/core/handlers/base.py" in get_response
  132.                     response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/Users/ryancastner/Code/pdpsite/venv/lib/python2.7/site-packages/django/contrib/auth/decorators.py" in _wrapped_view
  22.                 return view_func(request, *args, **kwargs)
File "/Users/ryancastner/Code/pdpsite/venv/lib/python2.7/site-packages/django/views/generic/base.py" in view
  71.             return self.dispatch(request, *args, **kwargs)
File "/Users/ryancastner/Code/pdpsite/venv/lib/python2.7/site-packages/django/utils/decorators.py" in _wrapper
  34.             return bound_func(*args, **kwargs)
File "/Users/ryancastner/Code/pdpsite/venv/lib/python2.7/site-packages/django/contrib/auth/decorators.py" in _wrapped_view
  22.                 return view_func(request, *args, **kwargs)
File "/Users/ryancastner/Code/pdpsite/venv/lib/python2.7/site-packages/django/utils/decorators.py" in bound_func
  30.                 return func.__get__(self, type(self))(*args2, **kwargs2)
File "/Users/ryancastner/Code/pdpsite/pd/views.py" in dispatch
  410.         return super(MinutesCreate, self).dispatch(*args, **kwargs)
File "/Users/ryancastner/Code/pdpsite/venv/lib/python2.7/site-packages/django/views/generic/base.py" in dispatch
  89.         return handler(request, *args, **kwargs)
File "/Users/ryancastner/Code/pdpsite/pd/views.py" in post
  422.             minutes_form.save_m2m()
File "/Users/ryancastner/Code/pdpsite/venv/lib/python2.7/site-packages/django/forms/models.py" in save_m2m
  102.                 f.save_form_data(instance, cleaned_data[f.name])
File "/Users/ryancastner/Code/pdpsite/venv/lib/python2.7/site-packages/django/db/models/fields/related.py" in save_form_data
  2576.         setattr(instance, self.attname, data)
File "/Users/ryancastner/Code/pdpsite/venv/lib/python2.7/site-packages/django/db/models/fields/related.py" in __set__
  1259.         manager = self.__get__(instance)
File "/Users/ryancastner/Code/pdpsite/venv/lib/python2.7/site-packages/django/db/models/fields/related.py" in __get__
  1242.             through=self.field.rel.through,
File "/Users/ryancastner/Code/pdpsite/venv/lib/python2.7/site-packages/django/db/models/fields/related.py" in __init__
  874.                                  (instance, source_field_name))

Exception Type: ValueError at /committee/agenda/2016/6/22/minutes/add
Exception Value: "<Minutes: Minutes object>" needs to have a value for field "minutes" before this many-to-many relationship can be used.

post data

Request information

GET

No GET data

POST

Variable    Value

csrfmiddlewaretoken u'W7tWohIGT1YsMR45pbGhuK5I6VbdIv9m'

minutes                 u'<p>testing this out</p>'

participants            u'17'

with a print statement to prove minutes.minutes has the posted value

[23/Jun/2016 13:08:04]"GET /committee/agenda/2016/6/22/minutes/add HTTP/1.1" 200 25470
minutes object's minutes:
<p>testinga askdlj;slkdje</p>
[23/Jun/2016 13:08:14]"POST /committee/agenda/2016/6/22/minutes/add HTTP/1.1" 500 148995

class MinutesCreate(LoginRequiredMixin, View):
    @method_decorator(permission_required('pd.add_agenda'))
    def dispatch(self, *args, **kwargs):
        return super(MinutesCreate, self).dispatch(*args, **kwargs)

    def get(self, request, **kwargs):
        minutes_form = MinutesForm()
        return render(request, "pd/minutes_form.html", {"form": minutes_form, "kwargs": kwargs})

    def post(self, request, **kwargs):
        minutes_form = MinutesForm(request.POST)
        if minutes_form.is_valid():
            minutes = minutes_form.save(commit=False)
            minutes.author = request.user
            print "minutes object's minutes:\n", minutes.minutes # new print that is shown above
            minutes.save()
            minutes_form.save_m2m()
            return redirect('pd:agenda_list')
        return render(request, 'pd/minutes_form.html', {'form': minutes_form})

Upvotes: 0

Views: 2619

Answers (1)

Ryan Castner
Ryan Castner

Reputation: 1054

Update & Answer

The issue was due to me using BetterModelForm Django package. BetterModelForm divides forms into fieldsets to allow for repeatable or multiple forms on the same page. As a result the method of save_m2m() was not propagating through on the form but on a formset which required a different way to call save_m2m().

I fixed this by converting my BetterModelForm classes to standard forms.ModelForm and when the fieldsets were removed everything worked as expected.

NOTE** I also was providing a UserProfile queryset to my form and my model accepted a User object. I updated the code to provide the correct queryset to my form as well.

Updated code is as follows:

forms.py

class AgendaForm(forms.ModelForm):
    time_fmt = ["%I:%M %p"]
    date_fmt = ["%Y/%m/%d"]

    start = forms.SplitDateTimeField(label="Meeting Start", input_time_formats=time_fmt,
                                     input_date_formats=date_fmt,
                                     widget=forms.widgets.SplitDateTimeWidget(date_format=date_fmt[0], time_format=time_fmt[0]))
    end = forms.SplitDateTimeField(label="Meeting End", input_time_formats=time_fmt,
                                     input_date_formats=date_fmt,
                                     widget=forms.widgets.SplitDateTimeWidget(date_format=date_fmt[0], time_format=time_fmt[0]))

    location = forms.CharField(initial='TBD')

    class Meta:
        model = Agenda
        fields = ["start", "end", "location", "announcements", "agenda"]


class MinutesForm(forms.ModelForm):
    participants = UserModelMultipleChoiceField(queryset=User.objects.all(), widget=forms.CheckboxSelectMultiple())

    class Meta:
        model = Minutes
        fields = ["participants","minutes"]

views.py

class MinutesCreate(LoginRequiredMixin, View):
    @method_decorator(permission_required('pd.add_agenda'))
    def dispatch(self, *args, **kwargs):
        return super(MinutesCreate, self).dispatch(*args, **kwargs)

    def get(self, request, **kwargs):
        minutes_form = MinutesForm()
        return render(request, "pd/minutes_form.html", {"form": minutes_form, "kwargs": kwargs})

    def post(self, request, **kwargs):
        minutes_form = MinutesForm(request.POST)
        if minutes_form.is_valid():
            minutes = minutes_form.save(commit=False)
            minutes.author = request.user
            ag = get_object_or_404(Agenda, start__year=kwargs.get('year'),
                               start__month=kwargs.get('month'),
                               start__day=kwargs.get('day'))
            minutes.agenda = ag
            minutes.save()
            minutes_form.save_m2m()
            return redirect('pd:agenda_detail', **kwargs)
        return render(request, 'pd/minutes_form.html', {'form': minutes_form})

Upvotes: 1

Related Questions