Djangoでタスク管理アプリをつくりたい！ タスク管理編１

# はじめに
前回[（ユーザ情報編）](https://qiita.com/kuma3_it/items/bbca1565a62aac0d0a63)で使用したプロジェクトを引き継いで作成する。

# 開発環境
・Windows10 professional 64bit
・Python 3.7.1
・Django 2.1.2
・MySQL 8.0.12

# タスク管理
## タスク登録
### タスクModelの作成
`models.py`にタスク用のモデルと，プロジェクトとタスクを結びつけるモデルを作成する

```python:models.py

class Task(models.Model):
    PRIOLITY_NUMBER = (
        (0, '未選択'),
        (1, '最低'),
        (2, '低'),
        (3, '常'),
        (4, '高'),
        (5, '最高')
    )

    task_cd = models.AutoField(
        primary_key=True
    )

    task_name = models.CharField(
        max_length=100,
        verbose_name='タスク名'
    )

    user_name = models.CharField(
        max_length=30,
        null=True,
        blank=True,
        verbose_name='担当者名'
    )

    user = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        null=True,
        verbose_name='担当者'
    )

    start_date = models.DateField(
        null=True,
        blank=True,
        verbose_name='開始日'
    )

    end_date = models.DateField(
        null=True,
        blank=True,
        verbose_name='終了日'
    )

    details = models.CharField(
        max_length=200,
        null=True,
        blank=True,
        verbose_name='詳細'
    )

    priolity = models.IntegerField(
        choices=PRIOLITY_NUMBER,
        default=0
    )

    update_date = models.DateField(
        auto_now=True
    )

    is_delete = models.BooleanField(
        default=False
    )

""" プロジェクト To タスク （1 対 多） """
class ProjectToTask(models.Model):
    project_cd = models.ForeignKey(
        Project,
        on_delete=models.CASCADE
    )

    task_cd = models.ForeignKey(
        Task,
        on_delete=models.CASCADE
    )
```

### 登録画面を作成
`our_project.html`を編集し登録画面を作成する

```html:our_project.html(編集前）
<div style="margin:20px"></div>
    <h4>タスクがありません</h4>
</div>
```

```html:our_project.html(編集後）
<div id="taskList">
    {% if task == None %}
        <ul>
            <li>タスクがありません</li>        
        </ul>
    {% else %}
        <ul id="taskAccordion">
            {% for content in task %}
            
                <li class="selectTask task{{content.priolity}}" value="{{content.task_cd}}"> {{content.task_name}} </li> 
            {% endfor %}
        </ul>
    {% endif %}

</div>

<div id="modal-task-add">
    <h4>追加タスク</h4>
    <form name="addTask" method="POST" action="{% url 'task:add_task' project.0.pk %}">
        <table class="table">
            <tr>
                <th>タスク名</th>
                <td><input type="text" name="taskName" style="width:80%;"></td>
            </tr>
            <tr>
                <th>担当</th>
                <td>
                    <select name="user" id="selectC" style="width:80%;">
                        <option value="">---</option>
                        <option value={{ project.0.leader.pk }}>{{ project.0.leader }}</option>
                        {% for person in member %}
                            <option value={{ person.pk }}>{{ person }}</option>
                        {% endfor %}
                    </select>
                </td>
            </tr>
            <tr>
                <th></th>
                <td><input type="text" id="writeCN" name="userName" style="width:80%;"></td>
            </tr>
            <tr>
                <th>詳細</th>
                <td><textarea name="details" rows="4" cols="50" style="width:80%;"></textarea></td>
            </tr>
            <tr>
                <th>開始日</th>
                <td>
                    <input type="date" name="startDate" style="width: 80%;">
                </td>
            </tr>
            <tr>
                <th>終了日</th>
                <td>
                    <input type="date" name="endDate" style="width: 80%;">
                </td>
            </tr>
            <tr>
                <th>優先度</th>
                <td>
                    <input type="range" name="priolity" min="1" max="5" value="3" style="width: 80%;"> &emsp; <span id="priolityNo" style="font-size:14px;">3</span>
                    <br>
                    <p>数値が大きいほど優先度が高いです</p>
                </td>
            </tr>
        </table>
        {% csrf_token %}
    </form>
    <button type="button" id="CTBC" class="btn btn-primary" style="margin-right:10px;">キャンセル</button>
    <button type="button" id="TaskAddAjax" class="btn btn-success" id="addTask">追加</button>
</div>
```

タスク登録画面はモーダルで制御し，Ajaxで通信するのでJavaScriptも編集する

```javascript:modal.js
// タスク追加モーダル管理
$(function () {
    
    //追加ボタンを押した際
    $('#CTB').click(function () {
        //背景を付ける
        $("body").append('<div id="modal-bg"></div>');
        
        //モーダルウィンドウを表示
        $("#modal-bg,#modal-task-add").fadeIn("fast");

        //画面の背景もしくは戻るボタンを押したらモーダルを閉じる
        $('#modal-bg, #CTBC').click(function () {
            $("#modal-bg,#modal-task-add").fadeOut("fast", function () {
                $('#modal-bg').remove();
            });
        });
    });
});
```

```javascript:ajax.js
// プロジェクトタスク追加Ajax
$(function () {
    $('#TaskAddAjax').click(function () {

        //必須事項チェック
        let taskName = $('input[name=taskName]').val();

        if (taskName != "") {

            var form = $('form[name=addTask]');
            $.ajax({
                url: form.prop("action"),
                method: form.prop("method"),
                data: form.serialize(),
                dataType: "JSON",
            })
                .done(function (data) {
                    var result = JSON.parse(data['taskdata'])

                    $('#taskAccordion').empty()
                    
                    $.each(result, function (k, v) {
                        let taskData = v["fields"]

                        let taskHtml = '<li class="task' + taskData["priolity"] + '">' + taskData["task_name"] + '</li>'

                        $('#taskAccordion').append(taskHtml)
                    })
                })
            
        } else {
            alert('タスク名を入力してください')
        }
    })
})
```

CSSも追加します

```CSS:slider.css
input[type=range].vertical {
    -webkit-appearance: none;
    background-color: #0099FF;
    width: 150px;
    height: 4px;
    /*margin-top: 100px;*/
    -webkit-transform:rotate(-90deg);       
    -moz-transform:rotate(-90deg);
    -o-transform:rotate(-90deg);
    -ms-transform:rotate(-90deg);
    transform:rotate(-90deg);
    z-index: 0;
}

input[type=range] {
    -webkit-appearance: none;
    background-color: #0099FF;
    height: 4px;
    width: 300px;
    border: none;
    border-radius: 4px;
}

input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none;
    background-color: #0099FF;
    border: none;
    height: 30px;
    width: 30px;
    border-radius: 50%;
    opacity: 0.8;
}

input[type=range]:focus{
    outline: none;
}

input[type=range]:hover{
    background: #B2E0FF;
}
```

```CSS:adjustment.css
ul, li {
    padding: 0px;
    margin: 0px;
}

#taskList ul li,#taskList ol li{
    border-left: solid 8px black;/*左側の線*/
    border-bottom: solid 2px #dadada;/*下に灰色線*/
    background: #494949;/*背景色*/
    margin-bottom: 5px;/*下のバーとの余白*/
    border-radius: 15px 15px 15px 15px;
    padding: 20px;
    list-style-type: none!important;
    color: white;
    margin: 5px 50px;
}

#taskList .task5{
    border-left: solid 8px rgb(252, 44, 44);/*左側の線*/
}

#taskList .task4{
    border-left: solid 8px rgb(252, 255, 70);/*左側の線*/
}

#taskList .task3{
    border-left: solid 8px rgb(85, 250, 98);/*左側の線*/
}

#taskList .task2{
    border-left: solid 8px rgb(100, 253, 253);/*左側の線*/
}

#taskList .task1{
    border-left: solid 8px rgb(56, 53, 255);/*左側の線*/
}

.inDetail{
    display: none;
}

#taskAccordion li {
    cursor: pointer;
}
```
```CSS:modal.css
#modal-task-add{
    display: none;
    width: 50%;
    height: 100%;
    padding:100px 20px 20px 20px;
    border-radius:10px;
    position: absolute;
    left: 0px;
    right: 0px;
    top: 0px;
    bottom: 0px;
    margin: auto;
    background-color: white;
    position:fixed;
    z-index: 2;
    overflow:auto;
}
```

追加したcssやJavaScriptを`base.html`に追加する

```html:base.html
<link href="{% static 'css/slider.css' %}" rel="stylesheet" type="text/css">

<script src="{% static 'js/ajax.js' %}"></script>
```

画面の準備が完了した

### 登録処理
Ajaxによって渡されたタスク情報を登録し画面に表示する処理を作成する

#### URLルートの作成
`task/urls.py`でパスを作成する

```python:urls.py
urlpatterns = [
    path('', views.taskTop.as_view(), name='task_top'),
    path('build_project/', views.BuildProject.as_view(), name='build_project'),
    path('ProjectPage/<int:pk>/', views.ProjectPage.as_view(), name='our_project'),
    path('ProjectPage/<int:pk>/detail', views.ProjectDetail.as_view(), name='our_project_detail'),
    path('ProjectPage/<int:pk>/update', views.ProjectUpdate.as_view(), name='our_project_update'),
    path('ProjectPage/<int:pk>/delete', views.ProjectDelete.as_view(), name='our_project_delete'),
    path('ProjectPage/<int:pk>/update_member', views.UpdateProjectMember.as_view(), name='update_member'),
    path('ProjectPage/<int:pk>/delete_member', views.ProjectDeleteMember.as_view(), name='our_project_delete_member'),
    path('ProjectPage/<int:pk>/AATask', views.AddTaskForAjax.ajax_response, name='add_task'),    ## 追加
]
```

### 登録して表示する関数の作成
タスクを登録しプロジェクト内のタスク情報を取得する関数を`views.py`に作成する

```python:views.py
from django.http.response import JsonResponse
from django.core import serializers
from .models import ProjectToUsers, Project, ProjectToTask, Task

""" タスク追加 """
class AddTaskForAjax():

    def ajax_response(self, *args, **kwargs):
        taskName = self.POST['taskName']

        if self.POST['user'] != "":
            user = User.objects.get(pk=self.POST['user'])
        else:
            user = None

        if user == None:
            userName = self.POST['userName'] if self.POST['userName'] != "" else None
        else:
            userName = None

        details = self.POST['details'] if self.POST['details'] != "" else None
        startDate = self.POST['startDate'] if self.POST['startDate'] != "" else None
        endDate = self.POST['endDate'] if self.POST['endDate'] != "" else None
        priolity = self.POST['priolity'] if self.POST['priolity'] != "" else None

        task = Task.objects.create(
            task_name=taskName, user=user,
            user_name=userName, details=details,
            start_date=startDate, end_date=endDate,
            priolity=priolity
        )

        projectCd = kwargs['pk']

        project = Project.objects.get(project_cd=projectCd)

        result = ProjectToTask.objects.create(
            project_cd=project, task_cd=task
        )

        projectTask = ManupilateDataBase.getProjectTask(projectCd)

        json_serializer = serializers.get_serializer("json")()
        taskData = json_serializer.serialize(projectTask, ensure_ascii=False)

        return JsonResponse({"taskdata" : taskData})


""" DB操作クラス """
class ManupilateDataBase():

    def getJoinProject(joinCd):
        p_user = ProjectToUsers.objects.filter(user_cd=joinCd)

        if len(p_user) > 0:
            result = []
            for p in p_user:
                member = Project.objects.filter(project_cd=p.project_cd.pk, is_delete=0)
                result.extend(member)
        else:
            result = None

        return result
    
    def getProjectTask(projectCd):
        p_task = ProjectToTask.objects.filter(project_cd=projectCd)

        if len(p_task) > 0:
            result = []
            for t in p_task:
                task = Task.objects.filter(pk=t.task_cd.pk)
                result.extend(task)
        else:
            result = None

        return result

    def getProjectMenber(memberCd):
        p_user = ProjectToUsers.objects.filter(project_cd=memberCd)

        if len(p_user) > 0:
            result = []
            for person in p_user:
                member = User.objects.filter(pk=person.user_cd.pk)
                result.extend(member)
        else:
            result = None

        return result

    def getTaskInfo(taskCd):
        task = Task.objects.filter(task_cd=taskCd)

        if len(task) > 0:
            return task
        else:
            return None

    def getUserInfo(userCd):
        user = User.objects.filter(use_cd=userCd)

        if len(user) > 0:
            return user
        else:
            return None
```
渡されたタスク情報を登録し，登録したタスクとプロジェクトを結びつけるそして，プロジェクトに結びついているタスク情報を取得してJSON形式に変換して返すという処理になっている。
DB関連はクラスを作りそこで処理するように変更した。

### 画面確認
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/25d48717-0026-a520-2cb6-a7411f432f4b.png)

タスク追加ボタンを押しタスク情報を入力して登録する
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/f7bebbad-a9d4-671a-5d87-f0bd6cb8f917.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/4279fbf2-cdb1-5a89-cc1e-a059991d8c54.png)

新しいタスクが表示される
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/2ce24dfb-89f5-689c-7e42-cbaf4f26081e.png)

優先度によって左端の色が変わる
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/759b161e-215b-acb7-558d-e93afafc48a3.png)


## タスク詳細を表示
### 画面の作成
詳細を表示する画面モーダルとAjaxで作成する

htmlに以下を追加

```html:our_project.html
<div id="modal-task-details"></div>
<button type="button" id="TDI" style="display:none;"></button>
```

各AjaxとCSSを編集

```javascript:modal.js
// タスク詳細モーダル管理
$(function () {
    
    //追加ボタンを押した際
    $('#TDI').click(function () {
        //背景を付ける
        $("body").append('<div id="modal-bg"></div>');
        
        //モーダルウィンドウを表示
        $("#modal-bg,#modal-task-details").fadeIn("fast");

        //画面の背景もしくは戻るボタンを押したらモーダルを閉じる
        $('#modal-bg, #CTDI').click(function () {
            $("#modal-bg,#modal-task-details").fadeOut("fast", function () {
                $('#modal-bg').remove();
            });
        });
    });
});
```

```javascript:ajax.js
// タスク詳細取得Ajax
$(function () {
    $('.selectTask').click(function () {

        // タスクコードを取得
        let task_cd = this.value;

        if (task_cd != "") {
            
            $.ajax({
                url: "/task/ajax/getProjectTaskInfo",
                method: "POST",
                data: {task_cd : task_cd},
                dataType: "JSON",
            })
                .done(function (data) {
                    var result = JSON.parse(data['taskdata'])
                    
                    $('#modal-task-details').empty()
                    
                    $.each(result, function (k, v) {
                        let taskData = v["fields"]
                        let userName = ""
                        let details = ""
                        let period = ""

                        let taskHtml = ""
                        let formHtml = ""

                        if (taskData['user'] == "" || taskData['user'] == null) {
                            taskData['user'] = 0;
                        }

                        if (taskData['user_name'] != null) {
                            userName = taskData['user_name']
                        }

                        if (taskData['details'] != null) {
                            details = taskData['details'];
                        }

                        if (taskData['start_date'] != null) {
                            period += taskData['start_date']
                        }

                        if (period != "") {
                            period += " ～ "
                        }

                        if (taskData['end_date'] != null) {
                            period += taskData['end_date']
                        }

                        taskHtml = "<h4>" + taskData['task_name'] + "</h4>"
                            +   "<table class='table'>"
                            +   "   <tr>"
                            +   "       <th>担当</th>"
                            +   "       <td>" + userName + "<span style='display:none'>" + taskData['user'] + "</span></td>"
                            +   "   </tr>"
                            +   "   <tr>"
                            +   "       <th>詳細</th>"
                            +   "       <td>" + details + "</td>"
                            +   "   </tr>"
                            +   "   <tr>"
                            +   "       <th>期間</th>"
                            +   "       <td>" + period + "</td>"
                            +   "   </tr>"
                            +   "</table>"
                        $('#modal-task-details').append(taskHtml)

                        formHtml = "<button type='button' id='CTDI' class='btn btn-primary' style='margin-right:10px;'>閉じる</button>"
                            +   "<button type='button' class='btn btn-primary' style='margin-right:10px;'>変更</button>"
                            +   "<button type='button' class='btn delete-btn'>削除</button>"
                        $('#modal-task-details').append(formHtml)
                    })

                    $('#TDI').click();
                })
            }
        })
    })
```

```CSS:modal.css
#modal-task-add, #modal-task-details{
    display: none;
    width: 50%;
    height: 100%;
    padding:100px 20px 20px 20px;
    border-radius:10px;
    position: absolute;
    left: 0px;
    right: 0px;
    top: 0px;
    bottom: 0px;
    margin: auto;
    background-color: white;
    position:fixed;
    z-index: 2;
    overflow:auto;
}
```

### 詳細情報取関数を作成

`urls.py`と`views.py`を編集しタスク情報を取得する処理を作成

```python:urls.py
from django.urls import path
from . import views

app_name = 'task'

urlpatterns = [
    path('', views.taskTop.as_view(), name='task_top'),
    path('build_project/', views.BuildProject.as_view(), name='build_project'),
    path('ProjectPage/<int:pk>/', views.ProjectPage.as_view(), name='our_project'),
    path('ProjectPage/<int:pk>/detail', views.ProjectDetail.as_view(), name='our_project_detail'),
    path('ProjectPage/<int:pk>/update', views.ProjectUpdate.as_view(), name='our_project_update'),
    path('ProjectPage/<int:pk>/delete', views.ProjectDelete.as_view(), name='our_project_delete'),
    path('ProjectPage/<int:pk>/update_member', views.UpdateProjectMember.as_view(), name='update_member'),
    path('ProjectPage/<int:pk>/delete_member', views.ProjectDeleteMember.as_view(), name='our_project_delete_member'),
    path('ProjectPage/<int:pk>/AATask', views.AddTaskForAjax.ajax_response, name='add_task'),
    path('ajax/getProjectTaskInfo',views.getProjectTaskInfoAjax.ajax_response, name='getPJTask')  ## 追加
]
```

```python:views.py
from django.views.decorators.csrf import csrf_exempt 

""" プロジェクトタスク情報取得 """
class getProjectTaskInfoAjax():

    @csrf_exempt 
    def ajax_response(self, *args, **kwargs):
        task_cd = self.POST['task_cd']

        projectTask = ManupilateDataBase.getTaskInfo(task_cd)

        if (projectTask[0].user != None):
            projectTask[0].user_name = projectTask[0].user.username

        json_serializer = serializers.get_serializer("json")()
        taskData = json_serializer.serialize(projectTask, ensure_ascii=False)

        return JsonResponse({"taskdata" : taskData})
```
`@csrf_exempt`を関数につけることによりCSRF検証をはずせるので，POSTされた値にCSRFトークンがなくても処理が行われる。ただし，検証がないのでCSRFを受ける可能性が上がる
※[CSRF（Cross-Site Request Forgeries)とは](https://www.ipa.go.jp/security/vuln/vuln_contents/csrf.html)

### 画面確認
タスクを押すと画面が詳細が表示されることが確認できる
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/1ff7018a-f1c9-3e3f-e42f-3a785c2999c3.png)


# 今回のまとめ
今回はこのシステムのメインとなるタスク登録を作成した。登録自体は簡単だが，使いやすさや画面遷移の少なさを意識して作ってみた。今後はこの考えをベースに作っていきたい。
また，仕事のほうが忙しくなり，空き時間にプログラムを適当に触っていたためMVCモデルからかけ離れた構成になってしまったので，続きを作る前に構成を整理する。

# シリーズ
 ・[Djangoでタスク管理アプリをつくりたい！ 環境構築編](https://qiita.com/kuma3_it/items/d5adba5fae6d6cefa512)
・[Djangoでタスク管理アプリをつくりたい！ ユーザ認証編](https://qiita.com/kuma3_it/items/8038fac36c41b139e285)
・[Djangoでタスク管理アプリをつくりたい！ ユーザ情報編](https://qiita.com/kuma3_it/items/32edb3854c5154ca6ba6)
・[Djangoでタスク管理アプリをつくりたい！ プロジェクト管理編](https://qiita.com/kuma3_it/items/bbca1565a62aac0d0a63)
・Djangoでタスク管理アプリをつくりたい！ タスク登録編１