前回の記事では、タスク完了ボタンを実装しました。
前回の記事では、タスクの編集フォームを作成しました。 [sitecard subtitle=関連記事 url=https://freemas.stepupkaraoke.com/python/django/making-todolis[…]
今回はシリーズの最終回として、これまでの内容のまとめとしてタスク編集を便利にします。
現在は別画面に遷移してからの編集となるので、これをタスク一覧ページ内で行えるようにしましょう。
タスク一覧画面で「編集」ボタンを押すと、編集用のフォームが表示されます。
その状態で「編集完了」ボタンを押すと編集が完了するイメージです。
この記事を読んで取り組むことで、以下を習得することができます。
- jQueryを使用したインラインフォームの概要と使い方
- DjangoとAjaxの連携方法
- Djangoのフォームバリデーション
- DjangoでのJsonResponseについて
新しいことはやりませんが、やや深い内容を扱います。一つ一つゆっくり、しっかりと確認していきましょう。
DjangoでTODOリストを作ろう!最終回:タスク編集を便利にしよう!
今回は修正箇所が限られるために、フォームの表示→Viewの修正→データのAjaxでの送受信の順で実装していきましょう。
タスク編集フォームを表示する箱をHTMLで作る
タスクの編集は「編集」ボタンをクリックしたときに行が丸ごと編集フォームに切り替わるようにします。
まずはtask_list.htmlを以下のように修正しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{% for task in tasks %} <tr data-id="{{ task.id }}" {% if task.is_completed %} class="completed"{% endif %} class="task-display"> <td class="task_title">{{ task.title }}</td> <td class="task_due_date">{{ task.due_date | date:"Y年m月d日" }}</td> <td class="flex"> <button class="btn btn-primary btn-sm toggle-completed" data-id="{{ task.id }}"> {% if task.is_completed %}未完了にする{% else %}完了にする{% endif %} </button> <!-- <a href="{% url 'task_edit' task.id %}" class="btn btn-secondary">編集</a> --> <!-- コメントアウト --> <!-- 1.jQuery用ボタン --> <button class="edit-task btn btn-secondary" data-id="{{ task.pk }}">編集</button> <!-- 追記 --> <form method="post" action="{% url 'task_delete' task.id %}" style="display:inline;"> {% csrf_token %} <button type="submit" class="btn btn-danger" onclick="return confirm('本当に削除してもよろしいですか?')">削除</button> </form> </td> </tr> <!-- 2.jQueryによる編集フォーム表示場所 --> <tr class="edit-form-container" data-id="{{ task.id }}"></tr> <!-- 追記 --> {% empty %} |
画面遷移が必要なくなったので、ボタンの種類を変えています。
JSにてよく使われるbuttonに変更し、data-idにはタスクのIDを表示するようにしています。
編集ボタンをクリックしたときに表示するフォームを入れるための要素を追加しています。中身はJSファイルで設定するので、空になっています。
data-id属性にタスクのIDを設定し、このIDを用いてタスク更新フォームが表示される対象となるタスクの特定を行います。
タスク編集フォームを表示する箱の中身と処理をjQueryで作る
次にscript.jsファイルに編集ボタンクリック時に上記のtr要素の中に編集用フォームを表示する処理を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
//編集ボタンを押したときにフォームを出現させる $(document).ready(function() { $('.edit-task').click(function() { //1.情報取得 var taskId = $(this).data('id'); var taskName = $('tr[data-id="' + taskId + '"] .task_title').text().trim(); var taskDueDate = $('tr[data-id="' + taskId + '"] .task_due_date').text().trim(); //2.日付の形式変換 var convertedDate = taskDueDate.replace(/年/g, '-').replace(/月/g, '-').replace(/日/g, ''); //3.HTMLフォーム生成 var formHtml = ` <td> <input type="text" class="form-control task-name-input-${taskId}" value="${taskName}" required> </td> <td> <input type="date" class="form-control task-due-date-input-${taskId}" value="${convertedDate}"> </td> <td> <button type="submit" class="btn btn-primary edit-submit" data-id="${taskId}">編集完了</button> </td> `; //4.表示行を隠して、編集フォームを表示 $('tr[data-id="' + taskId + '"].edit-form-container').html(formHtml); $('tr[data-id="' + taskId + '"].task-display').hide(); $('tr[data-id="' + taskId + '"].edit-form-container').show(); }); }); |
またdefaultをFalseに設定することで、マイグレーションしたときに既存のデータのis_completedにFalseを設定しています。
まず、編集ボタンがクリックされた時に、そのタスクのID、タスク名、期限を取得します。これは、編集フォームに表示する内容で、編集ボタンのdata-id属性から取得することができます。
取得した期限は「YYYY年MM月DD日」という書式であるため、フォームに表示する前に「YYYY-MM-DD」に変換する必要があります。
この変換は、上記のようにreplace関数を使用して行います。
タスク更新フォームのHTMLを作成します。これはtask_list.htmlに追加したtr要素の中に追加する内容となります。
初期値であるvalueには取得した内容を表示しています。
またそれぞれclass名の最後にタスクIDを付与しているのは、他の行を更新してしまわない配慮です。
最後に、編集対象のタスクのタスク表示部分を非表示にし、タスク更新フォーム部分を表示します。
これでタスク表示からタスク編集に、その行だけが瞬時に切り替わったように見えます。
タスク更新ViewをAjax対応用に修正する
続いてviews.pyのタスク更新処理を修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# #CREATE VIEWを使う場合 # class TaskCreate(CreateView): # model = Task # fields = ['title', 'due_date'] # success_url = reverse_lazy('task_list') # class TaskUpdate(UpdateView): # model = Task # fields = ['title', 'due_date'] # template_name = 'task_edit.html' # success_url = reverse_lazy('task_list') # def get_form(self, form_class=None): # form = super().get_form(form_class) # form.fields['due_date'].widget = DateInput(attrs={'type': 'date'}) # due_dateフィールドのウィジェットを上書き # return form #1.Viewで作成 class TaskUpdate(View): def post(self, request, *args, **kwargs): #2.タスクを取得 task = get_object_or_404(Task, pk=kwargs['pk']) #3.送信データをチェック form = TaskForm(request.POST, instance=task) #4.問題がない場合はデータ登録 if form.is_valid(): form.save() data = { 'title': task.title, 'due_date': str(task.due_date) if task.due_date else "", } return JsonResponse(data) #5.問題がある場合はエラー処理 else: errors = form.errors.as_json() return HttpResponse(errors, status=400, content_type='application/json') |
既存のUpdateViewでは、レスポンスをHTMLとして返すため、Ajax通信での更新には適していません。
そこで、Viewを継承して、postメソッド内にAjax通信に必要な処理を記述します。
以下、修正した処理の詳細です。
まず、TaskUpdateクラスは、UpdateViewではなくViewクラスを継承しています。
UpdateViewに比べて記述量は増えますが、今回のようなカスタマイズの場合は拡張性に富むこちらを利用します。
受け取ったpkの値を元に、modelからタスクのデータを取得しています。
pkはユニークになるので、一つのデータが取得されます。
TaskFormは、Djangoのフォームフレームワークを使用して作成されたフォームで、上で取得したデータをセットしています。
これを行うことでデータが更新のフォームに合致しているかどうかをバリデーションしています。
フォームが正常にバリデーションを通過した場合は、form.save()メソッドを呼び出してフォームを保存します。
そして、更新されたタスクの情報をJSON形式でレスポンスとして返します。
フォームがバリデーションに失敗した場合はform.errors属性からエラーを取得し、それをJSON形式でレスポンスとして返します。
この場合、HTTPステータスコード400を指定しています。
今回は実にシンプルなアプリであること、使用する人数を一人と想定していること、エラーを引き起こすには意図的かつ手間をかける必要があることを踏まえて少し手抜きしています。
実際の業務の場合はエラー処理は重要な要素なので、さまざまなエラーを引き起こすケースの対策を考え、実装する必要があることを覚えておいてください。
さて、このコードでは通常のDjangoビューと異なり、HTMLテンプレートを使用しないためJSON形式でレスポンスを返しています。
これにより、Ajaxによる非同期通信に適したビューが実現されます。最後にAjaxでのデータの送受信処理を実装しましょう。
Ajaxを使用した非同期のタスク更新処理をJSで実装する
まずはtask_list.htmlの完了ボタンを少し修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
//編集処理 $(document).ready(function() { //動的に生成された要素に対してはonイベントを追加しないといけない。 $(document).on('click', '.edit-submit', function() { //1.データを取得 var taskId = $(this).data('id'); var taskName = $('tr[data-id="' + taskId + '"]').find('.task-name-input-' + taskId).val(); var taskDueDate = $('tr[data-id="' + taskId + '"]').find('.task-due-date-input-' + taskId).val(); $.ajax({ url: '/djangotodo/' + taskId + '/edit/', type: 'POST', dataType: 'json', data: { 'title': taskName, //2.期限日の空欄処理1 'due_date': taskDueDate || null, 'csrfmiddlewaretoken': $('input[name="csrfmiddlewaretoken"]').val(), }, success: function(data, textStatus, xhr) { $('tr[data-id="' + taskId + '"] .task_title').text(data.title); //3.期限日の空欄処理2 var dueDate = data.due_date ? new Date(data.due_date) : null; //4.日付の変換処理 var formattedDueDate = dueDate ? dueDate.getFullYear() + '年' + (dueDate.getMonth() + 1).toString().padStart(2, '0') + '月' + dueDate.getDate().toString().padStart(2, '0') + '日' : ''; $('tr[data-id="' + taskId + '"] .task_due_date').text(formattedDueDate); $('tr[data-id="' + taskId + '"].task-display').show(); $('tr[data-id="' + taskId + '"].edit-form-container').hide(); }, }); }); }); |
基本的な流れは前の記事でのAjax送受信処理と同じなので、一部異なっている部分のみ説明します。
IDを使用して、タスク名と期日を取得します。この処理で、「編集完了」ボタンを押した行のデータのみを取得しています。
期限に関しては入力しない場合もあるので、未入力の場合はnullをセットしています。
この処理をしないと未入力の場合にデータベースに誤った形式で登録されてしまうために要注意です。
上記とは違い、このコードでは編集後のデータをタスク一覧に戻しています。
Ajaxでの通信は画面のリロードを挟まないために、jQueryの処理で更新後のデータを表示しなくてはなりません。
この処理をしないと期限日が未入力の場合に表示が変になってしまうために要注意です。
期限日の表示も「YYYY年MM月DD日」の形式なので、「YYYY-MM-DD」から変換しています。
最後にタスクの表示と編集フォームを切り替えれば、タスクの更新が一瞬で行われたように見せかけることができます。
タスク編集が出来るかを確認する
お疲れ様でした。それでは実際に編集ができるかどうかテストしてみましょう。
開発環境に接続して「編集」ボタンを押してみてください。
タスク編集フォームが表示されたら、内容を変更して「編集完了」ボタンを押してみましょう。
上記のように画面のリロードなくタスクが更新されれば成功しています。
おわりに
以上で、Djangoを使ってTODOリストアプリの作成は終了となります。全6回と長丁場になりましたが、お疲れ様でした。
完成品のソースはGithubに掲載しておくので、確認したい場合には参考にしてください。
さて、TODOリストは作成しましたが、やろうと思えばいくらでも改良は可能です。
例えば
- TODOリストの中に登録、削除の2つのformがあるので1つにまとめる
- そもそもHTMLのformを撤廃し、登録、削除、編集のすべてをjQueryで行う
- TODOリストのタスクを並び替えられるようにする
- 親と子の関係を導入してみる
- マウスのドラッグで順番を入れ替えられるようにする
などです。もし思いつくような改良プランがあれば、これまでのシリーズで培った知識を駆使しつつ、ネットで調べるなどしてぜひともチャレンジしてみてください!