sitemap.xml の ping 送信は既にこちら で、仕込んでいるのですが、
即時(のような感覚) ではindexされることはなく、結構なタイムラグがあります。
何かいいのがないか、まさぐっていたところ、rss、atom feed も ping で送信しなさい。
とか、
pubsubhubbub で rss、atom feed を送信しなさい というのが、出てきましたので、
pubsubhubbub の pub を 実装してみました。
pubsubhubbub とは?
以下のサイトが参考になりました。
mezzanine での pubsubhubbub
mezzanine の blog モジュール は、 rssv2.0、atom feed をサポートしていますが、
pubsubhubbub 形式 での feed 出力 と 通知を行う機能はありません。1,2
なので、その部分は自前で実装する必要があります。
標準で blog モジュール のfeed 機能 は提供されていて、
rss、 atom が出力できます。
※1. blog モジュールを使うと、 blog ページに rssv2.0 atom feed へのリンクが付与されます。
※2. カテゴリごとの feed と 全ての投稿に対する feed の2種類があります。
どう実装したか?
作ったもの一式
作ったもの一式はこちらに上げました。
Pypi にも登録を済ましたので、pip 実行でもダウンロード可能です。
pip install mezzanine_pubsubhubbub_pub
フォルダ配下の init.py、defaults.py、feeds.py、models.py、urls.py、views.py について、
以下説明を記載します。
init.py
あまり、関数を書いたりするところではないのかと勝手に考えていましたが、
参考にしていたdjango-push に結構処理が記述してあり、
そのままあまり考えもなく踏襲しています。
package配下の代表する関数を置くとかそう言ったレベルの処理を書くには実はちょうど良い場所なのかと思います。
-
get_feed_url_patterns
feed url pattern を返します。
mezzanine.blog に依存し、そのfeed url を上書きするため、
インストールされていれば、url を includeし、
末インストールなら、空のurl pattren を返すようにしました。
from django.conf import settings
ではなく、
from mezzanine.conf import settings
を使うようにしていますが、
これは、mezzanine の settings でないと、defaluts.py で登録した設定値が見えなかったりしたためです。
mezzanine の plugin 書く且つ、defaluts.py を使う場合は、
mezzanine の settings を使ったほうがトラブルがなくてよさそうです。
def get_feed_url_patterns(): """ Returns feed url patterns if mezzanine.blog is installed. You must call this method before include("mezzanine.urls") """ from mezzanine.conf import settings from django.conf.urls import include, url blog_installed = "mezzanine.blog" in settings.INSTALLED_APPS if blog_installed: BLOG_SLUG = settings.BLOG_SLUG.rstrip("/") if BLOG_SLUG: BLOG_SLUG += "/" feed_url_patterns = [ url("^%s" % BLOG_SLUG, include("mezzanine_pubsubhubbub_pub.urls")), ] return feed_url_patterns else: return []
-
ping_hub
django-push から拝借しました。
ほぼ、from django.conf import settings
を
from mezzanine.conf import settings
に変えただけです。
def ping_hub(feed_url, hub_url=None):
"""
Makes a POST request to the hub. If no hub_url is provided, the
value is fetched from the PUSH_HUB setting.
Returns a dictionary with `requests.models.Response` object to the value
"""
from mezzanine.conf import settings
if hub_url is None:
hub_url = getattr(settings, 'PUSH_HUB', None)
if hub_url is None:
raise ValueError("Specify hub_url or set the PUSH_HUB setting.")
params = {
'hub.mode': 'publish',
'hub.url': feed_url,
}
results = {}
for elem in hub_url:
result = requests.post(elem, data=params, headers={'User-Agent': UA})
results.update({elem: result})
return results
defaults.py
設定値のみ記載します。
-
PUSH_HUB
通知先のHUBサーバーのURLを指定します。
Tapleで複数指定可能です。
複数指定時は、rss feed 、 atom feed に hub url が複数追加されます。
当初は、editable=True
で指定していましたが、
管理画面からTapleの編集がうまくできなかったため、
editable=False
としています。
何かうまいやり方があるのかもしれませんが、見つけられませんでした。 -
PUSH_URL_PROTOCOL
HUBサーバーに通知する Feed URL のプロトロルを指定します。
サイトによってはHTTP/HTTPS双方で送るケースもあるかもしれないので、
BOTH
も設定可能です。
choices=PROTOCOL_TYPE_CHOICES,
でTapleを設定可能で、
editable=True,
にしておくと、管理画面上以下のように、出力されます。 -
管理画面
feeds.py
feeds.py も django-push からの流用です。
もともとの実装が、rss feed がなかったため、rss feed 向けのclass を作成しました。
rss の場合は、タグの記載がatom feedとは異なるため、以下のように、
タグがatom:link
となるように実装しています。
- HubRss201rev2Feed
class HubRss201rev2Feed(Rss201rev2Feed):
def add_root_elements(self, handler):
super(Rss201rev2Feed, self).add_root_elements(handler)
hub = self.feed.get('hub')
if hub is not None:
for elem in hub:
handler.addQuickElement('atom:link', '', {'rel': 'hub',
'href': elem})
models.py
models.py といってもmodel の記述はあまりなく、signal 登録を行うようにしました。
mezzanine で blog 投稿した際、公開されている投稿であれば、hubサーバーに通知する
signal を作成しました。
HubBlogPost で ModelMixin を継承して、BlogPost を拡張しています。
signal 登録しているのは、HubBlogPost に対してですが、
ModelMixin で 拡張したクラスは、mezzanine の標準の model クラスと置きかわるため、
管理画面からBlog 投稿した場合、hubサーバーに通知されます。
extra_model_fields
でも拡張は行えますが、ModelMixin を使うとより細かい制御が行えるのかと思います。
今回の使い方だと、直接BlogPostに登録してしまえば良いのかもしれませんが。。
class HubBlogPost(ModelMixin):
class Meta:
mixin_for = BlogPost
def notify_blog_post(sender, instance, **kwargs):
if instance.status == CONTENT_STATUS_PUBLISHED:
site = Site.objects.get(id=current_site_id())
protocol_type = getattr(settings, 'PUSH_URL_PROTOCOL', None)
if protocol_type == PROTOCOL_TYPE_HTTP:
__ping_hub_http(site)
elif protocol_type == PROTOCOL_TYPE_HTTPS:
__ping_hub_https(site)
elif protocol_type == PROTOCOL_TYPE_BOTH:
__ping_hub_http(site)
__ping_hub_https(site)
else:
raise ValueError("PUSH_URL_PROTOCOL is None...")
post_save.connect(notify_blog_post, sender=HubBlogPost)
urls.py
mezzanine/urls.py at master · stephenmcd/mezzanine から、
feed url の部分のみ抜粋しています。
mezzanine の デフォルトのfeed を上書きしたいためです。
# Trailing slahes for urlpatterns based on setup.
_slash = "/" if settings.APPEND_SLASH else ""
# Blog patterns.
urlpatterns = [
url("^feeds/(?P<format>.*)%s$" % _slash,
views.blog_post_feed, name="blog_post_feed"),
url("^tag/(?P<tag>.*)/feeds/(?P<format>.*)%s$" % _slash,
views.blog_post_feed, name="blog_post_feed_tag"),
]
views.py
こちらも、
mezzanine/views.py at master · stephenmcd/mezzanine から、
部分抜粋して作成しました。
views.py の blog_post_feed を コピペして、Feedのクラスのみ変更しています。
from mezzanine_pubsubhubbub_pub.feeds import HubPostsRSS, HubPostsAtom
def blog_post_feed(request, format, **kwargs):
"""
Blog posts feeds - maps format to the correct feed view.
"""
try:
return {"rss": HubPostsRSS, "atom": HubPostsAtom}[format](**kwargs)(request)
except KeyError:
raise Http404()
クイックスタート
Github上の、README.jp.md を参照してください。
※ 是非、お願いします。
実際に効果があるのか?
いや、これは少し微妙です。
そもそもサイトの評価が高い、低いがあるのかもしれません。
投稿して公開の際、通知はしているけど、1日経ってもindexされていない、はあります。
Sitemap送信よりはマシ くらいに思ってもらった方が良いのかもしれません。
Pypi にUpload した package は 本当に即時でindexされていたので、
送ってるにしろそもそもの評価はあるんだろうなと思います。
Google Admin Console で Fetch するのが1番良いのは間違いない気がする。。
その他Tips
テスト時にぶつかって対処したことを2点記載します。
mezzanine.utils.tests.TestCase について
django.test.TestCase と 似たような存在でmezzanine.utils.tests.TestCase がいて、
mezzanine の mezzanine/tests.py at master · stephenmcd/mezzanine では
以下のような具合に mezzanine.utils.tests.TestCase が使われています。
class PagesTests(TestCase):
def setUp(self):
"""
Make sure we have a thread-local request with a site_id attribute set.
"""
super(PagesTests, self).setUp()
from mezzanine.core.request import _thread_local
request = self._request_factory.get('/')
request.site_id = settings.SITE_ID
_thread_local.request = request
_thread_local.request = request
みたいなことをやっていて、これまずいらなそうだと思い、
tests.py を記述していたのですが、test実行時に以下のエラーで落ちました。
File "/Library/Python/2.7/site-packages/mezzanine/blog/feeds.py", line 68, in add_domain
return add_domain(self._site.domain, link, self._request.is_secure())
AttributeError: 'NoneType' object has no attribute 'is_secure'
どうも mezzanine の内部で、current_request()
というメソッドの記述があり、
このメソッドの戻りがなくて、エラーになっていました。
_thread_local.request = request
でtestの際に値を設定すると、
current_request()
で戻りが取得できるようです。
xml.etree.ElementTree を使っていて atom:link タグが取得できなかった
test を書いていて、atom:link
タグが取得できず、
root = ET.fromstringlist(response._container)
elems = root.findall(".//atom:link")
root = ET.fromstringlist(response._container)
elems = root.findall(".//link")
と書いてみたりしていましたが、
root = ET.fromstringlist(response._container)
elems = root.findall(".//{http://www.w3.org/2005/Atom}link")
で取得できるようです。
:
は使えず、 名前空間指定で無事取得できました。
@patch.object を使う際は、テストメソッドに第2引数が必要になる。
テストケースを以下のように実装したところ、
@patch.object(requests, "post", side_effect=mocked_requests_post)
def test_feed_atom(self):
resolver_match = resolve('/blog/feeds/atom/')
blog_post_feed = resolver_match.func
# Create an instance of a GET request.
from mezzanine.core.request import _thread_local
request = self._request_factory.get('/blog/feeds/atom/')
request.site_id = settings.SITE_ID
_thread_local.request = request
response = blog_post_feed(request, format=resolver_match.kwargs['format'])
import xml.etree.ElementTree as ET
root = ET.fromstringlist(response._container)
elem = root.find("{http://www.w3.org/2005/Atom}link")
self.assertEqual('hub', elem.attrib['rel'])
self.assertEqual('https://pubsubhubbub.appspot.com/', elem.attrib['href'])
エラーとなりました。
======================================================================
ERROR: test_feed_atom (tests.tests.BlogPostFeedTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Library/Python/2.7/site-packages/mock/mock.py", line 1305, in patched
return func(*args, **keywargs)
TypeError: test_feed_atom() takes exactly 1 argument (2 given)
@patch.object のように、デコレータでテストを記述する際は、第二引数が必要で、
以下のように記述する必要がありました。
@patch.object(requests, "post", side_effect=mocked_requests_post)
def test_feed_atom(self, mock_post):
resolver_match = resolve('/blog/feeds/atom/')
blog_post_feed = resolver_match.func
# Create an instance of a GET request.
from mezzanine.core.request import _thread_local
request = self._request_factory.get('/blog/feeds/atom/')
request.site_id = settings.SITE_ID
_thread_local.request = request
response = blog_post_feed(request, format=resolver_match.kwargs['format'])
import xml.etree.ElementTree as ET
root = ET.fromstringlist(response._container)
elem = root.find("{http://www.w3.org/2005/Atom}link")
self.assertEqual('hub', elem.attrib['rel'])
self.assertEqual('https://pubsubhubbub.appspot.com/', elem.attrib['href'])
実装時に参考にしたサイト
長くなりましたが、
以上です。
コメント