django-ckeditorで画像アップロード & カスタマイズ

# なにをやったのか
### 完成形
![upload.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167177/15d94ec8-1691-3eea-c77c-ca2cac0dfdbd.gif)

- django template にWYSIWYGを導入したくて [django-ckeditor](https://github.com/django-ckeditor/django-ckeditor) を入れてみた
- デフォルトだと画像の埋め込みがURLリンクからしかできなかったので、ローカルストレージからアップロードできるようにした
- アップロード画面が日本語化されていなかったので自前で上書きして日本語化した
- 重たい画像をアップロードした時、画面が動かないので動作しているかが不明 -> ローディングイメージを実装

## 環境など
- macOS Mojave 10.14.6
- django-ckeditor 5.8.0

# どうやるのか
## django-ckeditorまわりの設定
### インストールと注意点
1. リポジトリの[README](https://github.com/django-ckeditor/django-ckeditor#id2) にある通りではありますが、普通に `pip install django-ckeditor` します。
2. djangoのsettings.py内の `INSTALLED_APPS` に `ckeditor` を追加します。
3. もしcollectstaticコマンドが実行できる設定がされていなければ、同じくsettings.pyに `STATIC_ROOT` の記述を加え、 `python manage.py collectstatic` します。
4. django-admin以外のTemplateで使用する場合は、Templateに `{{ form.media }}` を記載する必要があります。下記はその例です。
(私はこの設定で小一時間ハマりました…)

```sample.html
  <h2>ブログ記事登録</h2>
  <form method="POST" enctype="multipart/form-data">
    {{ form.media }}
    {{ form.as_p }}
    {% csrf_token %}
    <button type="submit">登録する</button>
  </form>
```

### upload imageプラグインの適用
1. djangoのsettings.py内の `INSTALLED_APPS` に `ckeditor_uploader` を追加します。
2. 同じくsettings.pyに `CKEDITOR_UPLOAD_PATH = 'uploads/'` など、アップロードした画像を保存するパスを指定します。(これはMEDIA_ROOT配下のディレクトリになります)
3. 合わせて、画像ファイルのみのアップロードに制限する `CKEDITOR_ALLOW_NONIMAGE_FILES = False` を指定します。(画像以外もアップロードする場合はこの指定は不要)
4. `CKEDITOR_IMAGE_BACKEND = "pillow"` の指定をsettings.pyに記述することで、アップロードした画像のサムネイルを自動で生成してくれ、後に出てくる画像ブラウジングの際にサムネイルが表示されます。
5. アプリのurlルーティングにckeditor uploaderの設定を追加します。 `path('ckeditor/', include('ckeditor_uploader.urls'))`
<br>公式ドキュメントにもありますが、上記5で追加したurlのdecoratorとして `@staff_member_required` が指定されているため、djangoのデフォルトUsermodelをカスタムしている場合にうまく動かない場合があります。(is_staffカラムを削除している場合など)
<br>その場合は、 `ckeditor_uploader.urls` の中身をラップしてしまえばいいので、 [公式のソース](https://github.com/django-ckeditor/django-ckeditor/blob/master/ckeditor_uploader/urls.py)に倣ってurls.pyを作成し、それをincludeするようにします。
<br>以下は `@staff_member_required` の代わりに `login_required` を使用する例です。

```ckedtiror_custom_urls.py
from __future__ import absolute_import

from django.conf.urls import url
from django.contrib.auth.decorators import login_required
from django.views.decorators.cache import never_cache

from ckeditor_uploader import views

urlpatterns = [
    url(r'^upload/', login_required(views.upload), name='ckeditor_upload'),
    url(r'^browse/', never_cache(login_required(views.browse)), name='ckeditor_browse'),
]
```

ここまでの設定で画像がアップロードできるようになったと思います

## 自前で日本語化
### デフォルトの状態
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167177/aa9b4065-ab41-9572-05e7-50358d4f36f7.png)

デフォルトの状態だと、まず不要なタブが多いですね。
こちらの記事を参考にして、必要最低限のタブだけに絞って日本語も自然に修正しています。

[CKEditorをカスタマイズする](https://qiita.com/tanakaworld/items/a329f864e38fa0a3ca1d)

ckeditorのconfig.jsという設定関連のソースが `static/ckeditor/ckeditor/config.js` にありますが、
こちらをそのまま編集すると後々面倒になりそうだったので、個別にjsを作成してそれを対象のtemplate側で読み込むようにしています。

```ckeditor_config.js
CKEDITOR.on("dialogDefinition", function (ev) {

// 元記事の記述そのままのため中略

});
```

これだけだと、 `サーバブラウザ` を開いたときに以下のように日本語化されていません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167177/5028cc21-3ab5-7737-b88a-6bf34b743b36.png)

これを日本語化していきます。

### browserのtemplateを上書きする
[公式ソースのviews.py](https://github.com/django-ckeditor/django-ckeditor/blob/master/ckeditor_uploader/views.py)を見ると、 `ckeditor/browse.html` をレンダリングしていることがわかります。
なので、[公式ソースのbrowse.html](https://github.com/django-ckeditor/django-ckeditor/blob/master/ckeditor_uploader/templates/ckeditor/browse.html)を参考にして、必要な部分を日本語化します。

で、Djangoの適当なApp配下のtemplates内にckeditorのtemplateとして配置します。 (例: blog/templates/ckeditor/browse.html)
そうすることでライブラリのtemplatesではなく、オリジナルのtemplatesが使用されて表示されるようになります。

## ローディングイメージを実装
画像をローカルから選択し、アップロードボタンを押すと容量の大きな画像の場合、アップロードに時間がかかり画面がしばらく切り替わらない状態となります。
ユーザーからすると何も動いていないように見えるので、アップロード中だということが分かるようローディングイメージを出そうと思います。

先程作成したckeditor_config.jsを編集し、以下を追加します。

```ckeditor_config.js
CKEDITOR.on( 'instanceReady', function(ev){
    const editor = ev.editor;

    editor.on('fileUploadRequest', function(ev){
        const fileLoader = ev.data.fileLoader;
        // Notificationsをbindすることでメインの編集画面に通知が出るが、今回は別Dialogなのであまり意味がない。
        // CKEDITOR.fileTools.bindNotifications(fileLoader.editor, fileLoader);

        fileLoader.on("uploading", function () {
            console.log("アップロード開始");
        });

        fileLoader.on("uploaded", function () {
            console.log("アップロード完了");
        });

        fileLoader.on("error", function () {
            console.log("アップロードエラー");
        });

        fileLoader.on("abort", function () {
            console.log("アップロード失敗");
        });
    });
});
```

上記では、CKEDITORの `instanceReady` イベントにhookして、ファイルアップロードWidgetである `fileLoader` を取得し、
`fileLoader` に定義されている各イベントごとにリスナーをセットしています。
これらのイベントにhookして、それぞれローディングイメージの表示/非表示を制御してあげればいいです。

# さいごに
- ckeditorは導入自体は楽だけど、カスタマイズしようと思うと本家のソースとかまで読まないといけなくて結構大変。
- ただ、自前でWYSIWYG作るのはもっと大変なので、それに比べればすごい楽。
- 繰り返しになりますが、 [こちらの記事](https://qiita.com/tanakaworld/items/a329f864e38fa0a3ca1d)には本当にお世話になりました。