Reputation: 228
I have a website using Django. Each post is an object called Article. I want to retrieve the post's HTML after saving it so I wrote the following post_save hook:
@receiver(models.signals.post_save, sender=Article)
def _send_article_mentions(sender, instance, **kwargs):
import requests
from django.contrib.sites.models import Site
from urlparse import urljoin
from ParallelTransport.settings import ARTICLES_URL
SITE_URL = 'http://'+Site.objects.get_current().domain
article_url = urljoin( SITE_URL, instance.get_absolute_url() )
import time
time.sleep(20)
r = requests.get(article_url)
error_file = open(ARTICLES_URL+'/'+'error.txt','w')
error_file.write('file started1\n')
m = r.status_code
error_file.write(str(m))
error_file.close()
It basically waits for 20s (added as a test) then tries to retrieve the HTML of the post using its URL, and writes the request status code to a file for debugging.
The problem is I always get status = 404 on the first save, it does work on 2nd and subsequent saves. I thought the way Django works would be, in order:
But then I should be able to retrieve the HTML in post_save. Am I understanding post_save incorrectly?
Added notes:
Upvotes: 7
Views: 6724
Reputation: 1380
Although this is a completely wrong approach(*), the problem is probably in database transactions. Current thread saves the article but within this uncommited transaction you are trying to get these data through another thread (through web server). In that case, this behaviour is fully correct. Either you need to commit before retrieving through another thread or get the HTML by another way.
(*) should be done asynchronously on the background (Celery or other more lightweight async queue app) or you can call the view directly if you want to get the HTML (depending on your view, you may have to forge the request; if too complicated, you can create a helper function that cherrypicks minimal code to render the template). If you only need to call a 3rd party API after you save something, you want to do it asynchronously. If you don't do it, the success of your "save() code" will depend on the availability of your connection or the 3rd party service and you will need to deal with transactions on place where you won't to deal with transactions ;)
Upvotes: 4
Reputation: 78
Edit 2:
I have created a simple example (using Django 1.5.5) to test whether this works as intended. As far as I can tell, it does. pre_save fires before a database commit and post_save fires after.
Example detailed:
Two example models.
Article is used for triggering signals.
ArticleUrl is used to log responses from Article.get_absolute_url().
# models.py
from django.db import models
from django.core.urlresolvers import reverse
class Article(models.Model):
name = models.CharField(max_length=150)
def get_absolute_url(self):
"""
Either return the actual url or a string containing '404' if reverse
fails (Article is not saved).
"""
try:
return reverse('article:article-detail', kwargs={'pk': self.pk})
except:
return '404'
class ArticleUrl(models.Model):
article_name = models.CharField(max_length=150)
url = models.CharField(max_length=300)
def __unicode__(self):
return self.article_name + ': ' + self.url
Example views.py and urls.py were omitted as they are simple. I can add them if needed.
# views.py, url.py
Creating pre_save and post_save signals for Article:
# signals.py
from django.db.models import signals
from django.dispatch import receiver
from article.models import Article, ArticleUrl
@receiver(signals.pre_save, sender=Article)
def _log_url_pre(sender, instance, **kwargs):
article = instance
article_url = ArticleUrl(
article_name = 'pre ' + article.name,
url = article.get_absolute_url()
)
article_url.save()
@receiver(signals.post_save, sender=Article)
def _log_url_post(sender, instance, **kwargs):
article = instance
article_url = ArticleUrl(
article_name = 'post ' + article.name,
url = article.get_absolute_url()
)
article_url.save()
Importing my signals.py so Django can use it:
# __init__.py
import signals
After defining the above I went ahead and created a new Article in Django shell (python.exe manage.py shell).
>>> from article.models import *
>>> a = Article(name='abcdd')
>>> a.save()
>>> ArticleUrl.objects.all()
[<ArticleUrl: pre abcdd: 404>, <ArticleUrl: post abcdd: /article/article/8>]
The above example does seem to show that pre_save did indeed not return a url, but post_save did. Both appear to be behaving as intended.
You should check whether your code either deviates from the above example or interferes with program execution in some way.
Edit 1:
Having said that (below), according to What Happens When You Save, the post_save signal should run after a database save.
Could there be other parts of your app/site somehow interfering with this?
Original post:
According to the Django documentation, the post_save signal is sent at the end of save(), not after it.
As far as I understand, Django Signals are synchronous (in-process) so they would stall the actual save(). It won't fully complete until the signals are done.
This isn't always applicable, but have you considered a custom signal you could call after save()?
Upvotes: 0
Reputation: 41
I had similar problem caused probably by the same issue (Asked differently, https://plus.google.com/u/0/106729891586898564412/posts/Aoq3X1g4MvX). I did not solve it in a proper way, but you can try playing with the database cache, or (seen it in another django database problem) close all of the database connections and requery.
Upvotes: 0
Reputation: 566
Have you tried overriding the object's save method, calling super, waiting and then trying to retrieve the HTML? Also are you using the development server? It may have issues handling the second request while the first one is still going. Maybe try it on a proper server?
Upvotes: 0