![]() |
version seven.   http://demongin.org |
Bit.ly, Twitter and Django: How to Automatically Tweet Blog Posts the Python Way
A how-to with commented code samples: a post-mortem on a scheme I recently used to implement automatic Twitter posts on a Django site.
Friday, 2010-01-29 | Django, Programming
| He said part of it was he'd have shot him because he was Tom Hanks. I think Oliver just would like to shoot Tom Hanks anyway, just to see which way he'd fall. |
| John "Charlie Don't Surf" Milius |
The easiest way to automatically tweet about your latest blog post is, in my experience, to just install Wordpress, download a plug-in or two and (beyond the occasional update) never think about it again.
But that's not much fun. And it definitely presents very few (if any) opportunities to write python code or play around with the Twitter API.
Developing your own blog in Django, using subversion for version control and working in a Linux environment, however, affords all of those opportunities to "make cool shit" and more. What follows, then, is one way that you might set up a blog that automatically tweets about new posts.
blog/models.py
So, assuming that you've run manage.py startapp blog and created a blog application in the root of your Django project, the first thing you'll need is a basic "blog" model. Since you might also want a model (in that same models.py file, probably) where you save information about your tweets, I'll suggest a way that such a model might look. You, perhaps, will want this for your own records and for accessing data about your blog in a programmatic way without getting Twitter or their API involved.Here's how your "blog" class might look:
class Blog(models.Model): title = models.CharField(max_length=666) body = models.TextField() published = models.BooleanField('Published?', default=False) published_on = models.DateTimeField('Published Date', null=True) def __unicode__(self): return self.title def get_absolute_url(self): return "http://demongin.org/blog/%s" % self.id def save(self): # Logic for setting published_on if self.published and self.published_on == None: self.published_on = datetime.datetime.now() # Logic for un-setting published_on if self.published == False and self.published_on != None: self.published_on = None super(Essay, self).save()
What's truly remarkable is the save() function. Basically, when you overwrite the save() function in this way, you're allowed to mess with an object before it gets saved by the database. This will be crucial during the part of the code that decides whether or not to automatically tweet our newly published post. But I'll come to that in a bit.
Next, here's how the model that saves information about your automatic tweets could look:
class BlogTweet(models.Model): blog = models.ForeignKey(Essay, null=True) tweet = models.CharField(max_length=140) bitly_url = models.URLField() twitter_status_id = models.CharField(max_length=15) def __unicode__(self): return self.tweet
- The blog attribute relates to objects created by your Blog class. Simple.
- tweet is just a CharField 140 characters in length: you'll use it to save the actual strings you send along to Twitter.
- bitly_url is where you'll save the shortened URL's returned by the bit.ly API. More on that later.
- Finally, twitter_status_id is a CharField where you'll save the "status id" values associated with a tweet that you can get from Twitter: you can use these to format links. For example, if you have a "status id" and a username, you can synthesize a static link to a page:This gives us the URL http://twitter.com/timothyoconnell/statuses/7796321094, which is a static URL for a single tweet.
username = "timothyoconnell" twitter_status_id = "7796321094" static_url = "http://twitter.com/%s/statuses/%s" % (username, twitter_status_id)
blog/admin.py
The next thing you're going to do, once you've got your models for saving "blog" posts and information about automatic tweets, is dig into the "admin.py" for your blog application. This is where most of the magic is going to happen: in this file, we're going to use the special save_model() method of Django's ModelAdmin class to check a blog post before saving it and decide whether or not to tweet about it.I call a few functions from a module called helper in the code sample below, and I'll explain that later. For now take a look at the ModelAdmin class for objects of the Blog class I created above:
from project_root import helper from project_root.blog.models import Blog, BlogTweet class BlogAdmin(admin.ModelAdmin): list_display = ('title', 'body', 'published') fieldsets = [ (None, { 'fields': [('title', 'body', 'published')] } ), ] # Over-write save_model function to automatically set # author and tweet, if indicated, recording the tweet def save_model(self, request, obj, form, change): if helper.tweet_logic(obj) == True: tweet_str, short_url, status_id = helper.tweet( obj.title.encode(), # Gotta encode() this back to ascii, # b/c it's a unicode obj. obj.get_absolute_url(), # Call this function; # we want the string it returns ) BlogTweet.objects.create( blog = obj, tweet = tweet_str, bitly_url = short_url, twitter_status_id = status_id, ) obj.save() admin.site.register(Blog, BlogAdmin)
- We get an interface that presents us with a small textfield for title, a big textfield for body and a checkbox for published.
- Assuming we fill in all three of those blanks (i.e. checking the check box), Django will take that data and create a Blog object. Before it saves that object to the database, it'll run everything in the save_model() function.
- The save_model function, then, is where we want to do everything Twitter-related. As you can see, I've got some simple conditional logic that goes like this:
- if helper.tweet_logic() comes back true:
- execute helper.tweet(), passing it the Blog object's title and get_absolute_url values; take the values it returns and instantiate the variables tweet_str, short_url and status_url
- Once those variables are populated, use them and our new Blog object to create a new BlogTweet object.
- Finally, save the new Blog object we're trying to create
helper.py
For the sakes of both modularity and clarity, I like to create a file called "helper.py" in the root of my Django projects that contains simple unit tests and functions that I can conveniently call from my "models.py" or "views.py" or whichever piece of whichever application in my project.Here's what "helper.py" needs to do to support the code above:
# # TWITTER! # from project_root import bitly, twyt # These non-standard imports # are explained below from bitly import bitly from twyt import twitter, data import json def tweet_logic(obj): """ Takes an object and does some tests. Saves the object (obj) before returning results just to make sure that the object has an id attribute. Finally returns a bool regarding whether or not tweeting is indicated. Note: requires objects with the attributes "published" and "published_on". """ p = getattr(obj, 'published') p_on = getattr(obj, 'published_on') # The "Blog" object hasn't been saved yet, since we modified its save() method # and we're running this tweet_logic() function during the overwritten # save_model() method for this class. What this means is that the # "published_on" attribute is still NULL/None even though we can evaluate the # user-submitted "published" attribute if (p == True and p_on == None): tw = True else: tw = False obj.save() # now, save the object here, since it's been tested accurately: # we need to save here to make sure that the object gets an # "id" value, which only happens during the save() method return tw def tweet(title, url, rejoinder=False): """ Use the bit.ly API to get a short URL and then use an svn:externals of twyt to tweet. """ # First, process title (truncate if necessary) max = 111 # The longest a post can be (with two single quotes) before it and the # bit.ly URL we're going to create for it cause Twitter to # automatically truncate our post. Bogus. length = len(title) if length > max: title = title[:(max-3)].strip() + "..." # bit.ly b = bitly.Api(login='toconnell', apikey='x_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') short_url = b.shorten(url) # twitter # 1.) make the tweet tweet_string = "'%s': %s" % (title, short_url) twitter_user = "timothyoconnell" twitter_pass = "xxxxxxxxxxxxx" # 2.) authenticate with twitter and update our status t = twitter.Twitter() t.set_auth(twitter_user, twitter_pass) t.status_update(tweet_string) # 3.) get status id from a json object that we'll get from twyt timeline = t.status_user_timeline(count=1) timeline = json.loads(timeline) twitter_status_id = str(timeline[0]["id"]) return tweet_string, short_url, twitter_status_id if __name__ == "__main__": print "Don't execute this."
Once our application has decided whether or not to tweet, the tweet() function it executes is pretty straightforward. There's some basic truncation for the title of our post (which is what we're choosing to tweet), a super-simple call to the bit.ly API and then a quick tweet using twyt.
And, speaking of twyt, you may have noticed that we imported some projects that aren't part of python's standard library and probably aren't available as packages on your distro. In the interest of completeness, here's a short explanation of what's going on with that.
svn:externals
The best (i.e. simplest, because who really wants to waste time dicking around with something that's supposed to be as simple and carefree as bit.ly) wrapper for the bit.ly in python is, in my opinion, Yoav Aviram's bit.ly API. Its developer makes his code available via public SVN repository, so getting it is easy.Andy Price's twyt, however, doesn't have a public SVN repo (that I know about), because its developer uses bazaar. My solution for this has been simply to manually create and update a twyt project in my own personal SVN repo and use it as needed.
Here's some sample syntax for setting up the svn:externals mentioned in the code above:
toconnell@esme:~/project_root$ svn propedit svn:externals .
bitly http://python-bitly.googlecode.com/svn/trunk/ twyt https://myprivatesvnrepo.com/toc/twyt/twyt-0.9.2/twyt
Good hunting.
