2

Update:

For anyone curious, I figured out what and why and how to fix it. In my view I had: fields = ['html', 'tags', 'title', 'text', 'taken_date', 'image'] And am using {{ form.as_p }} in my template. Apparently once that gets posted from the form it really, really doesn't want anything else touching the form fields that wasn't already in the form. So I took out the 'tags' field from my view and it works.

Thanks to everyone that responded.

Original Question:

Using Django 2.0.1 and PostgreSQL 9.2.18

I'm writing a simple photogallery application. In it I have a photo object and PhotoTag object. The photo can have many tags and the tags can be associated with many photos, thus it needing to be a ManyToManyField.

Upon save of the submitted photo, a post_save receiver calls functions to make thumbnails (which work fine) and a function to update tags.

The photo gets saved fine, update_tags gets called fine, tags get read from the photo fine, tags get saved into PhotoTag fine. But the manytomany table tying the two together does not get the new rows inserted. Unless the code exits abnormally during either the update_tags function or the post_save receiver function, thumbs after update_tags is called.

I've even tried using a connection.cursor to write directly into the m2m table and it has the same behavior.

If I try to call save() on the Photo object again, I just get into an infinite loop due to the post_save signal.

I'm baffled as to what is going on. Any clues?

# models.py

def update_tags(instance):
    tags = get_tags(instance.image)

    # Set initial values
    pt = []
    tagid = ''
    photoid = instance.id

    # Loop through tag list and insert into PhotoTag and m2m relation
    for x in range(0, len(tags)):
        # Make sure this tag doesn't already exist
        if PhotoTag.objects.filter(tag_text=tags[x]).count() == 0:
            pt = PhotoTag.objects.create(tag_text=tags[x])
            tagid = PhotoTag.objects.latest('id').id
            instance.tags.add(pt)
        else:
            # Only working with new tags right now
            pass

    return


class Photo(models.Model):
    author = models.ForeignKey(settings.AUTH_USER_MODEL,
                               on_delete=models.CASCADE)
    title = models.CharField(max_length=200, null=True, blank=True)
    text = models.TextField(null=True, blank=True)
    html = models.BooleanField(default=False)
    filename = models.CharField(default='', max_length=100, blank=True,
                                null=True)
    image = models.ImageField(upload_to=upload_path)
    location = models.CharField(max_length=100, blank=True, null=True)
    entry_date = models.DateTimeField(default=timezone.now)
    taken_date = models.DateTimeField(blank=True, null=True)

    tags = models.ManyToManyField(PhotoTag, blank=True)


@receiver(post_save, sender=Photo)
def thumbs(sender, instance, **kwargs):
    """
    Upon photo save, create thumbnails and then
    update PhotoTag and m2m with any Exif/XMP tags
    in the photo.
    """

    mk_thumb(instance.image, 'mid')
    mk_thumb(instance.image, 'th')
    mk_thumb(instance.image, 'sm')

    update_tags(instance)

    return

-------------
From views.py
-------------

class PhotoCreate(LoginRequiredMixin, CreateView):
    model = Photo
    template_name = 'photogallery/photo_edit.html'
    fields = ['html', 'tags', 'title', 'text', 'taken_date', 'image']

    def get_initial(self):
        self.initial = {'entry_date': timezone.now()}
        return self.initial

    def form_valid(self, form):
        form.instance.author = self.request.user

        return super(PhotoCreate, self).form_valid(form)

Update:

def save(self, mkthumb='', *args, **kwargs):
      super(Photo, self).save(*args, **kwargs)
      if mkthumb != "thumbs":
          self.mk_thumb(self.image, 'mid')
          self.mk_thumb(self.image, 'th')
          self.mk_thumb(self.image, 'sm')

          self.update_tags()

          mkthumb = "thumbs"

      return
Cynnyr
  • 21
  • 1
  • 4
  • Maybe off-topic, but any reason to use signals rather than overriding `save()` model method? It's a lot easier and clearer to me, and would probably help to debug this kind of problem. – David Dahan Jan 25 '18 at 19:07
  • I had thought about it, but I'm not sure that I could get the ordering right as both making the thumbnails and reading the tags need to come after the photo has been saved to the db and to the disk. Maybe I'll look into seeing what can be done by overriding the model's save() method. – Cynnyr Jan 25 '18 at 19:13
  • When overriding `save` method, you can chose if your actions occurs before or after the `save`, just writing your code before of after the `super()`. It will be a lot easier to read and debug after that. I'm pretty sure you'll find the problem on your own after that. – David Dahan Jan 25 '18 at 19:14
  • Nope, same behavior. The relation is being built, at the end of update_tags() I can put in `print(instance.tags.all())` or `print(self.tags.all())` depending on implementation and it outputs the correct tags. – Cynnyr Jan 25 '18 at 20:18
  • Can you edit yout post with the `save` version of your code please? – David Dahan Jan 25 '18 at 20:30
  • I mean...replace the original post, using edit feature :D This way it's impossible to read ^^ – David Dahan Jan 25 '18 at 20:41
  • what error are you getting ? – Sumeet Kumar Jan 25 '18 at 21:13
  • No error. Relation being created, but not written to/committed to db. – Cynnyr Jan 25 '18 at 21:16

2 Answers2

1

I had a similar issue where I was trying to add a group when a user instance was saved.

The anwer why this is happening is at the docs and more explicitly (using code) at this ticket.

When saving a ModelForm() (hitting save in the admin), first an instance of the object is saved, then all its signals are triggered etc. The third step is to save all m2m relations using ModelForm().cleaned_data. If ModelForm().cleaned_data['tags'] is None, all the relations created from your signal, will be deleted.

  • A hackish solution, is to use a post_save signal with transaction.on_commit() which will execute the relevant code after the existing transaction (which includes the procedure of saving all m2m relations) is committed to the database.

    def on_transaction_commit(func):
        ''' Create the decorator '''
        def inner(*args, **kwargs):
            transaction.on_commit(lambda: func(*args, **kwargs))
    
        return inner
    
    
    @receiver(post_save, sender=Photo)
    @on_transaction_commit
    def tags(instance, raw, **kwargs):
        """
        Create the relevant tags after the transaction 
        of instance is committed, unless the database is 
        populated with fixtures. 
        """
        if not raw:
            update_tags(instance)
    
  • A more sound solution if your many to many relation has not blank=True is to use a m2m_changed() signal, as explained in this post or the before mentioned ticket.

  • The best of all, is to ditch the signals and override the ModelForm().clean() method for the case where a ModelForm() is used, and also override the Model().save() method in case the model is directly saved.

    A ModelForm().instance.my_flag will be useful so you can check for an existing Model().my_flag in Model().save() to avoid accessing twice the database.

raratiru
  • 8,748
  • 4
  • 73
  • 113
0

override your save method like

def save(self, *args, **kwargs):
    tags = get_tags(self.image)

    # Set initial values
    pt = None

    # Loop through tag list and insert into PhotoTag and m2m relation
    for x in range(0, len(tags)):
        # Make sure this tag doesn't already exist
        if PhotoTag.objects.filter(tag_text=tags[x]).count() == 0:
            pt = PhotoTag.objects.create(tag_text=tags[x])
            self.tags.add(pt)
        else:
            # Only working with new tags right now
            pass
      super(Photo, self).save(*args, **kwargs)
Sumeet Kumar
  • 969
  • 1
  • 11
  • 22