Django+tinyMCEで画像アップロードの機能をつけた話

#概要
DjangoにWYSIWYGエディタでの記事投稿機能をつける際に画像投稿もできたらいいなと思って実装。
条件は
・Ajaxによるユーザーに不可視なとこでの通信
・<img>タグは自動付与
・CSRF対策

意外と大変だったので備忘録を書いておく。ちなみに対象読者はこれから自身のアプリに組み込もうとしている人。

#必要なパッケージとか
①Django(当然か)
②Pillow(DBで画像を使うのに必要)
③django-tinymce4-lite(tinyMCEをdjangoで利用するためのパッケージ)

全部pipでインストール可能

#実際の作業
コマンドラインにて（もちろんproject内で実行）

```
django-admin startapp log //好きな名前でかまわない:記事を管理する人
django-admin startapp api //好きな名前でかまわない:画像のPOSTをする人
```

log/models.py

```
from django.db import models
from tinymce import HTMLField
from datetime import datetime

class log(models.Model):
    title=models.CharField(max_length=100)#欲しけりゃつけて
    pub_date=models.DateTimeField(default=datetime.now)#欲しけりゃつけて
    author=models.CharField(max_length=100)#欲しけ(ry
    user_pk=models.IntegerField()#欲しけ(ry
    body=HTMLField("content")#絶対つけて
    def __str__(self):
        return self.title

```

もしユーザーに使用させたい意図があるならそれ用にviews.pyとurls.pyとforms.pyを改造。adminだけでよいなら、admin.pyのみいじればよい。

つづいてapiについて
api/models.py

```
from django.db import models
import <もとのプロジェクト名>.settings as conf
import os

def delete_previous_file(function):

    def wrapper(*args, **kwargs):

        self = args[0]

        # 保存前のファイル名を取得
        result = Image.objects.filter(pk=self.pk)
        previous = result[0] if len(result) else None
        super(Image, self).save()

        # 関数実行
        result = function(*args, **kwargs)

        # 保存前のファイルがあったら削除
        if previous:
            os.remove(conf.MEDIA_ROOT + '/' + previous.image.name)
        return result
    return wrapper

# Create your models here.
class Image(models.Model):

   
    @delete_previous_file
    def delete(self, using=None, keep_parents=False):
        super(Image, self).delete()

    image = models.ImageField(upload_to='user_upload/')#絶対つけよう
    ref= models.BooleanField(default=1)#もし削除機能もつけたいならあったほうがいいかも
    owner=models.IntegerField()#ほしかったらつけよう
    name=models.URLField()#もし削除機能もつけたいならあったほうがいいかも

    def __str__(self):
        return self.image.name
```
api/views.py

```
from django.shortcuts import render
from django.views.decorators.csrf import csrf_protect
from django.http.response import JsonResponse,HttpResponseNotFound
from .models import Image
# Create your views here.

@csrf_protect
def PostImageHandler(request):
    if request.FILES:
        upload_image = Image()
        upload_image.image=request.FILES["file"]
        upload_image.owner=request.user.id
        upload_image.save()
#ここからは削除機能もつけたい人向け
        upload_image.name="/media/"+upload_image.image.name
        upload_image.save()
#ここまで
        json={ "location" : '/media/'+upload_image.image.name }
        return JsonResponse(json)
    else:
        return HttpResponseNotFound("<h1>404</h1>")

```

ここまでいったらmigrationとmigrateも忘れずに

次はsettings.py（必要な部分のみ抜粋）

```

import os

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_ROOT = os.path.dirname(BASE_DIR)

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'tinymce',
    'api.apps.ApiConfig',
    'log.apps.LogConfig',
]


TINYMCE_DEFAULT_CONFIG = {
    'height': 360,
    'width': '100%',
    'cleanup_on_startup': True,
    'theme': 'modern',
    'plugins': '''
    textcolor save link image media preview  contextmenu
    table lists fullscreen  insertdatetime  nonbreaking
    contextmenu directionality searchreplace wordcount fullscreen autolink lists  charmap print  hr
    anchor pagebreak imagetools paste code
    ''',
    'toolbar1': '''
    fullscreen preview bold italic underline | forecolor backcolor | alignleft alignright |
    aligncenter alignjustify | indent outdent code | searchreplace  table |
    | link image |
    ''',
    'contextmenu': 'formats | link image',
    'menubar': False,
    'statusbar':True,
    'custom_undo_redo_levels': 20,
    'automatic_upload':True,
    'selector': 'textarea',
    'paste_data_images':False,
    'images_upload_credentials': False,
    'relative_urls' : False,
    'force_p_newlines' : False,
    'force_br_newlines' : True,
    'forced_root_block' : 'div',
    'element_format':'html',
    'content_css':'/path/to/custom.css',(あると便利、画像とかの大きさ調整に)
    }

TINYMCE_CALLBACKS={

    'images_upload_handler':"""
    function (blobInfo, success, failure) {
        var xhr, formData;
        var name;
        name='csrftoken'
        var cookieValue = null;
        if (document.cookie && document.cookie != '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = jQuery.trim(cookies[i]);
                if (cookie.substring(0, name.length + 1) == (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        var csrftoken =  cookieValue;
        xhr = new XMLHttpRequest();
        xhr.withCredentials = true;
        xhr.open('POST', '/api/image_post/');
        xhr.setRequestHeader("X-CSRFToken",csrftoken)

        xhr.onload = function() {
          var json;

          if (xhr.status != 200) {
            failure('HTTP Error: ' + xhr.status);
            return;
          }

          json = JSON.parse(xhr.responseText);

          if (!json || typeof json.location != 'string') {
            failure('Invalid JSON: ' + xhr.responseText);
            return;
          }
          success(json.location);
        };

        formData = new FormData();
        formData.append('file', blobInfo.blob(), blobInfo.filename());

        xhr.send(formData);
        }
        """
}

STATIC_URL = '/static/'
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, "static"),
)
STATIC_ROOT=os.path.join(PROJECT_ROOT,"static")

MEDIA_URL="/media/"
MEDIA_ROOT=BASE_DIR+"/media/"

```


#最後に

もし削除機能をつけたいのであれば次のように実装すると良いかもしれないです。

画像を保存する際適当なフラグを予め立てる（私の場合はrefという名前でつけている）
↓
編集したコンテンツを保存する前に全体を精査し、書かれているimgタグのsrc部分を取り出しImageテーブルのあるdbで検索にかける。
↓
ヒットしたらフラグをへし折る。
↓
フラグが立っているのをバッチ処理で一日おきに消す

このような実装をするとよろしいかと。

授業の合間の20分+家に帰ってからの10分で書いたのでかなり端折っている自信しかないから、需要ありそうならもっと詳しく書く。

以上。

#参考になるかもしれないサイト

https://www.tiny.cloud/docs-preview/
tinyMCEのドキュメント（英語）