Django Tips ーDjangoでランキングサイトを作るー


はじめに

初めてDjangoでアプリを作ったので、その時にあった課題や解決法の備忘録です。
今回はAtCoderのランキングサイトを作りました。
MVCモデル(DjangoだとMTV?)を全体像を理解するのに苦労しました。
参考(http://mimumimu.net/blog/2011/11/21/python-django-%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F%E3%80%82-mtv-model-%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/)

0. Webアプリの構想を練る

そもそもどんなサービスを作りたいか
->
* Model - どんなModelを作るか、どんなModelのリレーションがあるか
* View - どんなページを作るか、どのModelを使うか
...

1. Djangoでアプリを作成

$ django-admin startproject project-name
$ cd project-name
$ python manage.py startapp app-name

2. Templateをダウンロードする

HTML,CSS,JavaScriptを書くのがめんどくさかったので、Bootstrapのテンプレートをダウンロードする。

3. テンプレートを運用する

Djangoでは、HTMLを/project/app/templatesフォルダに入れて、CSS,JavaScriptを/project/static/appフォルダに入れる。

HTMLファイルの処理:

-BaseとなるHTMLファイル-
ナビゲーションバーや、読み込むファイルを{% static 'project/app/file' %} として書き込む。
<title>の中身に{% block title %} {% endblock %}、
<body>の中身に{% block body %} {% endblock %}を書く。

-ExtendするHTMLファイル-
HTMLファイルの<title>の中身を{% block title %} {% endblock %}、
HTMLファイルの<body>の中身を{% block body %} {% endblock %}の中に書く。

4. model.pyを編集

0.で考えたModelを実装する。

今回の場合は、デフォルトではなく、自分のUserでログインできるようにしたことがポイント。
models.py (一部抜粋)

from django.contrib.auth.models import (BaseUserManager, AbstractBaseUser)

class UserManager(BaseUserManager):
    def create_user(self, username, email, password, **extra_fields):
        now = timezone.now()
        if not email:
            raise ValueError('Users must have an email address.')
        email = UserManager.normalize_email(email)
        user = self.model(
            username=username,
            email=email,
            is_active=True,
            last_login=now,
            date_joined=now,
            **extra_fields
        )
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, username, email, password, **extra_fields):
        user = self.create_user(username, email, password)
        user.is_active = True
        user.is_staff = True
        user.is_admin = True
        user.is_superuser = True
        user.save(using=self._db)
        return user


class User(AbstractBaseUser):
    username = models.CharField(_('username'), max_length=30, unique=True,)
    arc_user_name = models.CharField(_('arc name'), max_length=15, blank=True)
    email = models.EmailField(verbose_name='email address', max_length=255, unique=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    is_admin = models.BooleanField(default=False)
    is_superuser = models.BooleanField(default=False)
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
    delete = models.BooleanField(default=0)
    score = models.IntegerField(default=0)
    main_language = models.CharField(max_length=15, default='')
    objects = UserManager()

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

    def email_user(self, subject, message, from_email=None):
        send_mail(subject, message, from_email, [self.email])

    def has_perm(self, perm, obj=None):
        return True

    def has_module_perms(self, app_label):
        return self.is_admin

    def get_short_name(self):
        "Returns the short name for the user."
        return self.arc_user_name

    def __str__(self):
        return self.username

5. views.pyを編集

  • 基本的にクラスベース汎用ビューを用いて書いた。
  • Userの登録や、結果の投稿はCreateView, 他はTemplateView。
  • LoginRequiredMixinを用いることで、ログインしているUserのみが閲覧できるようにする。
  • 外部のライブラリは一括でatcoder_ranking.commons.librariesで管理すると、コードが綺麗になる。
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView, CreateView
from atcoder_ranking.commons.libraries import *
from .models import *


class IndexView(LoginRequiredMixin, TemplateView):
    template_name = 'index.html'

    def get(self, _, *args, **kwargs):
        users = User.objects.all()
        results = Result.objects.all().select_related('user')
        for user in users:
            user.score = len(results.filter(user=user)) * 100
            language_list = [result.result_language for result in results.filter(user=user)]
            if language_list != []:
                # もっとも数が多い言語を主言語として選択
                user.main_language = Counter(language_list).most_common(1)[0][0]
            user.save()

        context = super(IndexView, self).get_context_data(**kwargs)
        context['users'] = User.objects.all().order_by('-score')[: 3]

        return render(self.request, self.template_name, context)

6. url.pyを編集

作ったviews.pyに対して作る。

from django.conf.urls import url
from django.contrib import admin

import ranking.views as ranking_view

urlpatterns = [
    # admin
    url(r'^admin/?', admin.site.urls),

    # top page
    url(r'^$', ranking_view.TopView.as_view()),

    # ranking
    url(r'^ranking/?$', ranking_view.IndexView.as_view()),
    url(r'^ranking/result.png$', ranking_view.plotResults),
    url(r'^ranking/create/$', ranking_view.CreateUserView.as_view()),
    url(r'^ranking/problems/$', ranking_view.AtCoderProblemsView.as_view()),
    url(r'^ranking/get_problems/$', ranking_view.GetProblemsView.as_view()),
    url(r'^ranking/posts/$', ranking_view.PostsView.as_view()),
    url(r'^ranking/posts/(?P<posts_id>[0-9]+)/$', ranking_view.PostsDetailView.as_view()),
    url(r'^ranking/create_posts/$', ranking_view.CreatePostsView.as_view()),
    url(r'^ranking/login/$', ranking_view.LoginView.as_view(), name='login'),
    url(r'^ranking/logout/$', ranking_view.logout, name='logout')
]

7. test.pyを編集

そのまま書くもよし、Seleniumを使うもよし。
factory_boyを使ってModelオブジェクトを生成するテストを作った。

factory.py

from atcoder_ranking.commons.libraries import *

from ranking.models import *


class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User
    username = factory.Sequence(lambda n: 'testing_user{}'.format(n))
    arc_user_name = factory.Faker('name')
    email = factory.Sequence(lambda n: 'testuser{}@gmail.com'.format(n))
    password = factory.PostGenerationMethodCall(
        'set_password', 'ranking_password')

test.py

class IndexViewTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.user = UserFactory.create()

    def setUp(self):
        self.client = ServiceTestTool.login(Client(), IndexViewTest.user, 'ranking_password')

    def test_index_ranking(self):
        # create 10 testusers
        users = [UserFactory.create() for i in range(10)]
        # create posts for testusers:
        result1 = [ResultFactory.create(user=users[1]) for i in range(30)]
        result2 = [ResultFactory.create(user=users[2]) for i in range(20)]
        result3 = [ResultFactory.create(user=users[3]) for i in range(40)]
        result4 = [ResultFactory.create(user=users[4]) for i in range(10)]
        result5 = [ResultFactory.create(user=users[5]) for i in range(5)]
        result6 = [ResultFactory.create(user=users[6]) for i in range(50)]

        response = self.client.get('/ranking/')

        self.assertContains(response, users[1].username)
        self.assertContains(response, users[3].username)
        self.assertContains(response, users[6].username)
        self.assertNotContains(response, users[2].username)
        self.assertNotContains(response, users[4].username)
        self.assertNotContains(response, users[5].username)
        self.assertNotContains(response, users[0].username)

8. testを動かしてみる

$ python manage.py test

9. ページを動かす

$ python manage.py runserver

こんな感じで表示されれば完成!