夜明け前の最も暗いとき

技術的なやったことをメモするブログ

Python + Djangoで動画共有サイトを作る Part2

前回の続きです。引き続きPythonDjangoで作っていきます。

ページの表示

まず、Django内にアプリケーションとしてプロジェクトを作成します。

$ python3 manage.py startapp video
$ ls
manage.py  mysite  video

これで新しくvideoフォルダができました。これからは主にvideoフォルダにあるファイルを編集していきます。フォルダの中は下記のようになっています。

$ ls video/
admin.py  apps.py  __init__.py  migrations  models.py  tests.py  views.py

新しくアプリケーションのurls.pyを作成します。urls.pyはブラウザからリクエストされたURLをどのように処理するのかを記述します。

  • video/urls.py
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from . import views

app_name = 'video'
urlpatterns = [
    path('', views.index, name='index'),
]

まずはインデックス("/")のみのURLを取り扱い、views.pyに記載されたindex関数を呼び出して処理します。name='index'は後ほど'index'という文字からURLを生成できるようするため設定します。 また、video/urls.pyをルートのURL設定に反映する必要があります。

  • mysite/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('video/', include('video.urls')),
    path('admin/', admin.site.urls),
]

次に、urls.pyから呼び出されるindex関数をviews.pyに定義します。

  • video/views.py
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse, HttpResponseRedirect

def index(request):
    return render(request, 'video/index.html')

render関数はHTMLのひな形を読み込んで内容を動的に生成し表示します。ひな形はアプリケーションのtemplateフォルダに設置します。

  • video/templates/video/index.html
{% extends "video/base.html" %}
{% block main %}
Hello, World
{% endblock %}

テンプレートを利用する利点の一つはは共通部をまとめることが可能な点です。extendsはvideo/base.htmlを参照しblockを必要に応じて置き換えます。

  • video/templates/video/base.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="robots" content="noindex,nofollow">
  <meta http-equiv="Pragma" Content="no-cache">
  <meta http-equiv="Cache-Control" Content="no-cache">
  <title>{% block title %}Video Manager{% endblock %}</title>
{% load static %}
  <link rel="stylesheet" type="text/css" href="{% static 'video/main.css' %}">
{% block header %}{% endblock %}
</head>
<body>

<h1><a href="{% url 'video:index' %}">Video Service</a></h1>
{% block main %}

{% endblock %}
</body>
</html>

urlテンプレートタグは指定した文字列を基にurls.pyを探索しURLを返します。これによって、URLの変更をurls.pyですれば各々のテンプレートを変更せずとも全体に反映されるようになります。 また、cssファイルのような動的に生成されないものについてはアプリケーションのstaticフォルダに設置します。

  • video/static/video/main.css
body {
        background-color:#FFFFFF;
}

最後に、テンプレートを有効にするためsettings.pyのINSTALLED_APPSにvideo.apps.VideoConfigを追加します。

  • mysite/settings.py
# Application definition

INSTALLED_APPS = [
    'video.apps.VideoConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

それでは、runserverを実行してhttp://192.168.12.9:8000/video/にブラウザから接続します。 問題がなければHello, Worldが表示されます。

サイトの設計

動画サイトを作成するにあたってどのような要素が必要になるのか検討します。 動画情報として以下の要素が挙げられます。

  • 管理ID
  • タイトル
  • 説明文
  • タグ情報
  • アップロード日
  • ファイル名

データベースにはこれらの情報が保存される必要があります。 タグ情報は複数つけることができます。また、複数の動画に同じタグが付けられます。 これは動画の管理IDとタグの管理IDを紐づけることで実現します。

以上の検討したデータベースをmodel.pyにクラスとして記述します。

  • video/model.py
from django.db import models

class VideoContent(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    upload_date = models.DateTimeField()
    original_name = models.CharField(max_length=200)
    filename = models.CharField(max_length=200, default="")
    thumb_frame = models.IntegerField(default=0)

class VideoTagName(models.Model):
    name = models.CharField(max_length=200, default="")

class VideoTagList(models.Model):
    content = models.ForeignKey(VideoContent, on_delete=models.CASCADE)
    tag = models.ForeignKey(VideoTagName, on_delete=models.CASCADE)

変更をデータベースに反映します。

$ python3 manage.py makemigrations video
Migrations for 'video':
  video/migrations/0001_initial.py
    - Create model VideoContent
    - Create model VideoTagName
    - Create model VideoTagList
$ python3 manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, video
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying sessions.0001_initial... OK
  Applying video.0001_initial... OK

これでデータベースに作成したクラスからテーブルが作成されました。

次に、どのようなページが必要か検討します。おおよそ以下のページがあると良さそうです。

  • トップページ
  • 検索結果
  • アップロードページ
  • 動画情報編集ページ
  • 動画削除ページ
  • 視聴ページ

各ページではURLに動画IDやタグ名を必要とします。これらを考慮してurls.pyを書き換えます。

  • video/urls.py
urlpatterns = [
    path('', views.index, name='index'),
#    path('<int:page>', views.index, name='index'),
#    path('tag/<str:tag_name>', views.tag, name='tag'),
#    path('tag/<str:tag_name>/<int:page>', views.tag, name='tag'),
#    path('search/', views.search_post, name='search'),
#    path('search/<str:search_word>', views.search, name='search'),
#    path('search/<str:search_word>/<int:page>', views.search, name='search'),
#    path('watch/<int:content_id>/', views.watch, name='watch'),
#    path('upload/', views.UploadView.as_view(), name='upload'),
#    path('edit/<int:content_id>', views.edit, name='edit'),
#    path('edit/<int:content_id>/thumb/<int:frame>', views.thumb, name='thumb'),
#    path('delete/<int:pk>', views.DeleteView.as_view(), name='delete'),
#    path('update/<int:content_id>', views.update, name='update'),
#    path('update/tag/<int:content_id>', views.update_add_tag, name='update_add_tag'),
#    path('update/tag/<int:content_id>/<str:tag_name>', views.update_remove_tag, name='update_remove_tag'),
]

コメントアウトしてある部分はviews.pyにまだ実装していない箇所であるため、実装したら外します。

トップページの作成

トップページはタグ表示領域と動画リストの2カラムで構成します。関連タグを左側、動画のリストを右側で表示します。表示するページ数は10とし、下部にページナビゲーションを付けます。

  • video/templates/video/index.html
{% extends "video/base.html" %}

{% block main %}

<div class="left">
  <br>
  <a href="./upload/">Upload Video</a>
  <br><br>
  <h4>関連タグ</h4>
  <ul class="taglist">
  {% for item in tags %}
    <li><a href="{% comment %}{% url 'video:tag' item.name %}{% endcomment %}">{{ item.name }}</a> ({{ item.count }})</li>
  {% endfor %}
  </ul>
</div>

<div class="center">
  <form action="{% comment %}{% url 'video:search' %}{% endcomment %}" method="post">
    {% csrf_token %}
    <input name="search_text" type="text" size="50" value="">
    <input type="submit" value="検索">
  </form>
  <br>
  {% for item in contents %}
    <div class="content">
      <a href="{% comment %}{% url 'video:watch' item.id %}{% endcomment %}"><img src="/media/video/{{ item.id }}/thumb.jpg"></a>
      <div class="detail">
        <a href="{% comment %}{% url 'video:watch' item.id %}{% endcomment %}"><h4>{{ item.title }}</h4></a>
        <br>
        {% for tag in item.tags %}
          <a href="{% comment %}{% url 'video:tag' tag.tag.name %}{% endcomment %}">{{ tag.tag.name }}</a>
        {% endfor %}
        <br>
        <br>
        <a href="{% comment %}{% url 'video:edit' item.id %}{% endcomment %}">Edit</a>
      </div>
    </div>
    {% endfor %}

  <br>

  {% if page.word != '' %}
    <a href="{% url page.type page.word 0 %}">最初</a>/
  {% else %}
    <a href="{% url page.type 0 %}">最初</a>/
  {% endif %}

  {% for item in page.list %}
    {% if item.valid %}
      {% if item.num == page.current %}
        <b>{{ page.current }}</b>/
      {% else %}
        {% if page.word != '' %}
          <a href="{% url page.type page.word item.num %}">{{ item.num }}</a>/
        {% else %}
          <a href="{% url page.type item.num %}">{{ item.num }}</a>/
        {% endif %}
      {% endif %}
    {% endif %}
  {% endfor %}

  {% if page.word != '' %}
    <a href="{% url page.type page.word page.max %}">最後</a><br>
  {% else %}
     <a href="{% url page.type page.max %}">最後</a><br>
  {% endif %}
  <br><br>
</div>

{% endblock %}

{% url ~ %}についてはurls.pyで実装していないと逆引きできないため、実装した後に{% comment %}~{% endcomment %}を外して有効にします。 テンプレートではrender関数に渡す辞書変数を使い動的にページを生成します。tags, contents, pageについてはviews.pyで定義します。

2カラムにするためのcssを書いていきます。

  • video/static/video/css.main
h4 {
        margin: 0px;
        padding:0px;
}

 /* 左サイドバー */
.left {
        float: left;
        width: 200px;
        margin: 0px 20px 0px 5px;
}
.taglist{
        margin : 0px;
        padding: 10px 25px;
        border: solid 1px #CCCCCC;
}

/* 真ん中 */
.center {
        min-width: 600px;
        position: absolute;
        left: 200px;
        border: solid 1px white;
        margin: 0px 10px 10px 20px;
        overflow: visible;
}
.footer{
        clear: both;
        overflow :hidden;
}
div .content {
        margin : 2px;
        padding: 3px;
        border: solid 0px #6666FF;
        border-top-width: 1px;
        border-bottom-width: 1px;
        min-height: 150px;
        position: relative;
}
div .content:hover {
        background-color: #DDDDFF;
}
div .detail {
        min-width: 200px;
        position: absolute;
        left: 200px;
        top: 0px;
        margin: 10px 10px 10px 20px;
        overflow: visible;
}

/* Watchページ */
video {
        min-width:320px;
        min-height:240px;
        width: 100%
}

続いてviews.pyを編集してサーバ側の動作を記述します。トップページではタグを多い順で関連タグとして表示し、最近投稿された順で動画を表示します。

index関数ではページの情報や表示する動画のリストを用意します。

  • video/views.py
from .models import VideoContent, VideoTagList, VideoTagName

def index(request, page=0):
    max_page = VideoContent.objects.count() // 10
    return construct_page(request, VideoTagList.objects.values('content_id'), VideoContent.objects.order_by('-upload_date')[page*10:(page+1)*10].values(), page, max_page, 'video:index')

トップページは処理がタグ検索やキーワード検索と重なるところが多いため実質的なページの作成はconstruct_page関数で実施します。 views.pyに以下のconstruct_page関数を追加します。

  • video/views.py
from django.db.models import Count

def construct_page(request, all_content_ids, page_contents, current_page, max_page, url_type, url_word=''):
    # page_contents(動画)に関連するタグを抜き出し、テンプレートで使えるよう整形
    contents = []
    for item in page_contents:
        tmp_dict = item
        tmp_dict.update({'tags': VideoTagList.objects.filter(content_id=item['id']).select_related('tag')})
        contents.append(tmp_dict)

    # all_content_idsからタグを多い順で集計し、整形する
    tag_cnt = VideoTagList.objects.filter(content__in = all_content_ids).values('tag').annotate(tag_count=Count('tag')).order_by('-tag_count')[:10]
    tag_names = [VideoTagName.objects.filter(id = item.get('tag'))[0] for item in tag_cnt]
    tags = [{'name': tag_names[i].name, 'count': tag_cnt[i]["tag_count"]} for i in range(len(tag_names))]

    # ページが有効な範囲をvalidでマークを付ける
    page_list = [{'num':x, 'valid':0 <= x and x <= max_page} for x in range(current_page-5, current_page+4)]

    return render(request, 'video/index.html', {'tags': tags, 'contents': contents, 'page':{'type':url_type, 'word': url_word, 'current': current_page, 'max': max_page, 'list': page_list}})

ORマッパーは初めて使いましたがSQLを知っていると苦戦しますね。

表示処理を書いたので動作させてみます。Djangoでは管理用ページからデータベースを操作することができます。まずは管理者用のアカウントを作成します。

$ python3 manage.py createsuperuser
Username (leave blank to use 'jinglan'): admin
Email address:
Password:
Password (again):
Superuser created successfully.
$

続いて、アプリケーションのモデルを登録します。表示するモデルや項目はadmin.pyに記述します。

  • video/admin.py
from django.contrib import admin
from .models import VideoContent, VideoTagName, VideoTagList


class VideoContentAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'upload_date')

class VideoTagListAdmin(admin.ModelAdmin):
    list_display = ('content', 'tag')

class VideoTagNameAdmin(admin.ModelAdmin):
    list_display = ('name',)

admin.site.register(VideoContent, VideoContentAdmin)
admin.site.register(VideoTagList, VideoTagListAdmin)
admin.site.register(VideoTagName, VideoTagNameAdmin)

管理用ページの設定が終わったので表示します。ブラウザからhttp://192.168.12.9:8000/admin/にアクセスします。 認証後、以下のようにモデルが表示されデータベースの操作ができます。

f:id:jianlan:20190804163016j:plain

Video contentにaddボタンからレコードを追加します。

f:id:jianlan:20190804163034j:plain

Video tagにTAG1とTAG2レコードを追加します。

f:id:jianlan:20190804163121j:plain

登録した動画とタグをVideo TagListで紐づけます。

f:id:jianlan:20190804163253j:plain

アプリケーションに戻ってトップページを表示します。

f:id:jianlan:20190813011315j:plain

動画リスト及び関連するタグが表示され、トップページが完成しました。

Djangoで動画共有サイトを作る Part3へ続きます。

 

トラブルシューティング

ORマッパーのクエリを確認する。

どうしてもSQLの動作を確認したい時があります。Djangoではshellからコマンドをインタラクティブに確認できる機能を持っています。

$ python3 manage.py shell
Python 3.6.8 (default, Apr 25 2019, 21:02:35)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-36)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

ORマッパーオブジェクトのqueryを参照することでSQL文を表示できます。

>>> from video.models import VideoContent, VideoTagList, VideoTagName
>>> rec = VideoTagList.objects.filter(content_id=1)
>>> print(rec.query)
SELECT `video_videotaglist`.`id`, `video_videotaglist`.`content_id`, `video_videotaglist`.`tag_id` FROM `video_videotaglist` WHERE `video_videotaglist`.`content_id` = 1

生成されたSQLの動作はコマンドラインから確認することができます。

$ mysql django_db -u django -p
Enter password:

MariaDB [django_db]> SELECT `video_videotaglist`.`id`, `video_videotaglist`.`content_id`, `video_videotaglist`.`tag_id` FROM `video_videotaglist` WHERE `video_videotaglist`.`content_id` = 1;
+----+------------+--------+
| id | content_id | tag_id |
+----+------------+--------+
|  1 |          1 |      1 |
+----+------------+--------+
1 row in set (0.00 sec)