はじめに
初めて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
こんな感じで表示されれば完成!