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](https://startbootstrap.com/)のテンプレートをダウンロードする。

##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
```

<img width="1356" alt="スクリーンショット 2017-07-01 14.44.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/177049/a8bbd436-6dee-dca6-ac96-7f11b15bf251.png">

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