言いたいことは例によって見出しの通りです。例えば以下の記事みたいな感じで video タグでの動画再生を導入してみました。
動画フィールドを実装するに当たって、すでに実装済みの画像フィールドと今回実装する動画フィールドでディレクトリ構成や URL 構成が違うのがイヤだったので、メディアファイルの持ち方を全体的に見直しました。もともとdjango-cloudinary-storage
を便利に使っていたのですが、リソースが動画となるといろいろ考えるべき点が出てきたため、すべてのImageField
をCloudinaryField
に変更することにしました。今回この作業を行うに当たって、Django 自体のアップデートも行いました。
- Django==2.2.8
+ Django==3.1.6
django-bootstrap4==2.2.0
django-cleanup==5.0.0
- django-cloudinary-storage==0.2.3
+ django-cloudinary-storage[video]==0.3.0
django-debug-toolbar==2.2
django-environ==0.4.5
django-summernote==0.8.11.6
djangorestframework==3.11.1
google-api-python-client==1.10.0
Janome==0.3.10
oauth2client==4.1.3
Pillow==7.2.0
psycopg2==2.8.5
psycopg2-binary==2.8.5
pyOpenSSL==19.1.0
uWSGI==2.0.19.1
モデルの変更点を書いていきます。変更前のモデル構成をまとめた別記事は以下となります。
まずはインポートから。
from cloudinary.models import CloudinaryField
記事モデルにはアイキャッチ画像があるのでこれを変更します。ディレクトリに/%Y/%m/%d/
のような日付情報は入れられないようです。さほど困ることではないので、これまでと違うディレクトリ構成にしました。
""" 記事 | メイン """
class Post(Base):
・・・
title = models.CharField(verbose_name='タイトル', max_length=128)
subtitle = models.CharField(verbose_name='サブタイトル', max_length=128, null=True, blank=True)
category = models.ForeignKey(Category, verbose_name='カテゴリー', null=True, blank=True, on_delete=models.SET_NULL)
tag = models.ManyToManyField(Tag, verbose_name='タグ', blank=True)
related_posts = models.ManyToManyField('self', verbose_name='関連記事', blank=True)
- eyecatch = models.ImageField(verbose_name='アイキャッチ画像', upload_to='image/post/eyecatch/%Y/%m/%d/', default='image/post/eyecatch/default/default.png',null=True, blank=True)
+ eyecatch = CloudinaryField(verbose_name='アイキャッチ画像', null=True, blank=True, overwrite=True, resource_type="auto", folder="media/image/eyecatch", tags="EyeCatch")
description = models.TextField(verbose_name='説明', blank=True, null=True)
text = models.TextField(verbose_name='本文', blank=True, null=True)
・・・
記事の本文で表示する画像は個別のモデルImage
として実装しており、管理サイトのPost
詳細画面でインライン表示しています。これにも変更を加えます。
""" 画像 | 記事の本文で利用 """
class Image(Base):
class Meta:
db_table = 'image'
verbose_name = '画像'
verbose_name_plural = '画像'
ordering = ['index']
index = models.IntegerField(verbose_name='並び順', null=True, blank=True)
post = models.ForeignKey(Post, verbose_name='記事', on_delete=models.PROTECT)
title = models.CharField('タイトル', max_length=255, blank=True, help_text='画像の alt 属性として利用されます')
- image = models.ImageField(verbose_name='画像', upload_to='image/post/text/%Y/%m/%d/', null=True, blank=True, help_text='保存後、本文挿入用 HTML を生成します')
+ image = CloudinaryField(verbose_name='画像', null=True, blank=True, help_text='保存後、本文挿入用 HTML を生成します', overwrite=True, resource_type="auto", folder="media/image/post/", tags="Post")
def __str__(self):
return < 省略 >
次に管理サイトのPost
詳細画面でインライン表示するためのVideo
モデルを新規で作成します。
+ """ 動画 | 記事の本文で利用 """
+ class Video(Base):
+ class Meta:
+ db_table = 'video'
+ verbose_name = '動画'
+ verbose_name_plural = '動画'
+ ordering = ['index']
+
+ index = models.IntegerField(verbose_name='並び順', null=True, blank=True)
+ post = models.ForeignKey(Post, verbose_name='記事', on_delete=models.PROTECT)
+ title = models.CharField('タイトル', max_length=255, blank=True, help_text='動画の alt 属性として利用されます')
+ video = CloudinaryField(verbose_name='動画', null=True, blank=True, help_text='保存後、本文挿入用 HTML を生成します', overwrite=True, resource_type="auto", folder="media/video/", tags="Video")
+
+ def __str__(self):
+ return < 省略 >
ImageField
からCloudinaryField
に差し替えた個所はほかにもありますが割愛します。続いてadmin.py
です。
+ class VideoInline(admin.StackedInline):
+ model = Video
+ ordering = ('index',)
+ extra = 1
+ readonly_fields = ('id', 'created_at', 'updated_at')
・・・
class PostAdmin(SummernoteModelAdmin):
model = Post
- inlines = [ImageInline, LinkInline]
+ inlines = [ImageInline, VideoInline, LinkInline]
・・・
投稿画面にこんな感じのフォームが挿入されます。
あとは、Xbox Gaming Bar で動画を撮影して、Windows フォトでトリミングすればいいでしょう。Windows 標準で十分でした。
書いてみると短いのですが、ImageField
のまま実装しようとして失敗したり、FileField
で実装しようとして失敗したり、django-cloudinary-storage
の仕様を確認したりでけっこう試行錯誤しました。その過程で不要なマイグレーションファイルが溜まってしまったので、一度環境をクリーンアップして DB データを再ロードしました。こういう作業するたびにコンテナで運用していてよかったなと思います。画像の数がまだ少ないので、再アップロードと本文内の URL の差し替えは手でやりました・・