Djangoを使って、PubHubSubbubに対応したレスポンスを返す方法

Djangoを使って、PubSubHubbub(PuSH)用のレスポンスを実装する方法を紹介します。PubSubHubbubを実装することで、Googleになどに、サイトの更新を伝えることができます。

PubSubHubbubについては、このブログがわかりやすいと思います。

実装について

PubSubHubbubの送信先サーバにある記述を見ると、Atom形式のフォーマットを拡張すればPusSubHubbub形式のフォーマットにできるみたいです。

Add an //atom:link tag under //atom:feed for Atom feeds or under //rss:rss/channel for RSS feeds. The //atom:link tag should have rel attribute set to hub and href attribute set to https://pubsubhubbub.appspot.com/

具体的には、Atom形式のfeedタグの下にlinkタグを置き、linkタグにhubhref属性を設定すればいいみたいです。

そのため、まずはDjangoを使ってAtom形式のフォーマットを返せるようにします。そして、それをPubSubHubbubに対応させるようにします。

実装の流れ

  1. 投稿を表示するサンプルを準備
  2. Atom形式のフィードを実装
  3. Atom形式をPubSubHubbub形式に修正

サンプルプログラムを準備

Atom軽視のフィードを実装する前に、modelなどを作成します。

project、 applicationを作成

startprojectstartappを使ってprojectとapplicationを作成します。

$ django-admin startproject myprj
$ cd myprj
$ ./manage.py startapp pushapp

modelを作成

pushapp/models.pyを編集して、投稿内容を表現するmodelを作成します。

pushapp/models.py

# coding: utf-8
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=64)
    text = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

DBにテーブルを作成

myprj/settings.pyINSTALLED_APPSに作成したapplicationを追加して、makemigrationsmigrateでDBに反映させます。

myprj/settings.py

INSTALLED_APPS = [
    ...
    'pushapp',
]

DBに反映

$ ./manage.py makemigrations
$ ./manage.py migrate 

DBにレコードを追加

shellを使って、DBにレコードを追加しておきます。

$ ./manage.py shell
>>> from pushapp.models import Post
>>> for idx in range(100):
>>>     p = Post()
>>>     p.title = 'タイトル_{}'.format(idx)
>>>     p.text = 'テキスト_{}'.format(idx) * 100
>>>     p.save()

投稿内容を表示するviewを作成

pushapp/views.pymyprj/urls.pyを編集し、投稿内容を表示できるようにします。

pushapp/views.py

# coding: utf-8
from django.shortcuts import get_object_or_404
from django.views.generic import View
from django.http import HttpResponse
from pushapp.models import Post


class PostView(View):
    def get(self, request, *args, **kwds):
        post = get_object_or_404(Post, pk=kwds['post_id'])
        return HttpResponse(post.text)

myprj/urls.py

# coding: utf-8
from django.conf.urls import url
from pushapp.views import PostView

urlpatterns = [
    url(r'^(?P<post_id>\d+)$', PostView.as_view(), name='postview'),
]

確認

runserverで開発用のサーバを立ち上げて、URLを叩いて期待通りのレスポンスが返ってくることを確認します。

$ ./manage.py runserver
$ curl http://localhost:8000/1
テキスト_0テキスト_0テキスト_0
...
テキスト_0

期待通りのレスポンスが返ってくれば、準備は完了です。

Step1. Atom形式のフィードを実装

Djangoに標準的に実装されているフィード配信用のフレームワークを使うと、Atom形式のレスポンスを簡単に実装することができます。

フィードを表示するviewを作成

このサンプルでは、pushapp下にfeeds.pyというモジュールを作成して、その中にフレームワークに従ってコードを書いていきます。なお、作成するフィードは、最新のPostを10件返すものとします。

pushapp/feeds.py

# coding: utf-8
from django.contrib.syndication.views import Feed
from django.utils.feedgenerator import Atom1Feed
from django.core.urlresolvers import reverse
from pushapp.models import Post


class LatestEntries(Feed):
    feed_type = Atom1Feed  # AtomFeedを指定します。
    title = 'フィードタイトル'
    link = '/'

    def items(self):
        # 最新の投稿10件を取得します。
        return Post.objects.all().order_by('-created_at')[:10]

    def item_title(self, obj):
        return obj.title  # objにはPostのインスタンスがか入っています。

    def item_description(self, obj):
        return obj.text[0:100]  # textの先頭100文字を返します。

    def item_link(self, obj):
        return reverse('postview', args=[obj.id])

    def item_pubdate(self, obj):
        return obj.created_at  # 作成時刻を返します。

フィード配信用フレームワークを利用するときはclass LatestEntries(Feed)のように、from django.contrib.syndication.views import Feedを継承したクラスを作成します。また、feed_type属性で、フィードの形式にAtom形式を指定しています。titlelink属性に関しては、フィードのfeedタグ直下のtitleタグとlinkタグなどを決定しています。

itemsメソッドは、feedタグ下のentryタグで使うアイテムを決定しています。また、同メソッドは10件返しているので、フィードにはentryタグが10件表示されます。

item_xxxメソッドは、各entryタグのtitleタグやpublishedタグなどを決定しています。引数のobjは、itemsで返したオブジェクトの各々が格納されています。

フィードを表示するためのURLを登録

myprj/urls.pyを修正して、/feedを叩くとAtom形式のフィードが返却されるようにします。

myprj/urls.py

# coding: utf-8
from django.conf.urls import url
from pushapp.views import PostView
from pushapp.feeds import LatestEntries

urlpatterns = [
    url(r'^(?P<post_id>\d+)$', PostView.as_view(), name='postview'),
    url(r'^feed$', LatestEntries()),
]

確認

http://localhost:8000/feedsを叩き、Atom形式のレスポンスが返ってくることを確認します。

$ curl http://localhost:8000/feed
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us">
...
</feed>

これで、Atom形式のフィード作成が完成しました。

Step2. Atom形式をPubSubHubbub形式に修正

Atom形式のフィードを作成するときに、LatestEntriesクラスでfeed_typeAtom1Feedを指定しました。

class LatestEntries(Feed):
    feed_type = Atom1Feed  # AtomFeedを指定します。

このAtom1Feedを継承したPushFeedを作成し、PushFeedでPubSubHubbubに対応するためのコードを追加します。そして、feed_type = PushFeedというように、PubSubHubbubに対応したフォーマットを使うように指定することになります。

PushFeedを作成

pushapp/feedgenerator.pyモジュールの中に、PushFeedクラスを作成します。

# coding: utf-8
from django.utils.feedgenerator import Atom1Feed


class PushFeed(Atom1Feed):
    """PubSubHubbub用のFeedクラス"""
    HUBHREF_LIST = [
        'https://pubsubhubbub.appspot.com/',
    ]

    def add_root_elements(self, handler):
        super().add_root_elements(handler)
        for hubhref in self.HUBHREF_LIST:
            handler.addQuickElement(
                'link', '', {'rel': 'hub', 'href': hubhref})

HUBHREF_LISTは、linkタグのhref属性に指定するURLを定義しています。このURLが増えていった場合を考慮して、list型で定義しています。

add_root_elementsメソッドをオーバーライドして、feedタグ下のタグを設定していきます。修正内容は、Atom形式を元にして、linkタグを1つ追加するだけです。親クラス(Atom1Feed)のadd_root_elmentメソッドを呼んで、その後にhandleraddQuickElementメソッドを使ってlinkタグを追加しています。

なお、addQuickElementメソッドの2番目の引数が空文字なのは、タグの中身を空にしておくためです。

PubSubHubub用のフィードを作成

PushFeedを作成したら、先ほどAtom形式のフィードを作成したpushapp/feeds.pyを修正します。PubSubHubbubに対応したフォーマットを返すクラスを、LatestEntriesPushとして作成します。

pushapp/feeds.py

...
from pushapp.feedgenerator import PushFeed
...


class LatestEntriesPush(LatestEntries):
    feed_type = PushFeed  # PubSubHubbubに対応した形式を指定します。

feed_type = PushFeedで、フィードの形式を、先ほど作成したPushFeedを指定します。

LatestEntriesPushの中身はLatestEntriesと殆ど一緒なので、LatestEntriesを継承しています。

URLを登録

myprj/urls.pyを修正して、PubHubSubbubに対応したレスポンスを返すURLを追加します。

# coding: utf-8
from django.conf.urls import url
from pushapp.views import PostView
from pushapp.feeds import LatestEntries, LatestEntriesPush

urlpatterns = [
    url(r'^(?P<post_id>\d+)$', PostView.as_view(), name='postview'),
    url(r'^feed$', LatestEntries()),
    url(r'^feed/push$', LatestEntriesPush()),
]

確認

/feed/pushにアクセスして、レスポンスが返ってくることを確認します。

$ curl http://localhost:8000/feed/push
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us">
  <title>フィードタイトル</title>
  <link rel="alternate" href="http://localhost:8000/"></link>
  <link rel="self" href="http://localhost:8000/feed/push"></link>
  <id>http://localhost:8000/</id>
  <updated>2016-01-04T01:26:52+00:00</updated>
  <link rel="hub" href="https://pubsubhubbub.appspot.com/"></link>
  <entry>
  ...
  </entry>
  ...
</feed>

feedタグ直下に、<link rel="hub" href="https://pubsubhubbub.appspot.com/"></link>が追加されていることが確認できます。

これで、PubSubHubbubに対応した形式で、レスポンスを返せるようになりました。

サンプルプログラム全体

今回作成した、PubSubHubbubに対応させたレスポンスを返すサンプルの全体を掲載します。※myprj/settings.pyは省略しています。

  • myprj/urls.py
  • pushapp/models.py
  • pubhapp/views.py
  • pushapp/feedgenerator.py
  • pushapp/feeds.py

myprj/urls.py

# coding: utf-8
from django.conf.urls import url
from pushapp.views import PostView
from pushapp.feeds import LatestEntries, LatestEntriesPush

urlpatterns = [
    url(r'^(?P<post_id>\d+)$', PostView.as_view(), name='postview'),
    url(r'^feed$', LatestEntries()),
    url(r'^feed/push$', LatestEntriesPush()),
]

pushapp/models.py

# coding: utf-8
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=64)
    text = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

pushapp/views.py

# coding: utf-8
from django.shortcuts import get_object_or_404
from django.views.generic import View
from django.http import HttpResponse
from pushapp.models import Post


class PostView(View):
    def get(self, request, *args, **kwds):
        post = get_object_or_404(Post, pk=kwds['post_id'])
        return HttpResponse(post.text)

pushapp/feedgenerator.py

# coding: utf-8
from django.utils.feedgenerator import Atom1Feed


class PushFeed(Atom1Feed):
    """PuSH(PubSubHubbub)生成用のFeedクラス"""
    HUBHREF_LIST = [
        'https://pubsubhubbub.appspot.com/',
    ]

    def add_root_elements(self, handler):
        super().add_root_elements(handler)
        for hubhref in self.HUBHREF_LIST:
            handler.addQuickElement(
                'link', '', {'rel': 'hub', 'href': hubhref})

pushapp/feeds.py

# coding: utf-8
from django.contrib.syndication.views import Feed
from django.utils.feedgenerator import Atom1Feed
from django.core.urlresolvers import reverse
from pushapp.models import Post
from pushapp.feedgenerator import PushFeed


class LatestEntries(Feed):
    feed_type = Atom1Feed  # AtomFeedを指定します。
    title = 'フィードタイトル'
    link = '/'

    def items(self):
        # 最新の投稿10件を取得します。
        return Post.objects.all().order_by('-created_at')[:10]

    def item_title(self, obj):
        return obj.title  # objにはPostのオブジェクトが入っています。

    def item_description(self, obj):
        return obj.text[0:100]  # textの先頭100文字を返します。

    def item_link(self, obj):
        return reverse('postview', args=[obj.id])

    def item_pubdate(self, obj):
        return obj.created_at  # 作成時刻を返します。


class LatestEntriesPush(LatestEntries):
    feed_type = PushFeed  # PubSubHubbubに対応した形式を指定します。

この記事が役に立った場合、シェアしていただけると励みになります!!