はじめに
- Django の良い使い方やアプリケーションのキレイな作り方に関する理解をさらに深めたいので、手頃なサイズのオープンソースプロジェクトを読み進めてみる
- 今回のお題はこれ https://github.com/vitorfs/woid
- いくつかのサイトのトップニュースを定期的にクローリングし、一覧表示するような Web サービス
- Web アプリケーション全体としての部分とクローリング部分両方いろいろ勉強になるので、2 回に分けて進めていく
- Web サービスとしての機能にはあまり言及せずに、自分の勉強になる書き方をピックアップする
全体を俯瞰する
- Two Scoops of Django とかで紹介されているディレクトリ構造とはやや異なるが
- apps
- accounts
- 完全にブランクのファイルのみ
- 名前からユーザー管理のためのアプリケーションだと想像
- ユーザー管理系の機能は利用するフィールドなどのカスタマイズを行わない場合、Django のデフォルトの機能を利用することで簡単に作成することができる
- core
- view に一つ関数が定義されているだけ
- services
- Web アプリケーションとしてのメインのロジックやクローラーはこの中に定義されている
- settings
- 設定はこのディレクトリ配下にまとまっている
- templates
- template もこのディレクトリ配下にまとまっている
基本的に services という名前のアプリケーション内を見ていく形になります。
Model とその周辺
まずはデータ定義を行う model 周りから見ていきます。主に追加で定義されている関数に学びがありました。
/woid/woid/apps/services/models.py
class Service(models.Model): GOOD = 'G' ERROR = 'E' CRAWLING = 'C' CURRENT_STATUS = ( (GOOD, '✓ good'), (ERROR, '× error'), (CRAWLING, '~ running') ) name = models.CharField(max_length=255) slug = models.SlugField(max_length=30, unique=True) url = models.URLField() story_url = models.URLField() last_run = models.DateTimeField(null=True, blank=True) status = models.CharField(max_length=1, default=GOOD, choices=CURRENT_STATUS) class Meta: verbose_name = 'service' verbose_name_plural = 'services' ordering = ('name',) def __str__(self): return self.name def to_dict(self): return { 'name': self.name, 'slug': self.slug, 'url': self.url } def get_story_template(self): template = 'services/includes/{0}_story.html'.format(self.slug) return template
- choices を利用するとき、タプルのバリューの方はユーザーが見るときの文字列にする
- to_dict 関数は後ほど view の中で出てくる
- get_story_template(self) 関数は template をすっきり書くようにできている
slug は他のフィールドの値を URL に利用したい場合につける名前
なお、最初にデータをロードするときのために dixtures という機構が使われています。
/woid/apps/services/fixtures/services.json
View とその周辺
view はそこまで珍しい使い方をしている印象は無いが、json 形式で返すか HTML をレンダリングして返すかを分岐するような作りが各関数で定義されていた
/woid/apps/services/views.py
@cache_page(60) def front_page(request): today = timezone.now() stories = list() services = Service.objects.all() for service in services: top_story = service.stories.filter(status=Story.OK, date=today).order_by('-score').first() if top_story: stories.append(top_story) subtitle = today.strftime('%d %b %Y') if 'application/json' in request.META.get('HTTP_ACCEPT'): stories_dict = map(lambda story: story.to_dict(), stories) dump = json.dumps({ 'stories': stories_dict, 'subtitle': subtitle }) return HttpResponse(dump, content_type='application/json') else: return render(request, 'services/front_page.html', {'stories': stories, 'subtitle': subtitle})
- @cache_page() というアノテーションは URL 単位(?確か)でキャッシュを任意の時間保持するためのもの
- HTTP_ACCEPT が application/json の時は json を返却、そうでない場合は HTML をレンダリングして返却している
ちゃんと意図が理解できているわけではないが、おそらく API 利用もできるようにしている?
本番環境のサイトを触ってみてもちょっとわからなかった - json 形式で送るために map()で dict を作っているが、その中で呼ばれているのが先程 models.py で定義されていた to_dict()という関数
Template とその周辺
続いて Template を見てみます。下記はトップページが呼ばれたときに利用される Template です。
/woid/templates/services/front_page.html
{% extends 'base.html' %}
{% load static %}
{% block javascript %}
<script src="{% static 'js/services/stories.js' %}"></script>
{% endblock %}
{% block content %}
<h2>front page <small>({{ subtitle }})</small></h2>
<ul class="stories">
{% for story in stories %}
{% include story.get_template with append_service=True %}
{% endfor %}
</ul>
{% endblock %}
story.get_template の部分で先程の models.py で定義されていた関数を呼び出しています。その処理を追っていくと、下記にたどり着きます。
def get_story_template(self): template = 'services/includes/{0}_story.html'.format(self.slug) return template
- 各 service(クローリング先のサイト)用に用意されているパーツ化された template を取りに行くための処理
このように構築することで、template 内で if 文とかを書かずに対応できるようになっている