title: django formset: it is always trying to create when the form is updated
body: <p>I have been Googling around for this question for quite long. And sadly no luck. Indeed I found questions like <a href="https://stackoverflow.com/questions/11683043/django-modelformset-creates-new-record-instead-of-updating-existing-one">SO question</a> and <a href="https://stackoverflow.com/questions/3339986/formset-creating-new-entries-instead-of-updating">SO question</a>. I would say I found myself having a very similar question to the first one. However, I do not understand the discussions under that question.</p>
  
  <p>So first of all, to make it clear, I did not even get formset.is_valid() to return True--that is the problem. There are indeed uniqueness check but my edit should be able to pass that check in the database level.</p>
  
  <p>And here comes my code:</p>
  
  <pre><code>### models.py
  class DataAccess(models.Model):
      user = models.ForeignKey(User, models.CASCADE)
      project_data = models.ForeignKey(ProjectData, models.CASCADE)
      role = models.CharField(
          max_length=2, choices=ROLES, default='N'
      )
      created_at = models.DateTimeField(auto_now=True)
      updated_at = models.DateTimeField(auto_now=True)
  
      class Meta:
          unique_together = (('user', 'project_data', 'role'),)
          # query from project data should happen more
          index_together = [['project_data', 'user', 'role']]
  
      def __str__(self):
          return '{}&amp;{}&amp;{}'.format(self.user, self.project_data, self.role)
  
      def save(self, *args, **kwargs):
          curr = datetime.now()
          if not self.created_at:
              self.created_at = curr
          self.updated_at = curr
          return super(DataAccess, self).save(*args, **kwargs)
  
  
  class DataSyncPath(models.Model):
      server = models.ForeignKey(Server, models.CASCADE)
      project_data = models.ForeignKey(ProjectData, models.CASCADE)
      path = models.CharField(max_length=128)
      created_at = models.DateTimeField(auto_now=True)
      updated_at = models.DateTimeField(auto_now=True)
  
      class Meta:
          unique_together = (('server', 'project_data',),)
          index_together = [['project_data', 'server']]
  
      def __str__(self):
          return '{}&amp;{}'.format(self.server, self.project_data)
  
      def save(self, *args, **kwargs):
          curr = datetime.now()
          if not self.created_at:
              self.created_at = curr
          self.updated_at = curr
          return super(DataSyncPath, self).save(*args, **kwargs)
  
  ### forms.py
  class DataAccessForm(forms.ModelForm):
      def __init__(self, *args, **kwargs):
          super(DataAccessForm, self).__init__(*args, **kwargs)
          helper = FormHelper()
          helper.template = 'bootstrap/table_inline_formset.html'
          self.helper = helper
          self.helper.form_tag = False
  
      class Meta:
          model = DataAccess
          exclude = ['created_at', 'updated_at']
  
  
  def get_data_access_formset(initial=[], extra=3):
      return inlineformset_factory(
          ProjectData, DataAccess,
          form=DataAccessForm, extra=extra + len(initial)
      )
  
  
  class DataSyncPathForm(forms.ModelForm):
      def __init__(self, *args, **kwargs):
          super(DataSyncPathForm, self).__init__(*args, **kwargs)
          helper = FormHelper()
          helper.template = 'bootstrap/table_inline_formset.html'
          self.helper = helper
          self.helper.form_tag = False
  
      class Meta:
          model = DataSyncPath
          exclude = ['created_at', 'updated_at']
  
      def clean(self):
          super(DataSyncPathForm, self).clean()
          if (self.cleaned_data['id'] is None and 'path' in self.errors and
                  self.errors['path'][0] == 'This field is required.'):
              # a special hack to silent path error
              self.errors.pop('path')
  
  
  def get_data_sync_path_formset(initial=[], extra=0):
      return inlineformset_factory(
          ProjectData, DataSyncPath, can_delete=False,
          form=DataSyncPathForm, extra=extra + len(initial)
      )
  
  ### views.py, import and everything is taken care of already
  class ProjectDataUpdateView(LoginRequiredMixin, UpdateView):
      login_url = reverse_lazy('login')
      redirect_field_name = 'redirect_to'
      template_name = 'project_data/project_data_form.html'
      form_class = ProjectDataForm
      context_object_name = 'project_data'
      pk_url_kwarg = 'project_data_id'
      success_url = reverse_lazy('data_projects:project_data_list')
  
      def get_queryset(self):
          return dbmanager.get_project_data_by_user(
              self.request.user
          )
  
      def get_initial(self):
          initial = super(ProjectDataUpdateView, self).get_initial()
          try:
              initial['phabricator_links'] = ','.join(json.loads(
                  self.object.phabricator_links))
          except Exception:
              initial['phabricator_links'] = ''
          return initial
  
      def get_data_access_initial(self):
          data_access_queryset = self.object.dataaccess_set.all()
          data_access_initial = [
              model_to_dict(obj) for obj in data_access_queryset
          ]
          return data_access_initial
  
      def get_data_sync_path_initial(self):
          datasyncpath_set = self.object.datasyncpath_set.all()
          sync_path_initial = [
              model_to_dict(obj) for obj in datasyncpath_set
          ]
          servers = self.request.user.server_set.all()
          missing_servers = set(
              [server.id for server in servers]) - set(
              [obj['server'] for obj in sync_path_initial]
          )
          for server_id in missing_servers:
              sync_path_initial.append({'server': server_id})
          return sync_path_initial
  
      def process_context_data(self, data):
          data_access_initial = self.get_data_access_initial()
          sync_path_initial = self.get_data_sync_path_initial()
          if self.request.POST:
              if (self.request.user.is_superuser or
                      self.request.user.groups.filter(name='Manager').exists()):
                  data['data_access_formset'] = get_data_access_formset()(
                      self.request.POST, self.request.FILES,
                      instance=self.object, initial=data_access_initial,
                      queryset=self.object.dataaccess_set.all()
                  )
              data['data_sync_path_formset'] = get_data_sync_path_formset()(
                  self.request.POST, self.request.FILES,
                  instance=self.object, initial=sync_path_initial,
                  queryset=self.object.datasyncpath_set.all()
              )
          else:
              data['data_access_formset'] = get_data_access_formset(
                  initial=data_access_initial
              )(initial=data_access_initial)
              data['data_sync_path_formset'] = get_data_sync_path_formset(
                  initial=sync_path_initial
              )(initial=sync_path_initial)
          return data
  
      def get_context_data(self, **kwargs):
          data = super(ProjectDataUpdateView, self).get_context_data(**kwargs)
          data_access_formset = kwargs.pop('data_access_formset', None)
          data_sync_path_formset = kwargs.pop('data_sync_path_formset', None)
          if data_access_formset and data_sync_path_formset:
              data['data_access_formset'] = data_access_formset
              data['data_sync_path_formset'] = data_sync_path_formset
          else:
              data = self.process_context_data(data)
          data['is_data_sync_path_set'] = True
          return data
  
      def post(self, request, *args, **kwargs):
          self.object = self.get_object()
          if not (self.request.user.is_superuser or
                  self.request.user.groups.filter(name='Manager').exists()):
              self.request.POST['name'] = self.object.name
              self.request.POST['dmgr'] = self.object.dmgr
          form_class = self.get_form_class()
          form = self.get_form(form_class)
          context_data = self.get_context_data()
          data_access_formset = context_data.get('data_access_formset')
          sync_path_formset = context_data.get('data_sync_path_formset')
          if (form.is_valid() and
                  data_access_formset.is_valid() and
                  sync_path_formset.is_valid()):
              return self.form_valid(
                  form, data_access_formset, sync_path_formset)
          else:
              return self.form_invalid(
                  form, data_access_formset, sync_path_formset)
  
      def form_valid(
              self, form, data_access_formset, sync_path_formset, **kwargs):
          for deleted_form in data_access_formset.deleted_forms:
              try:
                  deleted_form.cleaned_data['id'].delete()
              except Exception:
                  pass
          self.object = form.save()
          data_access_formset.save()
          sync_path_formset.save()
          return HttpResponseRedirect(self.get_success_url())
  
      def form_invalid(
              self, form, data_access_formset, sync_path_formset, **kwargs):
          return self.render_to_response(
              self.get_context_data(
                  form=form, data_access_formset=data_access_formset,
                  sync_path_formset=sync_path_formset
              )
          )
  </code></pre>
  
  <p>I have also put breakpoint in post method of the UpdateView class:</p>
  
  <pre><code>ipdb&gt; sync_path_formset.is_valid()
  False
  ipdb&gt; sync_path_formset.forms[0]
  &lt;DataSyncPathForm bound=True, valid=False, fields=(server;project_data;path;id;DELETE)&gt;
  ipdb&gt; sync_path_formset.forms[0].cleaned_data
  {'DELETE': False, 'project_data': &lt;ProjectData: bbb&gt;, 'path': 'that', 'server': &lt;Server: jgu-pc&gt;, 'id': &lt;DataSyncPath: jgu-pc&amp;bbb&gt;}
  ipdb&gt; sync_path_formset.forms[0].instance
  &lt;DataSyncPath: jgu-pc&amp;bbb&gt;
  ipdb&gt; sync_path_formset.forms[0].save(commit=False)
  *** ValueError: The DataSyncPath could not be created because the data didn't validate.
  ipdb&gt; sync_path_formset.forms[0].has_changed()
  True
  </code></pre>
  
  <p>So clearly the instance exists. But it just tries to create another instance anyway.</p>